Skip to content

Instantly share code, notes, and snippets.

@volodalexey
Forked from brattonc/README.md
Last active October 24, 2022 21:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save volodalexey/64a2e9444a36f66322e37d8416919387 to your computer and use it in GitHub Desktop.
Save volodalexey/64a2e9444a36f66322e37d8416919387 to your computer and use it in GitHub Desktop.
D3 v5 ES6 Liquid Fill Gauge (optimized for React or Vue)

Liquid Fill Gauge Forked from https://gist.github.com/brattonc/5e5ce9beee483220e2f6

optimized for React or Vue

It means you can invoke renderChart() as many times as you wish.

E.g. in React:

import React, { Component } from 'react'
import { renderChart, destroyChart } from '...'

class Chart extends Component {
  ...
  render () {
    return <div ref={(el) => this.$wrapper = el}/>
  }
  
  componentDidUpdate () {
    renderChart(this.$wrapper, this.props.data)
  }
  
  componentWillUnmount () {
    destroyChart(this.$wrapper)
  }
  ...
}

E.g. in Vue:

<template>
  <div ref="$wrapper"></div>
</template>

<script>
import { renderChart, destroyChart } from '...'

export default {
  name: 'chart',
  ...
  watch: {
    data () {
      renderChart(this.$refs.$wrapper, this.data)
    }
  },
  beforeDestroy () {
    destroyChart(this.$refs.$wrapper)
  }
  ...
}
</script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>D3 v5 ES6 Liquid Fill Gauge (optimized for React or Vue)</title>
<script src="//d3js.org/d3.v5.min.js"></script>
</head>
<body>
<div id="wrapper"></div>
<script>
function renderChart (wrapper, curData) {
if (!wrapper) {
return
}
const {
select: d3Select, scaleLinear: d3ScaleLinear,
arc: d3Arc, area: d3Area, active: d3Active,
interpolate: d3Interpolate, easeLinear: d3EaseLinear,
} = d3
const width = 300
const height = 300
const minValue = 0
const maxValue = 100
const initialWaveHeight = 0.3
const textSize = 1
const initialCircleThickness = 0.1
const initialCircleFillGap = 0.2
const waveCount = 1
const circleColor = '#accbea'
const textColor = '#5a98d5'
const waveColor = '#5a98d5'
const waveTextColor = '#ffffff'
const textVertPosition = 0.8
const waveRiseTime = 2000
const waveAnimateTime = 2000
const waveOffset = 0
const fillPercent = Math.max(minValue, Math.min(maxValue, curData)) / maxValue
const textRounder = (value) => String(parseFloat(value).toFixed(2))
const svgData = d3Select(wrapper).selectAll('svg').data([curData])
const svgEnter = svgData.enter().append('svg') // append only on enter
const radius = Math.min(width, height) / 2
const locationX = width / 2 - radius
const locationY = height / 2 - radius
svgEnter.attr('width', width)
svgEnter.attr('height', height)
const gEnter = svgEnter.append('g')
.attr('transform', 'translate(' + locationX + ',' + locationY + ')')
.attr('class', 'liquid-gauge')
const svgMerge = svgData.merge(svgEnter)
const waveHeightScale = d3ScaleLinear()
.range([0, initialWaveHeight, 0])
.domain([minValue, minValue + (maxValue - minValue) / 2, maxValue])
const textPixels = (textSize * radius / 2)
const startValue = 0
const percentText = '%'
const circleThickness = initialCircleThickness * radius
const circleFillGap = initialCircleFillGap * radius
const fillCircleMargin = circleThickness + circleFillGap
const fillCircleRadius = radius - fillCircleMargin
const waveHeight = fillCircleRadius * waveHeightScale(fillPercent * 100)
const waveLength = fillCircleRadius * 2 / waveCount
const waveClipCount = 1 + waveCount
const waveClipWidth = waveLength * waveClipCount
// Scales for drawing the outer circle.
const gaugeCircleX = d3ScaleLinear().range([0, 2 * Math.PI]).domain([0, 1])
const gaugeCircleY = d3ScaleLinear().range([0, radius]).domain([0, radius])
// Scales for controlling the size of the clipping path.
const waveScaleX = d3ScaleLinear().range([0, waveClipWidth]).domain([0, 1])
const waveScaleY = d3ScaleLinear().range([0, waveHeight]).domain([0, 1])
// Scales for controlling the position of the clipping path.
const waveRiseScale = d3ScaleLinear()
.range([(fillCircleMargin + fillCircleRadius * 2 + waveHeight), (fillCircleMargin - waveHeight)])
.domain([0, 1])
const waveAnimateScale = d3ScaleLinear()
.range([0, waveClipWidth - fillCircleRadius * 2]) // Push the clip area one full wave then snap back.
.domain([0, 1])
// Scale for controlling the position of the text within the gauge.
const textRiseScaleY = d3ScaleLinear()
.range([fillCircleMargin + fillCircleRadius * 2, (fillCircleMargin + textPixels * 0.7)])
.domain([0, 1])
// Draw the outer circle.
const gaugeCircleArc = d3Arc()
.startAngle(gaugeCircleX(0))
.endAngle(gaugeCircleX(1))
.outerRadius(gaugeCircleY(radius))
.innerRadius(gaugeCircleY(radius - circleThickness))
gEnter
.append('path')
.attr('d', gaugeCircleArc)
.style('fill', circleColor)
.attr('transform', 'translate(' + radius + ',' + radius + ')')
// Text below the wave
gEnter
.append('text')
.attr('class', 'below-wave-text')
.attr('text-anchor', 'middle')
.attr('font-size', textPixels + 'px')
.style('fill', textColor)
.text(textRounder(startValue) + percentText)
.attr('transform', 'translate(' + radius + ',' + textRiseScaleY(textVertPosition) + ')')
svgMerge
.select('.below-wave-text')
.transition()
.duration(waveRiseTime)
.on('start', (d, i, group) => {
const element = group[i]
d3Active(element).tween('text', () => {
const textI = d3Interpolate(element.textContent, textRounder(d))
return (t) => {
element.textContent = textRounder(textI(t)) + percentText
}
})
})
// The clipping wave area.
const clipArea = d3Area()
.x((d) => waveScaleX(d.x))
.y0((d) => waveScaleY(Math.sin(Math.PI * 2 * waveOffset * -1 + Math.PI * 2 * (1 - waveCount) + d.y * 2 * Math.PI)))
.y1(() => fillCircleRadius * 2 + waveHeight)
const elementId = 'elementId'
// Data for building the clip wave area.
let data = []
for (let i = 0; i <= 40 * waveClipCount; i++) {
data.push({x: i / (40 * waveClipCount), y: (i / (40))})
}
const waveGroupXPosition = fillCircleMargin + fillCircleRadius * 2 - waveClipWidth
const waveGroupEnter = gEnter.append('defs')
.append('clipPath')
.attr('id', 'clipWave' + elementId)
.attr('transform', 'translate(' + waveGroupXPosition + ',' + waveRiseScale(startValue) + ')')
waveGroupEnter
.append('path')
.attr('class', 'wave-clip-path')
.attr('d', clipArea(data))
.attr('T', 0)
svgMerge
.select('clipPath')
.transition()
.duration(waveRiseTime)
.attr('transform', 'translate(' + waveGroupXPosition + ',' + waveRiseScale(fillPercent) + ')')
// The inner circle with the clipping wave attached.
const ggEnter = gEnter.append('g')
.attr('clip-path', 'url(#clipWave' + elementId + ')')
ggEnter.append('circle')
.attr('cx', radius)
.attr('cy', radius)
.attr('r', fillCircleRadius)
.style('fill', waveColor)
// Text above the wave
ggEnter
.append('text')
.attr('class', 'above-wave-text')
.attr('text-anchor', 'middle')
.attr('font-size', textPixels + 'px')
.style('fill', waveTextColor)
.text(textRounder(startValue) + percentText)
.attr('transform', 'translate(' + radius + ',' + textRiseScaleY(textVertPosition) + ')')
svgMerge
.select('.above-wave-text')
.transition()
.duration(waveRiseTime)
.on('start', (d, i, group) => {
const element = group[i]
d3Active(element).tween('text', () => {
const textI = d3Interpolate(element.textContent, textRounder(d))
return (t) => {
element.textContent = textRounder(textI(t)) + percentText
}
})
})
function animateWave () {
if (!wrapper) {
return
}
const wave = svgMerge.select('.wave-clip-path');
const T = wave.attr('T')
wave
.attr('transform', 'translate(' + waveAnimateScale(T) + ',0)')
wave.transition()
.duration(waveAnimateTime * (1 - T))
.ease(d3EaseLinear)
.attr('transform', 'translate(' + waveAnimateScale(1) + ',0)')
.attr('T', 1)
.on('end', () => {
wave.attr('T', 0)
animateWave(waveAnimateTime)
})
}
animateWave()
}
function destroyChart (wrapper) {
const {select: d3Select} = d3
d3Select(wrapper).selectAll('*').remove()
}
document.addEventListener('DOMContentLoaded', () => {
setInterval(() => {
renderChart(document.querySelector('#wrapper'), Math.random()*100)
}, 5000)
renderChart(document.querySelector('#wrapper'), Math.random()*100)
})
</script>
</body>
</html>
@Borderliner
Copy link

Thanks! Works wonderfully!

@harshilishere
Copy link

Hey so I am trying to make it work in vue js. I have a dashboard.vue in views and a component named gauge.vue. Please guide me where to keep what. I am very new to this. I tried different things but some says d3 not defined kind of errors. Please help me I have tried everything.
Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment