I am creating a gauge using svg.
Is there a way to add an arrow at the end of the animated gauge to indicate the entered value?
Since the gauge is displayed with the path's stroke-dasharray, it seems that a marker cannot be applied according to the svg specification.
class arcGauge {
constructor(targetEl) {
this.el = targetEl;
this.minmax = [];
this.arcCoordinate = [];
this.valueArcDataValue = 0;
this.gaugeArc = document.createElementNS('http://www.w3.org/2000/svg', 'path');
}
// draw gauge
init(data) {
// set data
this.viewBox = [0, 0, 110, 100];
this.minmax = data.minmax || [];
this.arcCoordinate = data.arcCoordinate || [120, 60];
this.threshold = data.threshold || [];
this.valueArcData = data.valueArcData;
this.gaugeRadius = 42;
this.gaugeStrokeWidth = 6;
this._makeGauge();
}
_makeGauge() {
const radius = this.gaugeRadius;
let arcCoord = [];
// coordinate
this.arcCoordinate.forEach((ang) => {
const radian = this._degreesToRadians(ang);
const x = this.viewBox[2] / 2 + Math.cos(radian) * radius;
const y = this.viewBox[2] / 2 + Math.sin(radian) * radius;
arcCoord.push([x.toFixed(2), y.toFixed(2)]);
});
//arc
this.gaugeArc.setAttribute('id', 'gaugeArc');
this.gaugeArc.setAttribute('d', `M ${arcCoord[0][0]} ${arcCoord[0][1]} A ${radius} ${radius} 0 1 1 ${arcCoord[1][0]} ${arcCoord[1][1]}`);
this.gaugeArc.setAttribute('fill', 'none');
this.gaugeArc.setAttribute('stroke', this.valueArcData.color);
this.gaugeArc.setAttribute('stroke-width', this.gaugeStrokeWidth);
this.gaugeArc.setAttribute('transform', 'scale(-1, 1) translate(-110, 0)');
let percentage = 0;
percentage = this.valueArcData.value;
this.gaugeArc.style.strokeDasharray = this._getArcLength(radius, 300, percentage);
this.el.appendChild(this.gaugeArc);
}
// degree
_degreesToRadians(degrees) {
const pi = Math.PI;
return degrees * (pi / 180);
}
// arc length
_getArcLength(radius, degrees, val) {
const radian = this._degreesToRadians(degrees);
const arcLength = 2 * Math.PI * radius * (degrees / 360);
const pathLength = arcLength * (val / 100);
const dasharray = `${pathLength.toFixed(2)} ${arcLength.toFixed(2)}`;
return dasharray;
}
// set gauge value
setValue(v) {
const baseValue = this.minmax[1] - this.minmax[0];
const percentage = (v / baseValue) * 100;
this.valueArcData.value = v;
this.gaugeArc.style.strokeDasharray = this._getArcLength(this.gaugeRadius, 300, percentage);
//gauge animation
this.gaugeArc.style.transition = 'stroke-dasharray 1s ease-in-out';
}
}
const arcGaugeData = {
minmax: [0, 100],
thresholdColor: ['#22b050', '#f4c141', '#e73621'],
valueArcData: { type: 'arrow', color: '#3e3eff', value: 80 },
};
const arcGaugeIns = new arcGauge(document.querySelector('#chart'));
arcGaugeIns.init(arcGaugeData);
function changeArc(ipt) {
arcGaugeIns.setValue(ipt.value);
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
<div class="container">
<svg id="chart" xmlns="http://www.w3.org/2000/svg" width="180px" height="auto" viewBox="0 0 110 100"></svg>
<input type="range" min="0" max="100" step="1" value="80" onchange="changeArc(this)" />
</div>
If this is not possible, please advise if there is an alternative.
The main idea is this:
you calculate the end point of the gauge using getPointAtLength
and you move the arrow at this point.
Also you calculate the angle of the path at the end point so that you can rotate the arrow accordingly.
In order to calculate the angle you will need 2 points:
• point1
i.e the end point of the gauge
• point2
i.e a slightly offseted point (val + 0.1
).
This will work unless the value of the slider is 100. In this case I use the arrow as a marker.
let length = chart.getTotalLength();
chart.style.strokeDasharray = length;
chart.style.strokeDashoffset = length;
function gauge(rangeValue) {
let val = length - (length * rangeValue) / 100;
if (rangeValue < 100 && rangeValue > 0) {
let point1 = chart.getPointAtLength(val);
let point2 = chart.getPointAtLength(val + 0.1);
let angle = Math.atan2(point2.y - point1.y, point2.x - point1.x);
arrow.setAttribute(
"transform",
"translate(" +
[point1.x, point1.y] +
")" +
"rotate(" +
(angle * 180) / Math.PI +
")"
);
} else {
chart.setAttribute("marker", "url(#m)");
}
chart.style.strokeDashoffset = val;
}
gauge(Number(range.value));
range.addEventListener("input", () => {
let rangeValue = Number(range.value);
gauge(rangeValue);
});
svg{border:solid;}
<svg xmlns="http://www.w3.org/2000/svg" width="180px" height="auto" viewBox="0 0 110 100">
<path id="chart" d="M 34.00 91.37 A 42 42 0 1 1 76.00 91.37" fill="none" stroke="#3e3eff" stroke-width="6" transform="scale(-1, 1) translate(-110, 0)" marker="url(#m)"></path>
<marker id="m">
<path id="markerarrow" fill="red" d="M0,0L0,-10L-14,0L0,10" />
</marker>
<use href="#markerarrow" id="arrow"/>
</svg>
</br>
<p><input type="range" id="range" min="0" max="100" step="1" value="80" /></p>
Observation: This solution is not perfect. If you move the slider very fast you may get an offseted arrow.