UNPKG

@csn_chile/fuelgauge

Version:

D3 Fuel Gauge Chart

425 lines (369 loc) 12.5 kB
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 };