<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Radial Gauge Example</title>
<style>
.radial-gauge-svg {
width: 300;
height: 300;
}
.radial-gauge-svg > g > text,
g > text {
text-anchor: middle;
dominant-baseline: alphabetic;
font-weight: normal;
stroke: none;
}
.radial-gauge-path {
fill: none;
stroke: gainsboro;
stroke-width: 8;
}
.radial-gauge-path-filled {
fill: none;
stroke: lightskyblue;
stroke-width: 8;
}
.radial-gauge-title-text {
font-size: 0.28rem;
text-anchor: middle;
alignment-baseline: middle;
dominant-baseline: central;
fill: gray;
}
.radial-gauge-units-text {
font-size: 0.22rem;
text-anchor: middle;
alignment-baseline: middle;
dominant-baseline: central;
fill: gray;
}
.radial-gauge-value-text {
font-size: 0.55rem;
font-family: sans-serif;
font-weight: normal;
text-anchor: middle;
alignment-baseline: middle;
dominant-baseline: central;
fill: dimgray;
}
.radial-gauge-circle {
fill: orangered;
}
.radial-gauge-needle {
fill: unset;
stroke-width: 0.4;
stroke: orangered;
}
.radial-gauge-scale-text {
font-size: 0.22rem;
fill: dimgray;
}
.radial-gauge-tick {
stroke: darkgray;
stroke-width: 0.4;
}
.gauge210 {
display: inline-block;
border: 0.5px solid darkgray;
box-sizing: border-box;
}
.gauge270 {
display: inline-block;
border: 0.5px solid darkgray;
box-sizing: border-box;
}
</style>
<script>
;(function (global, factory) {
const Gauge = factory(global)
if (typeof define === 'function' && define.amd) {
// AMD support
define(() => Gauge)
} else if (typeof module === 'object' && module.exports) {
// CommonJS support
module.exports = Gauge
} else {
// Browser global
global.Gauge = Gauge
}
})(typeof window === 'undefined' ? this : window, (global) => {
const document = global.document
const requestAnimationFrame = global.requestAnimationFrame || global.mozRequestAnimationFrame || global.webkitRequestAnimationFrame || global.msRequestAnimationFrame || ((cb) => setTimeout(cb, 1000 / 60))
const SVG_NS = 'http://www.w3.org/2000/svg'
/**
* Animation utility for smooth transitions.
*/
class Animation {
constructor(options) {
this.duration = options.duration
this.start = options.start || 0
this.end = options.end
this.change = this.end - this.start
this.step = options.step
this.easing =
options.easing ||
((pos) => {
if ((pos /= 0.5) < 1) return 0.5 * Math.pow(pos, 3)
return 0.5 * (Math.pow(pos - 2, 3) + 2)
})
this.currentIteration = 1
this.iterations = 60 * this.duration
}
animate() {
const progress = this.currentIteration / this.iterations
const value = this.change * this.easing(progress) + this.start
this.step(value, this.currentIteration)
this.currentIteration += 1
if (progress < 1) {
requestAnimationFrame(() => this.animate())
}
}
startAnimation() {
requestAnimationFrame(() => this.animate())
}
}
/**
* Utility functions for SVG and math operations.
*/
const Utils = {
createSVGElement(label, attrs, children) {
const elem = document.createElementNS(SVG_NS, label)
for (const attrName in attrs) {
elem.setAttribute(attrName, attrs[attrName])
}
if (children) {
children.forEach((child) => elem.appendChild(child))
}
return elem
},
getAngle(percentage, gaugeSpanAngle) {
return (percentage * gaugeSpanAngle) / 100
},
normalize(value, min, max) {
let val = Number(value)
if (val < 0) val += min
return Math.min(val, max)
},
getValueInPercentage(value, min, max) {
const newMax = max - min
const newVal = value - min
return (100 * newVal) / newMax
},
getCartesian(cx, cy, radius, angle) {
const rad = (angle * Math.PI) / 180
return {
x: Math.round((cx + radius * Math.cos(rad)) * 1000) / 1000,
y: Math.round((cy + radius * Math.sin(rad)) * 1000) / 1000,
}
},
getDialCoords(radius, startAngle, endAngle) {
const cx = 50
const cy = 50
return {
start: this.getCartesian(cx, cy, radius, startAngle),
end: this.getCartesian(cx, cy, radius, endAngle),
}
},
pathString(radius, startAngle, endAngle, largeArc = 1) {
const { start, end } = this.getDialCoords(radius, startAngle, endAngle)
return ['M', start.x, start.y, 'A', radius, radius, 0, largeArc, 1, end.x, end.y].join(' ')
},
}
/**
* Gauge class for creating and managing the gauge.
*/
class Gauge {
constructor(container, options = {}) {
this.container = container
this.options = { ...Gauge.defaultOptions, ...options }
this.initialize()
}
static defaultOptions = {
gaugeSize: 45,
offset: 10,
displayNeedle: true,
displayScale: true,
displaySmallScale: true,
displayValue: true,
precision: 2,
title: 'Speed',
units: 'Km/h',
gaugeColor: null,
}
initialize() {
const { gaugeSize, offset, value, displayNeedle } = this.options
this.radius = gaugeSize - offset
this.createSVGElements()
this.updateGauge(value)
if (displayNeedle) this.drawNeedle()
}
createSVGElements() {
const { startAngle, endAngle, title, units, displayNeedle, displayScale, needleY, titleY, unitsY, valueY } = this.options
// Create SVG elements
this.titleText = Utils.createSVGElement('text', { x: 50, y: titleY, class: 'radial-gauge-title-text' }, [document.createTextNode(title)])
this.unitsText = Utils.createSVGElement('text', { x: 50, y: unitsY, class: 'radial-gauge-units-text' }, [document.createTextNode(units)])
this.valueText = Utils.createSVGElement('text', { x: 50, y: valueY, class: 'radial-gauge-value-text' })
this.barPath = Utils.createSVGElement('path', { class: 'radial-gauge-path', d: Utils.pathString(this.radius, startAngle, endAngle) })
this.barFilledPath = Utils.createSVGElement('path', { class: 'radial-gauge-path-filled', d: Utils.pathString(this.radius, startAngle, startAngle) })
this.rootSvg = Utils.createSVGElement('svg', { viewBox: '0 0 100 100', class: 'radial-gauge-svg' }, [this.barPath, this.barFilledPath, this.valueText, this.titleText, this.unitsText])
if (displayNeedle) {
this.rootSvg.appendChild(Utils.createSVGElement('circle', { class: 'radial-gauge-circle', cx: 50, cy: needleY, r: 2 }))
}
if (displayScale) {
this.createScale()
}
this.container.appendChild(this.rootSvg)
}
createScale() {
const { gaugeSize, startAngle, endAngle, min, max, ticks, displaySmallScale } = this.options
const scaleGroup = Utils.createSVGElement('g', { class: 'radial-gauge-scale' })
const startTick = startAngle + 90
const factor = (360 - (startAngle - endAngle)) / (ticks * 10)
for (let n = 0; n <= ticks * 10; n++) {
let yT = 50 - gaugeSize + gaugeSize / 10 - (gaugeSize > 40 ? 2 : 0)
let dT = n % 10 === 0 ? 5 : 2
if (n % 10 === 0) {
yT -= 3
}
if (n % 10 === 0 || displaySmallScale) {
const tickLine = Utils.createSVGElement('line', {
x1: 50,
y1: yT,
x2: 50,
y2: yT + dT,
class: 'radial-gauge-tick',
transform: `rotate(${n * factor + startTick} 50 50)`,
})
scaleGroup.appendChild(tickLine)
if (n % 10 === 0) {
const scaleText = Utils.createSVGElement(
'text',
{
x: 50,
y: yT - 1,
class: 'radial-gauge-scale-text',
transform: `rotate(${n * factor + startTick} 50 50)`,
},
[document.createTextNode((n * (max / ticks / 10) + min).toFixed(0))],
)
scaleGroup.appendChild(scaleText)
}
}
}
this.rootSvg.appendChild(scaleGroup)
}
drawNeedle() {
const { id, needleY } = this.options
const needleCoord = this.barFilledPath.getAttribute('d').split(' ')
let prev = document.querySelector(`.id-${id}`)
if (prev) {
prev.remove()
}
const needle = Utils.createSVGElement('line', {
class: `id-${id} radial-gauge-needle`,
x1: 50,
y1: needleY,
x2: needleCoord[needleCoord.length - 2],
y2: needleCoord[needleCoord.length - 1],
})
this.rootSvg.appendChild(needle)
}
updateGauge(value) {
const { min, max, startAngle, endAngle, precision, displayValue, displayNeedle } = this.options
const percentage = Utils.getValueInPercentage(value, min, max)
const angle = Utils.getAngle(percentage, 360 - Math.abs(startAngle - endAngle))
const flag = angle <= 180 ? 0 : 1
this.barFilledPath.setAttribute('d', Utils.pathString(this.radius, startAngle, angle + startAngle, flag))
if (displayValue) {
this.valueText.textContent = value.toFixed(precision)
}
if (displayNeedle) {
this.drawNeedle()
}
}
setValue(value) {
const { min, max } = this.options
this.options.value = Utils.normalize(value, min, max)
this.updateGauge(this.options.value)
}
setValueAnimated(value, duration = 1) {
const { value: oldValue, min, max } = this.options
this.options.value = Utils.normalize(value, min, max)
new Animation({
start: oldValue,
end: this.options.value,
duration,
step: (val) => this.updateGauge(val),
}).startAnimation()
}
getValue() {
return this.options.value
}
getRange() {
const { min, max } = this.options
return { min, max }
}
}
return Gauge
})
</script>
</head>
<body>
<div class="gauge210"></div>
<div class="gauge270"></div>
<script>
const gauge210 = new Gauge(document.querySelector('.gauge210'), {
id: '210',
min: 0,
max: 210,
value: 100,
ticks: 7,
startAngle: 165,
endAngle: 15,
titleY: 35,
unitsY: 41,
needleY: 50,
valueY: 62,
})
const gauge270 = new Gauge(document.querySelector('.gauge270'), {
id: '270',
min: 0,
max: 270,
value: 100,
ticks: 9,
startAngle: 135,
endAngle: 45,
titleY: 40,
unitsY: 46,
needleY: 60,
valueY: 78,
})
setInterval(function () {
const randomValue210 = (Math.random() * gauge210.getRange().max).toFixed(2)
gauge210.setValueAnimated(parseFloat(randomValue210), 0.5)
const randomValue270 = (Math.random() * gauge270.getRange().max).toFixed(2)
gauge270.setValueAnimated(parseFloat(randomValue270), 0.5)
}, 1000)
</script>
</body>
</html>
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme