Home Reference Source

lib/RadialGauge.js

  1. /*!
  2. * The MIT License (MIT)
  3. *
  4. * Copyright (c) 2016 Mykhailo Stadnyk <mikhus@gmail.com>
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to deal
  8. * in the Software without restriction, including without limitation the rights
  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. * copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in
  14. * all copies or substantial portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22. * SOFTWARE.
  23. */
  24. require('./polyfill');
  25.  
  26. const GenericOptions = require('./GenericOptions');
  27. const BaseGauge = require('./BaseGauge');
  28. const SmartCanvas = require('./SmartCanvas');
  29. const drawings = require('./drawings');
  30.  
  31. const PI = Math.PI;
  32. const HPI = PI / 2;
  33.  
  34. /**
  35. * Gauge configuration options
  36. *
  37. * @typedef {GenericOptions | {
  38. * exactTicks: boolean,
  39. * ticksAngle: number,
  40. * startAngle: number,
  41. * colorNeedleCircleOuter: string,
  42. * colorNeedleCircleOuterEnd: string,
  43. * colorNeedleCircleInner: string,
  44. * colorNeedleCircleInnerEnd: string,
  45. * needleCircleSize: number,
  46. * needleCircleInner: boolean,
  47. * needleCircleOuter: boolean,
  48. * animationTarget: string,
  49. * useMinPath: boolean,
  50. * barStartPosition: 'right' | 'left',
  51. * }} RadialGaugeOptions
  52. */
  53.  
  54. /**
  55. * Default gauge configuration options
  56. *
  57. * @access private
  58. * @type {RadialGaugeOptions}
  59. */
  60. const defaultRadialGaugeOptions = Object.assign({}, GenericOptions, {
  61. // basic options
  62. ticksAngle: 270,
  63. startAngle: 45,
  64.  
  65. // colors
  66. colorNeedleCircleOuter: '#f0f0f0',
  67. colorNeedleCircleOuterEnd: '#ccc',
  68. colorNeedleCircleInner: '#e8e8e8',
  69. colorNeedleCircleInnerEnd: '#f5f5f5',
  70.  
  71. // needle
  72. needleCircleSize: 10,
  73. needleCircleInner: true,
  74. needleCircleOuter: true,
  75. needleStart: 20,
  76.  
  77. // custom animations
  78. animationTarget: 'needle', // 'needle' or 'plate'
  79. useMinPath: false,
  80.  
  81. barWidth: 0,
  82. barStartPosition: 'left'
  83. });
  84.  
  85. /* istanbul ignore next: private, not testable */
  86. /**
  87. * Draws gradient-filled circle on a canvas
  88. *
  89. * @access private
  90. * @param {number} radius
  91. * @param {number} width
  92. * @param {Canvas2DContext} context
  93. * @param {string} start gradient start color
  94. * @param {string} end gradient end color
  95. */
  96. function drawRadialBorder(radius, width, context, start, end) {
  97. context.beginPath();
  98. //noinspection JSUnresolvedFunction
  99. context.arc(0, 0, abs(radius), 0, PI * 2, true);
  100. context.lineWidth = width;
  101. context.strokeStyle = end ?
  102. drawings.linearGradient(context, start, end, radius) :
  103. start;
  104. context.stroke();
  105. context.closePath();
  106. }
  107.  
  108. /* istanbul ignore next: private, not testable */
  109. /**
  110. * Returns max radius without borders for the gauge
  111. *
  112. * @param {Canvas2DContext} context
  113. * @param {RadialGaugeOptions} options
  114. * @return {number}
  115. */
  116. function maxRadialRadius(context, options) {
  117. let pxRatio = SmartCanvas.pixelRatio;
  118.  
  119. if (!context.maxRadius) {
  120. context.maxRadius = context.max
  121. - options.borderShadowWidth
  122. - options.borderOuterWidth * pxRatio
  123. - options.borderMiddleWidth * pxRatio
  124. - options.borderInnerWidth * pxRatio
  125. + (options.borderOuterWidth ? 0.5 : 0)
  126. + (options.borderMiddleWidth ? 0.5 : 0)
  127. + (options.borderInnerWidth ? 0.5 : 0);
  128. }
  129.  
  130. return context.maxRadius;
  131. }
  132.  
  133. /* istanbul ignore next: private, not testable */
  134. /**
  135. * Draws gauge plate on the canvas
  136. *
  137. * @access private
  138. * @param {Canvas2DContext} context
  139. * @param {RadialGaugeOptions} options
  140. */
  141. function drawRadialPlate(context, options) {
  142. let pxRatio = SmartCanvas.pixelRatio;
  143. let d0 = options.borderShadowWidth * pxRatio;
  144. let r0 = context.max - d0 - (options.borderOuterWidth * pxRatio) / 2;
  145. let r1 = r0 - (options.borderOuterWidth * pxRatio) / 2 -
  146. (options.borderMiddleWidth * pxRatio) / 2 + 0.5;
  147. let r2 = r1 - (options.borderMiddleWidth * pxRatio) / 2 -
  148. (options.borderInnerWidth * pxRatio) / 2 + 0.5;
  149. let r3 = maxRadialRadius(context, options);
  150. let grad;
  151. let shadowDrawn = false;
  152.  
  153. context.save();
  154.  
  155. if (options.borderOuterWidth) {
  156. shadowDrawn = drawings.drawShadow(context, options, shadowDrawn);
  157. drawRadialBorder(r0,
  158. options.borderOuterWidth * pxRatio,
  159. context,
  160. options.colorBorderOuter,
  161. options.colorBorderOuterEnd);
  162. }
  163.  
  164. if (options.borderMiddleWidth) {
  165. shadowDrawn = drawings.drawShadow(context, options, shadowDrawn);
  166. drawRadialBorder(r1,
  167. options.borderMiddleWidth * pxRatio,
  168. context,
  169. options.colorBorderMiddle,
  170. options.colorBorderMiddleEnd);
  171. }
  172.  
  173. if (options.borderInnerWidth) {
  174. shadowDrawn = drawings.drawShadow(context, options, shadowDrawn);
  175. drawRadialBorder(r2,
  176. options.borderInnerWidth * pxRatio,
  177. context,
  178. options.colorBorderInner,
  179. options.colorBorderInnerEnd);
  180. }
  181.  
  182. drawings.drawShadow(context, options, shadowDrawn);
  183.  
  184. context.beginPath();
  185. //noinspection JSUnresolvedFunction
  186. context.arc(0, 0, abs(r3), 0, PI * 2, true);
  187.  
  188. if (options.colorPlateEnd) {
  189. grad = context.createRadialGradient(0, 0, r3 / 2, 0, 0, r3);
  190. grad.addColorStop(0, options.colorPlate);
  191. grad.addColorStop(1, options.colorPlateEnd);
  192. }
  193.  
  194. else {
  195. grad = options.colorPlate;
  196. }
  197.  
  198. context.fillStyle = grad;
  199.  
  200. context.fill();
  201. context.closePath();
  202.  
  203. context.restore();
  204. }
  205.  
  206. /* istanbul ignore next: private, not testable */
  207. /**
  208. * Draws gauge highlight areas on a canvas
  209. *
  210. * @access private
  211. * @param {Canvas2DContext} context
  212. * @param {RadialGaugeOptions} options
  213. */
  214. function drawRadialHighlights(
  215. context,
  216. options
  217. ) {
  218. let hlWidth = context.max *
  219. (parseFloat(options.highlightsWidth) || 0) / 100;
  220.  
  221. if (!hlWidth) return;
  222.  
  223. //noinspection JSUnresolvedFunction
  224. let r = abs(radialTicksRadius(context, options) - hlWidth / 2);
  225. let i = 0, s = options.highlights.length;
  226. let vd = (options.maxValue - options.minValue) / options.ticksAngle;
  227.  
  228. context.save();
  229.  
  230. for (; i < s; i++) {
  231. let hlt = options.highlights[i];
  232.  
  233. context.beginPath();
  234.  
  235. context.rotate(HPI);
  236. context.arc(0, 0, r,
  237. drawings.radians(options.startAngle +
  238. (hlt.from - options.minValue) / vd),
  239. drawings.radians(options.startAngle +
  240. (hlt.to - options.minValue) / vd),
  241. false
  242. );
  243. context.strokeStyle = hlt.color;
  244. context.lineWidth = hlWidth;
  245. context.lineCap = options.highlightsLineCap;
  246. context.stroke();
  247. context.closePath();
  248.  
  249. context.restore();
  250. context.save();
  251. }
  252. }
  253.  
  254. /* istanbul ignore next: private, not testable */
  255. /**
  256. * Draws minor ticks bar on a canvas
  257. *
  258. * @access private
  259. * @param {Canvas2DContext} context
  260. * @param {RadialGaugeOptions} options
  261. */
  262. function drawRadialMinorTicks(context, options) {
  263. let radius = radialTicksRadius(context, options);
  264. let s, range, angle;
  265. let i = 0;
  266. let delta = 0;
  267. let minTicks = Math.abs(options.minorTicks) || 0;
  268. let ratio = options.ticksAngle / (options.maxValue - options.minValue);
  269.  
  270. context.lineWidth = SmartCanvas.pixelRatio;
  271. context.strokeStyle = options.colorMinorTicks || options.colorStrokeTicks;
  272.  
  273. context.save();
  274.  
  275. if (options.exactTicks) {
  276. range = options.maxValue - options.minValue;
  277. s = minTicks ? range / minTicks : 0;
  278. delta = (BaseGauge.mod(options.majorTicks[0], minTicks) || 0) * ratio;
  279. }
  280.  
  281. else {
  282. s = minTicks * (options.majorTicks.length - 1);
  283. }
  284.  
  285. for (; i < s; ++i) {
  286. angle = options.startAngle + delta + i * (options.ticksAngle / s);
  287. if (angle <= (options.ticksAngle + options.startAngle )) {
  288. context.rotate(drawings.radians(angle));
  289.  
  290. context.beginPath();
  291. context.moveTo(0, radius);
  292. context.lineTo(0, radius - context.max * 0.075);
  293. closeStrokedPath(context);
  294. }
  295. }
  296. }
  297.  
  298. /* istanbul ignore next: private, not testable */
  299. /**
  300. * Returns ticks radius
  301. *
  302. * @access private
  303. * @param context
  304. * @param options
  305. * @return {number}
  306. */
  307. function radialTicksRadius(context, options) {
  308. let unit = context.max / 100;
  309.  
  310. return maxRadialRadius(context, options) - 5 * unit -
  311. (options.barWidth ?
  312. ((parseFloat(options.barStrokeWidth) || 0) * 2 +
  313. ((parseFloat(options.barWidth) || 0) + 5) * unit) :
  314. 0);
  315. }
  316.  
  317. /* istanbul ignore next: private, not testable */
  318. /**
  319. * Draws gauge major ticks bar on a canvas
  320. *
  321. * @param {Canvas2DContext} context
  322. * @param {RadialGaugeOptions} options
  323. */
  324. function drawRadialMajorTicks(context, options) {
  325. drawings.prepareTicks(options);
  326.  
  327. //noinspection JSUnresolvedFunction
  328. let r = abs(radialTicksRadius(context, options));
  329. let i, colors;
  330. let s = options.majorTicks.length;
  331. let pixelRatio = SmartCanvas.pixelRatio;
  332.  
  333. context.lineWidth = 2 * pixelRatio;
  334. context.save();
  335.  
  336. colors = options.colorMajorTicks instanceof Array ?
  337. options.colorMajorTicks : new Array(s).fill(options.colorStrokeTicks ||
  338. options.colorMajorTicks);
  339.  
  340. i = 0;
  341. for (; i < s; ++i) {
  342. context.strokeStyle = colors[i];
  343. context.rotate(drawings.radians(radialNextAngle(
  344. options,
  345. options.exactTicks ? options.majorTicks[i] : i,
  346. s
  347. )));
  348.  
  349. context.beginPath();
  350. context.moveTo(0, r);
  351. context.lineTo(0, r - context.max * 0.15);
  352. closeStrokedPath(context);
  353. }
  354.  
  355. if (options.strokeTicks) {
  356. context.strokeStyle = options.colorStrokeTicks || colors[0];
  357. context.rotate(HPI);
  358.  
  359. context.beginPath();
  360. context.arc(0, 0, r,
  361. drawings.radians(options.startAngle),
  362. drawings.radians(options.startAngle + options.ticksAngle),
  363. false
  364. );
  365. closeStrokedPath(context);
  366. }
  367. }
  368.  
  369. /* istanbul ignore next: private, not testable */
  370. function radialNextAngle(options, i, s) {
  371. if (options.exactTicks) {
  372. let ratio = options.ticksAngle / (options.maxValue - options.minValue);
  373. return options.startAngle + ratio * (i - options.minValue);
  374. }
  375.  
  376. return options.startAngle + i * (options.ticksAngle / (s - 1));
  377. }
  378.  
  379. /* istanbul ignore next: private, not testable */
  380. /**
  381. * Strokes, closes path and restores previous context state
  382. *
  383. * @param {Canvas2DContext} context
  384. */
  385. function closeStrokedPath(context) {
  386. context.stroke();
  387. context.restore();
  388. context.closePath();
  389. context.save();
  390. }
  391.  
  392. /* istanbul ignore next: private, not testable */
  393. /**
  394. * Draws gauge bar numbers
  395. *
  396. * @access private
  397. * @param {Canvas2DContext} context
  398. * @param {RadialGaugeOptions} options
  399. */
  400. function drawRadialNumbers(context, options) {
  401. let radius = radialTicksRadius(context, options) - context.max * 0.15;
  402. let points = {};
  403. let i = 0;
  404. let s = options.majorTicks.length;
  405. let isAnimated = options.animationTarget !== 'needle';
  406. let colors = options.colorNumbers instanceof Array ?
  407. options.colorNumbers : new Array(s).fill(options.colorNumbers);
  408.  
  409. let plateValueAngle = isAnimated ? -(options.value - options.minValue) /
  410. (options.maxValue - options.minValue) * options.ticksAngle : 0;
  411.  
  412. if (isAnimated) {
  413. context.save();
  414. context.rotate(-drawings.radians(plateValueAngle));
  415. }
  416.  
  417. context.font = drawings.font(options, 'Numbers', context.max / 200);
  418. context.lineWidth = 0;
  419. context.textAlign = 'center';
  420. context.textBaseline = 'middle';
  421.  
  422. for (; i < s; ++i) {
  423. let angle = plateValueAngle + radialNextAngle(options,
  424. options.exactTicks ? options.majorTicks[i] : i, s);
  425. let textWidth = context.measureText(options.majorTicks[i]).width;
  426. let textHeight = options.fontNumbersSize;
  427. let textRadius = Math.sqrt(textWidth * textWidth +
  428. textHeight * textHeight) / 2;
  429. let point = drawings.radialPoint(radius - textRadius -
  430. options.numbersMargin / 100 * context.max,
  431. drawings.radians(angle));
  432.  
  433. if (angle === 360) angle = 0;
  434.  
  435. if (points[angle]) {
  436. continue; //already drawn at this place, skipping
  437. }
  438.  
  439. points[angle] = true;
  440.  
  441. context.fillStyle = colors[i];
  442. context.fillText(options.majorTicks[i], point.x, point.y);
  443. }
  444.  
  445. isAnimated && context.restore();
  446. }
  447.  
  448. /* istanbul ignore next: private, not testable */
  449. /**
  450. * Draws gauge title
  451. *
  452. * @access private
  453. * @param {Canvas2DContext} context
  454. * @param {RadialGaugeOptions} options
  455. */
  456. function drawRadialTitle(context, options) {
  457. if (!options.title) return;
  458.  
  459. context.save();
  460. context.font = drawings.font(options, 'Title', context.max / 200);
  461. context.fillStyle = options.colorTitle;
  462. context.textAlign = 'center';
  463. context.fillText(options.title, 0, -context.max / 4.25, context.max * 0.8);
  464. context.restore();
  465. }
  466.  
  467. /* istanbul ignore next: private, not testable */
  468. /**
  469. * Draws units name on the gauge
  470. *
  471. * @access private
  472. * @param {Canvas2DContext} context
  473. * @param {RadialGaugeOptions} options
  474. */
  475. function drawRadialUnits(context, options) {
  476. if (!options.units) return;
  477.  
  478. context.save();
  479. context.font = drawings.font(options, 'Units', context.max / 200);
  480. context.fillStyle = options.colorUnits;
  481. context.textAlign = 'center';
  482. context.fillText(
  483. drawings.formatContext(options, options.units),
  484. 0,
  485. context.max / 3.25,
  486. context.max * 0.8);
  487. context.restore();
  488. }
  489.  
  490. /* istanbul ignore next: private, not testable */
  491. /**
  492. * Draws gauge needle
  493. *
  494. * @access private
  495. * @param {Canvas2DContext} context
  496. * @param {RadialGaugeOptions} options
  497. */
  498. function drawRadialNeedle(context, options) {
  499. if (!options.needle) return;
  500.  
  501. let value = options.ticksAngle < 360 ?
  502. drawings.normalizedValue(options).indented : options.value;
  503. let startAngle = isFixed ? options.startAngle :
  504. (options.startAngle + (value - options.minValue) /
  505. (options.maxValue - options.minValue) * options.ticksAngle);
  506. if (options.barStartPosition === 'right') {
  507. startAngle = options.startAngle + options.ticksAngle -
  508. (value - options.minValue) / (options.maxValue - options.minValue) *
  509. options.ticksAngle;
  510. }
  511. let max = maxRadialRadius(context, options);
  512. //noinspection JSUnresolvedFunction
  513. let r1 = abs(max / 100 * options.needleCircleSize);
  514. //noinspection JSUnresolvedFunction
  515. let r2 = abs(max / 100 * options.needleCircleSize * 0.75);
  516. //noinspection JSUnresolvedFunction
  517. let rIn = abs(max / 100 * options.needleEnd);
  518. //noinspection JSUnresolvedFunction
  519. let rStart = abs(options.needleStart ?
  520. max / 100 * options.needleStart : 0);
  521. //noinspection JSUnresolvedFunction
  522. let pad1 = max / 100 * options.needleWidth;
  523. let pad2 = max / 100 * options.needleWidth / 2;
  524. let pixelRatio = SmartCanvas.pixelRatio;
  525. let isFixed = options.animationTarget !== 'needle';
  526.  
  527. context.save();
  528.  
  529. drawings.drawNeedleShadow(context, options);
  530.  
  531. context.rotate(drawings.radians(startAngle));
  532.  
  533. context.fillStyle = drawings.linearGradient(
  534. context,
  535. options.colorNeedle,
  536. options.colorNeedleEnd,
  537. rIn - rStart);
  538.  
  539. if (options.needleType === 'arrow') {
  540. context.beginPath();
  541. context.moveTo(-pad2, -rStart);
  542. context.lineTo(-pad1, 0);
  543. context.lineTo(-1 * pixelRatio, rIn);
  544. context.lineTo(pixelRatio, rIn);
  545. context.lineTo(pad1, 0);
  546. context.lineTo(pad2, -rStart);
  547. context.closePath();
  548. context.fill();
  549.  
  550. context.beginPath();
  551. context.lineTo(-0.5 * pixelRatio, rIn);
  552. context.lineTo(-1 * pixelRatio, rIn);
  553. context.lineTo(-pad1, 0);
  554. context.lineTo(-pad2, -rStart);
  555. context.lineTo(pad2 / 2 * pixelRatio - 2 * pixelRatio, -rStart);
  556. context.closePath();
  557. context.fillStyle = options.colorNeedleShadowUp;
  558. context.fill();
  559. }
  560.  
  561. else { // simple line needle
  562. context.beginPath();
  563. context.moveTo(-pad2, rIn);
  564. context.lineTo(-pad2, rStart);
  565. context.lineTo(pad2, rStart);
  566. context.lineTo(pad2, rIn);
  567. context.closePath();
  568. context.fill();
  569. }
  570.  
  571. if (options.needleCircleSize) {
  572. context.restore();
  573.  
  574. drawings.drawNeedleShadow(context, options);
  575.  
  576. if (options.needleCircleOuter) {
  577. context.beginPath();
  578. context.arc(0, 0, r1, 0, PI * 2, true);
  579. context.fillStyle = drawings.linearGradient(
  580. context,
  581. options.colorNeedleCircleOuter,
  582. options.colorNeedleCircleOuterEnd,
  583. r1
  584. );
  585. context.fill();
  586. context.closePath();
  587. }
  588.  
  589. if (options.needleCircleInner) {
  590. context.beginPath();
  591. context.arc(0, 0, r2, 0, PI * 2, true);
  592. context.fillStyle = drawings.linearGradient(
  593. context,
  594. options.colorNeedleCircleInner,
  595. options.colorNeedleCircleInnerEnd,
  596. r2
  597. );
  598. context.fill();
  599. context.closePath();
  600. }
  601.  
  602. context.restore();
  603. }
  604. }
  605.  
  606. /* istanbul ignore next: private, not testable */
  607. /**
  608. * Draws gauge value box
  609. *
  610. * @param {Canvas2DContext} context
  611. * @param {RadialGaugeOptions} options
  612. * @param {number} value
  613. */
  614. function drawRadialValueBox(
  615. context,
  616. options,
  617. value
  618. ) {
  619. drawings.drawValueBox(context, options, value, 0,
  620. context.max - context.max * 0.33, context.max);
  621. }
  622.  
  623. /* istanbul ignore next: private, not testable */
  624. /**
  625. * Computes start and end angle depending on barStartPositionOption
  626. *
  627. * @param {RadialGaugeOptions} options
  628. */
  629. function computeAngles(options) {
  630. let sa = options.startAngle;
  631. let ea = options.startAngle + options.ticksAngle;
  632. let startAngle = sa;
  633. let endAngle = sa +
  634. (drawings.normalizedValue(options).normal -
  635. options.minValue) / (options.maxValue - options.minValue) *
  636. options.ticksAngle;
  637. if (options.barStartPosition === 'middle') {
  638. let midValue = (options.minValue + options.maxValue) * 0.5;
  639. if (options.value < midValue) {
  640. startAngle = 180 - ((
  641. (midValue - drawings.normalizedValue(options).normal) /
  642. (options.maxValue - options.minValue) *
  643. options.ticksAngle
  644. ));
  645. endAngle = 180;
  646. } else {
  647. startAngle = 180;
  648. endAngle = 180 + ((
  649. (drawings.normalizedValue(options).normal - midValue) /
  650. (options.maxValue - options.minValue) *
  651. options.ticksAngle));
  652. }
  653. } else if (options.barStartPosition === 'right') {
  654. startAngle = ea - endAngle + sa;
  655. endAngle = ea;
  656. }
  657. return {startAngle, endAngle};
  658. }
  659.  
  660. /* istanbul ignore next: private, not testable */
  661. /**
  662. * Draws gauge progress bar
  663. *
  664. * @param {Canvas2DContext} context
  665. * @param {RadialGaugeOptions} options
  666. */
  667. function drawRadialProgressBar(context, options) {
  668. let unit = context.max / 100;
  669. let rMax = maxRadialRadius(context, options) - 5 * unit;
  670. let sw = (parseFloat(options.barStrokeWidth + '') || 0);
  671. let w = (parseFloat(options.barWidth + '') || 0) * unit;
  672. let rMin = rMax - sw * 2 - w;
  673. let half = (rMax- rMin) / 2;
  674. let r = rMin + half;
  675. let delta = sw / r;
  676. let sa = options.startAngle;
  677. let ea = options.startAngle + options.ticksAngle;
  678.  
  679. context.save();
  680. context.rotate(HPI);
  681.  
  682. if (sw) {
  683. // draw stroke
  684. context.beginPath();
  685. context.arc(0, 0, r, drawings.radians(sa) - delta,
  686. drawings.radians(ea) + delta, false);
  687. context.strokeStyle = options.colorBarStroke;
  688. context.lineWidth = half * 2;
  689. context.stroke();
  690. context.closePath();
  691. }
  692.  
  693. if (w) {
  694. // draw bar
  695. context.beginPath();
  696. context.arc(0, 0, r, drawings.radians(sa), drawings.radians(ea), false);
  697. context.strokeStyle = options.colorBar;
  698. context.lineWidth = w;
  699. context.stroke();
  700. context.closePath();
  701.  
  702. if (options.barShadow) {
  703. // draw shadow
  704. context.beginPath();
  705. context.arc(0, 0, rMax, drawings.radians(sa), drawings.radians(ea),
  706. false);
  707. context.clip();
  708.  
  709. context.beginPath();
  710. context.strokeStyle = options.colorBar;
  711. context.lineWidth = 1;
  712. context.shadowBlur = options.barShadow;
  713. context.shadowColor = options.colorBarShadow;
  714. context.shadowOffsetX = 0;
  715. context.shadowOffsetY = 0;
  716. context.arc(0, 0, rMax,
  717. drawings.radians(options.startAngle),
  718. drawings.radians(options.startAngle + options.ticksAngle),
  719. false);
  720. context.stroke();
  721. context.closePath();
  722.  
  723. context.restore();
  724. context.rotate(HPI);
  725. }
  726.  
  727. // draw bar progress
  728. if (options.barProgress) {
  729. let angles = computeAngles(options);
  730. let startAngle = angles.startAngle;
  731. let endAngle = angles.endAngle;
  732.  
  733. context.beginPath();
  734. context.arc(0, 0, r,
  735. drawings.radians(startAngle),
  736. drawings.radians(endAngle),
  737. false);
  738. context.strokeStyle = options.colorBarProgress;
  739. context.lineWidth = w;
  740. context.stroke();
  741. context.closePath();
  742. }
  743. }
  744.  
  745. context.restore();
  746. }
  747.  
  748. /**
  749. * Find and return gauge value to display
  750. *
  751. * @param {RadialGauge} gauge
  752. */
  753. function displayValue(gauge) {
  754. if (gauge.options.animatedValue) {
  755. return gauge.options.value;
  756. }
  757.  
  758. return gauge.value;
  759. }
  760.  
  761. /**
  762. * Minimalistic HTML5 Canvas Gauge
  763. * @example
  764. * var gauge = new RadialGauge({
  765. * renderTo: 'gauge-id', // identifier of HTML canvas element or element itself
  766. * width: 400,
  767. * height: 400,
  768. * units: 'Km/h',
  769. * title: false,
  770. * value: 0,
  771. * minValue: 0,
  772. * maxValue: 220,
  773. * majorTicks: [
  774. * '0','20','40','60','80','100','120','140','160','180','200','220'
  775. * ],
  776. * minorTicks: 2,
  777. * strokeTicks: false,
  778. * highlights: [
  779. * { from: 0, to: 50, color: 'rgba(0,255,0,.15)' },
  780. * { from: 50, to: 100, color: 'rgba(255,255,0,.15)' },
  781. * { from: 100, to: 150, color: 'rgba(255,30,0,.25)' },
  782. * { from: 150, to: 200, color: 'rgba(255,0,225,.25)' },
  783. * { from: 200, to: 220, color: 'rgba(0,0,255,.25)' }
  784. * ],
  785. * colorPlate: '#222',
  786. * colorMajorTicks: '#f5f5f5',
  787. * colorMinorTicks: '#ddd',
  788. * colorTitle: '#fff',
  789. * colorUnits: '#ccc',
  790. * colorNumbers: '#eee',
  791. * colorNeedleStart: 'rgba(240, 128, 128, 1)',
  792. * colorNeedleEnd: 'rgba(255, 160, 122, .9)',
  793. * valueBox: true,
  794. * animationRule: 'bounce'
  795. * });
  796. * // draw initially
  797. * gauge.draw();
  798. * // animate
  799. * setInterval(() => {
  800. * gauge.value = Math.random() * -220 + 220;
  801. * }, 1000);
  802. */
  803. export default class RadialGauge extends BaseGauge {
  804.  
  805. /**
  806. * Fired each time before gauge plate is drawn
  807. *
  808. * @event RadialGauge#beforePlate
  809. */
  810.  
  811. /**
  812. * Fired each time before gauge highlight areas are drawn
  813. *
  814. * @event RadialGauge#beforeHighlights
  815. */
  816.  
  817. /**
  818. * Fired each time before gauge minor ticks are drawn
  819. *
  820. * @event RadialGauge#beforeMinorTicks
  821. */
  822.  
  823. /**
  824. * Fired each time before gauge major ticks are drawn
  825. *
  826. * @event RadialGauge#beforeMajorTicks
  827. */
  828.  
  829. /**
  830. * Fired each time before gauge tick numbers are drawn
  831. *
  832. * @event RadialGauge#beforeNumbers
  833. */
  834.  
  835. /**
  836. * Fired each time before gauge title is drawn
  837. *
  838. * @event RadialGauge#beforeTitle
  839. */
  840.  
  841. /**
  842. * Fired each time before gauge units text is drawn
  843. *
  844. * @event RadialGauge#beforeUnits
  845. */
  846.  
  847. /**
  848. * Fired each time before gauge progress bar is drawn
  849. *
  850. * @event RadialGauge#beforeProgressBar
  851. */
  852.  
  853. /**
  854. * Fired each time before gauge value box is drawn
  855. *
  856. * @event RadialGauge#beforeValueBox
  857. */
  858.  
  859. /**
  860. * Fired each time before gauge needle is drawn
  861. *
  862. * @event RadialGauge#beforeNeedle
  863. */
  864.  
  865. /**
  866. * @constructor
  867. * @param {RadialGaugeOptions} options
  868. */
  869. constructor(options) {
  870. options = Object.assign({}, defaultRadialGaugeOptions, options || {});
  871. super(RadialGauge.configure(options));
  872. }
  873.  
  874. /**
  875. * Checks and updates gauge options properly
  876. *
  877. * @param {*} options
  878. * @return {*}
  879. * @access protected
  880. */
  881. static configure(options) {
  882. if (options.barWidth > 50) options.barWidth = 50;
  883.  
  884. /* istanbul ignore if */
  885. if (isNaN(options.startAngle)) options.startAngle = 45;
  886. /* istanbul ignore if */
  887. if (isNaN(options.ticksAngle)) options.ticksAngle = 270;
  888.  
  889. /* istanbul ignore if */
  890. if (options.ticksAngle > 360) options.ticksAngle = 360;
  891. /* istanbul ignore if */
  892. if (options.ticksAngle < 0) options.ticksAngle = 0;
  893.  
  894. /* istanbul ignore if */
  895. if (options.startAngle < 0) options.startAngle = 0;
  896. /* istanbul ignore if */
  897. if (options.startAngle > 360) options.startAngle = 360;
  898.  
  899. return options;
  900. }
  901.  
  902. /**
  903. * Sets the value for radial gauge
  904. *
  905. * @param {number} value
  906. */
  907. set value(value) {
  908. value = BaseGauge.ensureValue(value, this.options.minValue);
  909.  
  910. if (this.options.animation &&
  911. this.options.ticksAngle === 360 &&
  912. this.options.useMinPath
  913. ) {
  914. this._value = value;
  915. value = this.options.value +
  916. ((((value - this.options.value) % 360) + 540) % 360) - 180;
  917. }
  918.  
  919. super.value = value;
  920. }
  921.  
  922. /**
  923. * Returns current gauge value
  924. *
  925. * @return {number}
  926. */
  927. get value() {
  928. return super.value;
  929. }
  930.  
  931. /**
  932. * Triggering gauge render on a canvas.
  933. *
  934. * @returns {RadialGauge}
  935. */
  936. draw() {
  937. try {
  938. let canvas = this.canvas;
  939. let [x, y, w, h] = [
  940. -canvas.drawX,
  941. -canvas.drawY,
  942. canvas.drawWidth,
  943. canvas.drawHeight
  944. ];
  945. let options = this.options;
  946.  
  947. if (options.animationTarget === 'needle') {
  948. if (!canvas.elementClone.initialized) {
  949. let context = canvas.contextClone;
  950.  
  951. // clear the cache
  952. context.clearRect(x, y, w, h);
  953. context.save();
  954.  
  955. this.emit('beforePlate');
  956. drawRadialPlate(context, options);
  957. this.emit('beforeHighlights');
  958. drawRadialHighlights(context, options);
  959. this.emit('beforeMinorTicks');
  960. drawRadialMinorTicks(context, options);
  961. this.emit('beforeMajorTicks');
  962. drawRadialMajorTicks(context, options);
  963. this.emit('beforeNumbers');
  964. drawRadialNumbers(context, options);
  965. this.emit('beforeTitle');
  966. drawRadialTitle(context, options);
  967. this.emit('beforeUnits');
  968. drawRadialUnits(context, options);
  969.  
  970. canvas.elementClone.initialized = true;
  971. }
  972.  
  973. this.canvas.commit();
  974.  
  975. // clear the canvas
  976. canvas.context.clearRect(x, y, w, h);
  977. canvas.context.save();
  978.  
  979. canvas.context.drawImage(canvas.elementClone, x, y, w, h);
  980. canvas.context.save();
  981.  
  982. this.emit('beforeProgressBar');
  983. drawRadialProgressBar(canvas.context, options);
  984. this.emit('beforeValueBox');
  985. drawRadialValueBox(canvas.context, options, displayValue(this));
  986. this.emit('beforeNeedle');
  987. drawRadialNeedle(canvas.context, options);
  988. }
  989.  
  990. else {
  991. let plateValueAngle = -drawings.radians(
  992. (options.value - options.minValue) /
  993. (options.maxValue - options.minValue) *
  994. options.ticksAngle);
  995.  
  996. // clear the canvas
  997. canvas.context.clearRect(x, y, w, h);
  998. canvas.context.save();
  999.  
  1000. this.emit('beforePlate');
  1001. drawRadialPlate(canvas.context, options);
  1002.  
  1003. canvas.context.rotate(plateValueAngle);
  1004.  
  1005. // animated
  1006. this.emit('beforeHighlights');
  1007. drawRadialHighlights(canvas.context, options);
  1008. this.emit('beforeMinorTicks');
  1009. drawRadialMinorTicks(canvas.context, options);
  1010. this.emit('beforeMajorTicks');
  1011. drawRadialMajorTicks(canvas.context, options);
  1012. this.emit('beforeNumbers');
  1013. drawRadialNumbers(canvas.context, options);
  1014. this.emit('beforeProgressBar');
  1015. drawRadialProgressBar(canvas.context, options);
  1016.  
  1017. // non-animated
  1018. canvas.context.rotate(-plateValueAngle);
  1019. canvas.context.save();
  1020.  
  1021. if (!canvas.elementClone.initialized) {
  1022. let context = canvas.contextClone;
  1023.  
  1024. // clear the cache
  1025. context.clearRect(x, y, w, h);
  1026. context.save();
  1027.  
  1028. this.emit('beforeTitle');
  1029. drawRadialTitle(context, options);
  1030. this.emit('beforeUnits');
  1031. drawRadialUnits(context, options);
  1032. this.emit('beforeNeedle');
  1033. drawRadialNeedle(context, options);
  1034.  
  1035. canvas.elementClone.initialized = true;
  1036. }
  1037.  
  1038. canvas.context.drawImage(canvas.elementClone, x, y, w, h);
  1039. }
  1040.  
  1041. // value box animations
  1042. this.emit('beforeValueBox');
  1043. drawRadialValueBox(canvas.context, options, displayValue(this));
  1044.  
  1045. super.draw();
  1046. }
  1047.  
  1048. catch (err) {
  1049. drawings.verifyError(err);
  1050. }
  1051.  
  1052. return this;
  1053. }
  1054. }
  1055.  
  1056.  
  1057. /**
  1058. * @ignore
  1059. * @typedef {object} ns
  1060. */
  1061. /* istanbul ignore if */
  1062. if (typeof ns !== 'undefined') {
  1063. ns['RadialGauge'] = RadialGauge;
  1064. }
  1065.  
  1066. BaseGauge.initialize('RadialGauge', defaultRadialGaugeOptions);
  1067.  
  1068. module.exports = RadialGauge;