@csn_chile/fuelgauge
Version:
D3 Fuel Gauge Chart
425 lines (369 loc) • 12.5 kB
JavaScript
const d3 = require('d3');
let fuelGauge = null;
function updateGauges() {
const number = Math.random() * 100;
const theNumber = Math.round(number);
fuelGauge.redraw(theNumber);
}
function Gauge(container, cfg) {
const self = this;
const configuration = cfg || {};
// default config options
self.config = {
size: 200,
label: '',
min: 0,
max: 100,
transitionDuration: 500,
majorTicks: 6,
minorTicks: 3,
showPointer: true,
showValue: true,
valueFontSize: 14,
unit: '%',
greenColor: '#72A544',
yellowColor: '#FFBE00',
redColor: '#C81922',
greyColor: '#D5D3D9',
up: 0.85,
low: 0.45,
margin_radius: 0.4,
};
this.configure = function setCfg(cfgu) {
Object.keys(cfgu).forEach((prop) => {
if ({}.hasOwnProperty.call(this.config, prop)) {
this.config[prop] = cfgu[prop];
}
});
// todo: add checks for valid values
this.config.radius = this.config.size / 2;
this.config.cx = this.config.size / 2;
this.config.cy = this.config.size / 2;
// que es I?
this.config.I = this.config.max;
// un rango, el largo del intervalo min a max
// escalar que indica la cantidad de grados disponibles para
// varias o jugar
this.config.range = this.config.max - this.config.min;
// cuanto mide cada zona
const zoneRange = Math.round(this.config.range / 3);
// default zone sizes
this.config.redZone = configuration.redZone || {
from: this.config.min,
to: this.config.min + zoneRange,
};
this.config.yellowZone = configuration.yellowZone || {
from: this.config.redZone.to,
to: this.config.redZone.to + zoneRange,
};
this.config.greenZone = configuration.greenZone || {
from: this.config.yellowZone.to,
to: this.config.max,
};
};
this.setvalue = function setvalue(nvalue) {
// retorna valor en degrees
// pero los ajusta al rango
// si es inferior o superior
let value = nvalue;
if (!nvalue) {
value = 0;
}
if (value + self.config.min >= this.config.max) {
return this.config.max;
}
if (value + self.config.min <= this.config.min) {
return this.config.min;
}
return value + self.config.min;
};
this.render = function render(value) {
this.body = d3
.select(container)
.append('svg:svg')
.attr('class', 'gauge')
.attr('width', this.config.size)
.attr('height', this.config.size);
// arco que se dibuja atras
this.drawRestBand(this.config.min, this.config.max, self.config.greyColor);
// arco que dibuja el status actual.
this.drawBand(this.config.min, this.config.max, self.config.greyColor);
this.drawLabel();
this.pointer = this.drawPointer(value);
this.redraw(value);
};
this.drawLabel = function drawLabel() {
let labelFontSize;
let major;
let minor;
let minorDelta;
let point1;
let point2;
if (this.config.label) {
labelFontSize = Math.round(this.config.size / 9);
this.body
.append('svg:text')
.attr('x', this.config.cx)
.attr('y', this.config.cy / 2 + labelFontSize / 2)
.attr('dy', labelFontSize)
.attr('class', 'label')
.attr('text-anchor', 'middle')
.text(this.config.label)
.style('font-size', `${labelFontSize}px`);
}
const majorDelta = this.config.range / (this.config.majorTicks - 1);
for (major = this.config.min; major <= this.config.max; major += majorDelta) {
minorDelta = majorDelta / this.config.minorTicks;
for (minor = major + minorDelta;
minor < Math.min(major + majorDelta, this.config.max);
minor += minorDelta) {
point1 = this.valueToPoint(minor, 0.75);
point2 = this.valueToPoint(minor, 0.85);
this.body
.append('svg:line')
.attr('class', 'minorTick')
.attr('x1', point1.x)
.attr('y1', point1.y)
.attr('x2', point2.x)
.attr('y2', point2.y);
}
point1 = this.valueToPoint(major, 0.6);
point2 = this.valueToPoint(major, 0.9);
this.body
.append('svg:line')
.attr('class', 'majorTick')
.attr('x1', point1.x)
.attr('y1', point1.y)
.attr('x2', point2.x)
.attr('y2', point2.y);
if (major === this.config.max) {
// const point = this.valueToPoint(major, 0.82);
}
if (major === this.config.min) {
// const point = this.valueToPoint(major, 0.82);
}
}
};
this.drawPointer = function drawPointer(nvalue) {
const midValue = this.setvalue(nvalue);
const pointerContainer = this.body.append('svg:g').attr('class', 'pointerContainer');
if (this.config.showPointer) {
const pointerPath = this.buildPointerPath(midValue);
const pointerLine = d3
.line()
.x((d) => d.x)
.y((d) => d.y)
.curve(d3.curveBasis);
pointerContainer
.selectAll('path')
.data([pointerPath])
.enter()
.append('svg:path')
.attr('d', pointerLine)
.attr('class', 'pointerLine')
.style('fill-opacity', 1);
pointerContainer
.append('svg:circle')
.attr('cx', this.config.cx)
.attr('cy', this.config.cy)
.attr('r', 0.06 * this.config.radius)
.attr('class', 'pointerCircle')
.style('opacity', 1);
}
if (this.config.showValue) {
const fontSize = this.config.valueFontSize;
pointerContainer
.selectAll('text')
.data([midValue])
.enter()
.append('svg:text')
.attr('x', this.config.cx)
.attr('y', this.config.size - this.config.cy / 1.6 - fontSize)
.attr('dy', fontSize)
.attr('text-anchor', 'middle')
.attr('class', 'value')
.style('font-size', `${fontSize}px`)
.style('stroke-width', '0px');
}
return pointerContainer;
};
this.isRendered = function isRendered() {
return this.body !== undefined;
};
this.buildPointerPath = function buildPointerPath(value) {
const nvalue = self.setvalue(value);
function valueToPoint(val, factor) {
const point = self.valueToPoint(val, factor);
point.x -= self.config.cx;
point.y -= self.config.cy;
return point;
}
const delta = this.config.range / 13;
const head = valueToPoint(nvalue, 0.6);
const head1 = valueToPoint(nvalue - delta, 0.12);
const head2 = valueToPoint(nvalue + delta, 0.12);
const tailValue = value - (this.config.range * (1 / (180 / 360))) / 2;
const tail = valueToPoint(tailValue, -0.1);
const tail1 = valueToPoint(tailValue - delta, 0.12);
const tail2 = valueToPoint(tailValue + delta, 0.12);
return [head, head1, tail2, tail, tail1, head2, head];
};
this.drawBand = function drawBand(start, end, color) {
let bands;
let arc;
let arcs;
if (end - start <= 0) {
return;
}
// interpolación
function arcTween(transition, newAngle) {
transition.attrTween('d', (d) => {
const theAngle = self.valueToRadians(newAngle);
const interpolate = d3.interpolate(d.endAngle, theAngle);
return function at(t) {
const nd = d;
nd.endAngle = interpolate(t);
self.config.I = newAngle;
return arc(nd);
};
});
}
bands = this.body.select('.bands');
if (bands.empty()) {
bands = this.body.append('svg:path').attr('class', 'bands');
}
// const theend = this.valueToRadians(end);
arc = d3
.arc()
.innerRadius((self.config.up - self.config.margin_radius) * this.config.radius)
.outerRadius((self.config.low + self.config.margin_radius) * this.config.radius)
.startAngle(this.valueToRadians(self.config.min));
const bandsdraw = bands.attr(
'transform',
() => `translate(${self.config.cx}, ${self.config.cy}) rotate(270)`,
);
// dibujar en transición el ángulo final en banda a la vista
if (self.config.I === 0) {
arcs = bandsdraw
.datum({
endAngle: 0,
})
.style('fill', color)
.attr('d', arc);
} else {
arcs = bandsdraw
.datum({
endAngle: this.valueToRadians(self.config.I),
})
.style('fill', color)
.attr('d', arc);
}
arcs
.transition()
.duration(this.config.transitionDuration)
.call(arcTween, end);
};
this.drawRestBand = function drawRestBand(start, end, color) {
let bands;
if (end - start <= 0) {
return;
}
bands = this.body.select('.backbands');
if (bands.empty()) {
bands = this.body.append('svg:path').attr('class', 'backbands');
}
// const theend = this.valueToRadians(end);
const bandsdrawn = bands.attr('transform',
() => `translate(${self.config.cx}, ${self.config.cy}) rotate(270)`);
const arcEnd = d3
.arc()
.innerRadius(self.config.low * this.config.radius)
.outerRadius(self.config.up * this.config.radius)
.startAngle(this.valueToRadians(start))
.endAngle(this.valueToRadians(end));
bandsdrawn.style('fill', color).attr('d', arcEnd);
};
this.updatePointer = function updatePoiter(value) {
// ajustamos en deg el nuevo valor
// al que se llega
const newValue = this.setvalue(value);
// se selecciona el contenedor
const pointerContainer = this.body.select('.pointerContainer');
pointerContainer.selectAll('text').text(Math.round(value) + this.config.unit);
// se selecciona puntero
const pointer = pointerContainer.selectAll('path');
// se habilita transición
pointer
.transition()
.duration(this.config.transitionDuration)
.attrTween('transform', () => {
const pointerValue = newValue;
const targetRotation = pointerValue - 90;//self.config.range;
const currentRotation = self._currentRotation || targetRotation;
self._currentRotation = targetRotation;
return function rotater(step) {
const rotation = currentRotation + (targetRotation - currentRotation) * step;
return `translate(${self.config.cx}, ${self.config.cy}) rotate(${rotation})`;
};
});
};
this.redraw = function redraw(value) {
const newValue = this.setvalue(value);
if (!this.isRendered() || !newValue
|| !(!Number.isNaN(parseFloat(newValue)) && Number.isFinite(newValue))) {
return;
}
this.updatePointer(value);
// todo: make color ranges configurable
if (newValue >= this.config.redZone.from && newValue < this.config.redZone.to) {
this.drawBand(this.config.min, newValue, this.config.redColor);
} else if (newValue >= this.config.yellowZone.from && newValue <= this.config.yellowZone.to) {
this.drawBand(this.config.min, newValue, this.config.yellowColor);
} else if (newValue >= this.config.greenZone.from && newValue <= this.config.greenZone.to) {
this.drawBand(this.config.min, newValue, this.config.greenColor);
} else {
this.drawBand(this.config.min, newValue, this.config.greyColor);
}
this.drawRestBand(this.config.min, this.config.max, self.config.greyColor);
};
this.valueToDegrees = function valueToDegrees(value) {
return (value / this.config.range) * 180 - (this.config.min / this.config.range) * 180;
};
this.valueToRadians = function valueToRadians(value) {
return (value * Math.PI) / 180;
};
this.valueToPoint = function valueToPoint(value, factor) {
return {
x: this.config.cx - this.config.radius * factor * Math.cos(this.valueToRadians(value)),
y: this.config.cy - this.config.radius * factor * Math.sin(this.valueToRadians(value)),
};
};
// initialization
this.configure(configuration);
}
function initialize() {
const config = {
size: 80,
minorTicks: 1,
showPointer: false,
showValue: true,
valueFontSize: 16,
unit: '%',
redZone: {
from: 0,
to: 20,
},
yellowZone: {
from: 20,
to: 60,
},
greenZone: {
from: 60,
to: 100,
},
};
fuelGauge = new Gauge('#fuelGaugeContainer', config);
fuelGauge.render(70);
setInterval(updateGauges, 2000);
}
export { initialize, Gauge };