Plugin/sparkline/index.ts

  1. /**
  2. * Copyright (c) 2021 ~ present NAVER Corp.
  3. * billboard.js project is licensed under the MIT license
  4. */
  5. import type {IData} from "../../ChartInternal/data/IData";
  6. import {$COMMON} from "../../config/classes";
  7. import {loadConfig} from "../../config/config";
  8. import Plugin from "../Plugin";
  9. import Options from "./Options";
  10. /**
  11. * Sparkline plugin.<br>
  12. * Generates sparkline charts
  13. * - **NOTE:**
  14. * - Plugins aren't built-in. Need to be loaded or imported to be used.
  15. * - Non required modules from billboard.js core, need to be installed separately.
  16. *
  17. * - **Bear in mind:**
  18. * - Use this plugin to visualize multiple tiny chart only and chart APIs won't work properly.
  19. * - Sparkline chart size will be based on the main chart element size. To control spakrline charts, is highly recommended to set `size` option.
  20. * - Bubble, scatter and Arc(pie, donut, ratdar) types aren't supported.
  21. * - Some options will be stricted to be:
  22. * - `resize.auto = false`
  23. * - `axis.x.show = false`
  24. * - `axis.y.show = false`
  25. * - `axis.y.padding = 10`
  26. * - `legend.show = false`
  27. *
  28. * @class plugin-sparkline
  29. * @param {object} options sparkline plugin options
  30. * @augments Plugin
  31. * @returns {Sparkline}
  32. * @example
  33. * // Plugin must be loaded before the use.
  34. * <script src="$YOUR_PATH/plugin/billboardjs-plugin-sparkline.js"></script>
  35. *
  36. * var chart = bb.generate({
  37. * ...
  38. * plugins: [
  39. * new bb.plugin.sparkline({
  40. * selector: ".sparkline"
  41. * }),
  42. * ]
  43. * });
  44. * @example
  45. * import {bb} from "billboard.js";
  46. * import Sparkline from "billboard.js/dist/billboardjs-plugin-sparkline";
  47. *
  48. * bb.generate({
  49. * ...
  50. * plugins: [
  51. * new Sparkline({ ... })
  52. * ]
  53. * })
  54. */
  55. export default class Sparkline extends Plugin {
  56. static version = `0.0.1`;
  57. private config;
  58. private element;
  59. constructor(options) {
  60. super(options);
  61. this.config = new Options();
  62. return this;
  63. }
  64. $beforeInit(): void {
  65. loadConfig.call(this, this.options);
  66. this.validate();
  67. this.element = [].slice.call(document.querySelectorAll(this.config.selector));
  68. // override internal methods
  69. this.overrideInternals();
  70. // override options
  71. this.overrideOptions();
  72. // bind event handlers's context
  73. this.overHandler = this.overHandler.bind(this);
  74. this.moveHandler = this.moveHandler.bind(this);
  75. this.outHandler = this.outHandler.bind(this);
  76. }
  77. validate(): void {
  78. const {$$, config} = this;
  79. let msg = "";
  80. if (!config.selector || !document.querySelector(config.selector)) {
  81. msg = "No holder elements found from given selector option.";
  82. }
  83. if ($$.hasType("bubble") || $$.hasType("scatter") || $$.hasArcType($$.data.targets)) {
  84. msg = "Contains non supported chart types.";
  85. }
  86. if (msg) {
  87. throw new Error(`[Sparkline plugin] ${msg}`);
  88. }
  89. }
  90. overrideInternals(): void {
  91. const {$$} = this;
  92. const {getBarW, getIndices} = $$;
  93. // override internal methods to positioning bars
  94. $$.getIndices = function(indices, d, caller) {
  95. return caller === "getShapeX" ? {} : getIndices.call(this, indices, d);
  96. };
  97. $$.getBarW = function(type, axis) {
  98. return getBarW.call(this, type, axis, 1);
  99. };
  100. }
  101. overrideOptions(): void {
  102. const {config} = this.$$;
  103. config.legend_show = false;
  104. config.resize_auto = false;
  105. config.axis_x_show = false;
  106. // set default axes padding
  107. if (config.padding !== false) {
  108. const hasOption = o => Object.keys(o || {}).length > 0;
  109. if (hasOption(config.axis_x_padding)) {
  110. config.axis_x_padding = {
  111. left: 15,
  112. right: 15,
  113. unit: "px"
  114. };
  115. }
  116. if (hasOption(config.axis_y_padding)) {
  117. config.axis_y_padding = 5;
  118. }
  119. }
  120. config.axis_y_show = false;
  121. if (!config.tooltip_position) {
  122. config.tooltip_position = function(data, width, height) {
  123. const {internal: {state: {event}}} = this;
  124. let top = event.pageY - (height * 1.35);
  125. let left = event.pageX - (width / 2);
  126. if (top < 0) {
  127. top = 0;
  128. }
  129. if (left < 0) {
  130. left = 0;
  131. }
  132. return {top, left};
  133. };
  134. }
  135. }
  136. $init(): void {
  137. const {$$: {$el}} = this;
  138. // make disable-ish main chart element
  139. $el.chart
  140. .style("width", "0")
  141. .style("height", "0")
  142. .style("pointer-events", "none");
  143. $el.tooltip?.node() && document.body.appendChild($el.tooltip.node());
  144. }
  145. $afterInit(): void {
  146. const {$$} = this;
  147. $$.$el.svg.attr("style", null)
  148. .style("width", "0")
  149. .style("height", "0");
  150. this.bindEvents(true);
  151. }
  152. /**
  153. * Bind tooltip event handlers for each sparkline elements.
  154. * @param {boolean} bind or unbind
  155. * @private
  156. */
  157. bindEvents(bind = true): void {
  158. const {$$: {config}} = this;
  159. if (config.interaction_enabled && config.tooltip_show) {
  160. const method = `${bind ? "add" : "remove"}EventListener`;
  161. this.element
  162. .forEach(el => {
  163. const svg = el.querySelector("svg");
  164. svg[method]("mouseover", this.overHandler);
  165. svg[method]("mousemove", this.moveHandler);
  166. svg[method]("mouseout", this.outHandler);
  167. });
  168. }
  169. }
  170. overHandler(e): void {
  171. const {$$} = this;
  172. const {state: {eventReceiver}} = $$;
  173. eventReceiver.rect = e.target.getBoundingClientRect();
  174. }
  175. moveHandler(e): void {
  176. const {$$} = this;
  177. const index = $$.getDataIndexFromEvent(e);
  178. const data = $$.api.data(e.target.__id)?.[0] as IData;
  179. const d = data?.values?.[index];
  180. if (d && !d.name) {
  181. d.name = d.id;
  182. }
  183. $$.state.event = e;
  184. if ($$.isPointFocusOnly?.() && d) {
  185. $$.showCircleFocus?.([d]);
  186. }
  187. $$.setExpand(index, data.id, true);
  188. $$.showTooltip([d], e.target);
  189. }
  190. outHandler(e): void {
  191. const {$$} = this;
  192. $$.state.event = e;
  193. $$.isPointFocusOnly() ? $$.hideCircleFocus() : $$.unexpandCircles();
  194. $$.hideTooltip();
  195. }
  196. $redraw(): void {
  197. const {$$} = this;
  198. const {$el} = $$;
  199. let el = this.element;
  200. const data = $$.api.data();
  201. const svgWrapper = $el.chart.html().match(/<svg[^>]*>/)?.[0];
  202. // append sparkline holder if is less than the data length
  203. if (el.length < data.length) {
  204. const chart = $el.chart.node();
  205. for (let i = data.length - el.length; i > 0; i--) {
  206. chart.parentNode.insertBefore(el[0].cloneNode(), chart.nextSibling);
  207. }
  208. this.element = document.querySelectorAll(this.config.selector);
  209. el = this.element;
  210. }
  211. data.map(v => v.id)
  212. .forEach((id, i) => {
  213. const selector = `.${$COMMON.target}-${id}`;
  214. const shape = $el.main.selectAll(selector);
  215. let svg = el[i].querySelector("svg");
  216. if (!svg) {
  217. el[i].innerHTML = `${svgWrapper}</svg>`;
  218. svg = el[i].querySelector("svg");
  219. svg.__id = id;
  220. }
  221. if (!svg.querySelector(selector)) {
  222. shape.style("opacity", null);
  223. }
  224. shape
  225. .style("fill", "none")
  226. .style("opacity", null);
  227. svg.innerHTML = "";
  228. svg.appendChild(shape.node());
  229. });
  230. }
  231. $willDestroy(): void {
  232. this.bindEvents(false);
  233. this.element
  234. .forEach(el => {
  235. el.innerHTML = "";
  236. });
  237. }
  238. }