graph.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.82
  5. */
  6. import { proxyCustomElement, HTMLElement, forceUpdate, h } from '@stencil/core/internal/client';
  7. import { g as guid } from './guid.js';
  8. import { c as createObserver } from './observers.js';
  9. /**
  10. * Calculate slope of the tangents
  11. * uses Steffen interpolation as it's monotonic
  12. * http://jrwalsh1.github.io/posts/interpolations/
  13. */
  14. function slope(p0, p1, p2) {
  15. const dx = p1[0] - p0[0];
  16. const dx1 = p2[0] - p1[0];
  17. const dy = p1[1] - p0[1];
  18. const dy1 = p2[1] - p1[1];
  19. const m = dy / (dx || (dx1 < 0 && 0));
  20. const m1 = dy1 / (dx1 || (dx < 0 && 0));
  21. const p = (m * dx1 + m1 * dx) / (dx + dx1);
  22. return (Math.sign(m) + Math.sign(m1)) * Math.min(Math.abs(m), Math.abs(m1), 0.5 * Math.abs(p)) || 0;
  23. }
  24. /**
  25. * Calculate slope for just one tangent (single-sided)
  26. */
  27. function slopeSingle(p0, p1, m) {
  28. const dx = p1[0] - p0[0];
  29. const dy = p1[1] - p0[1];
  30. return dx ? ((3 * dy) / dx - m) / 2 : m;
  31. }
  32. /**
  33. * Given two points and their tangent slopes,
  34. * calculate the bezier handle coordinates and return draw command.
  35. *
  36. * Translates Hermite Spline to Beziér curve:
  37. * stackoverflow.com/questions/42574940/
  38. */
  39. function bezier(p0, p1, m0, m1, t) {
  40. const [x0, y0] = p0;
  41. const [x1, y1] = p1;
  42. const dx = (x1 - x0) / 3;
  43. const h1 = t([x0 + dx, y0 + dx * m0]).join(",");
  44. const h2 = t([x1 - dx, y1 - dx * m1]).join(",");
  45. const p = t([x1, y1]).join(",");
  46. return `C ${h1} ${h2} ${p}`;
  47. }
  48. /**
  49. * Generate a function which will translate a point
  50. * from the data coordinate space to svg viewbox oriented pixels
  51. */
  52. function translate({ width, height, min, max }) {
  53. const rangeX = max[0] - min[0];
  54. const rangeY = max[1] - min[1];
  55. return (point) => {
  56. const x = ((point[0] - min[0]) / rangeX) * width;
  57. const y = height - (point[1] / rangeY) * height;
  58. return [x, y];
  59. };
  60. }
  61. /**
  62. * Get the min and max values from the dataset
  63. */
  64. function range(data) {
  65. const [startX, startY] = data[0];
  66. const min = [startX, startY];
  67. const max = [startX, startY];
  68. return data.reduce(({ min, max }, [x, y]) => ({
  69. min: [Math.min(min[0], x), Math.min(min[1], y)],
  70. max: [Math.max(max[0], x), Math.max(max[1], y)]
  71. }), { min, max });
  72. }
  73. /**
  74. * Generate drawing commands for an area graph
  75. * returns a string can can be passed directly to a path element's `d` attribute
  76. */
  77. function area({ data, min, max, t }) {
  78. if (data.length === 0) {
  79. return "";
  80. }
  81. // important points for beginning and ending the path
  82. const [startX, startY] = t(data[0]);
  83. const [minX, minY] = t(min);
  84. const [maxX] = t(max);
  85. // keep track of previous slope/points
  86. let m;
  87. let p0;
  88. let p1;
  89. // iterate over data points, calculating command for each
  90. const commands = data.reduce((acc, point, i) => {
  91. p0 = data[i - 2];
  92. p1 = data[i - 1];
  93. if (i > 1) {
  94. const m1 = slope(p0, p1, point);
  95. const m0 = m === undefined ? slopeSingle(p0, p1, m1) : m;
  96. const command = bezier(p0, p1, m0, m1, t);
  97. m = m1;
  98. return `${acc} ${command}`;
  99. }
  100. return acc;
  101. }, `M ${minX},${minY} L ${minX},${startY} L ${startX},${startY}`);
  102. // close the path
  103. const last = data[data.length - 1];
  104. const end = bezier(p1, last, m, slopeSingle(p1, last, m), t);
  105. return `${commands} ${end} L ${maxX},${minY} Z`;
  106. }
  107. const graphCss = "@-webkit-keyframes in{0%{opacity:0}100%{opacity:1}}@keyframes in{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes in-down{0%{opacity:0;-webkit-transform:translate3D(0, -5px, 0);transform:translate3D(0, -5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@keyframes in-down{0%{opacity:0;-webkit-transform:translate3D(0, -5px, 0);transform:translate3D(0, -5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@-webkit-keyframes in-up{0%{opacity:0;-webkit-transform:translate3D(0, 5px, 0);transform:translate3D(0, 5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@keyframes in-up{0%{opacity:0;-webkit-transform:translate3D(0, 5px, 0);transform:translate3D(0, 5px, 0)}100%{opacity:1;-webkit-transform:translate3D(0, 0, 0);transform:translate3D(0, 0, 0)}}@-webkit-keyframes in-scale{0%{opacity:0;-webkit-transform:scale3D(0.95, 0.95, 1);transform:scale3D(0.95, 0.95, 1)}100%{opacity:1;-webkit-transform:scale3D(1, 1, 1);transform:scale3D(1, 1, 1)}}@keyframes in-scale{0%{opacity:0;-webkit-transform:scale3D(0.95, 0.95, 1);transform:scale3D(0.95, 0.95, 1)}100%{opacity:1;-webkit-transform:scale3D(1, 1, 1);transform:scale3D(1, 1, 1)}}:root{--calcite-animation-timing:calc(150ms * var(--calcite-internal-duration-factor));--calcite-internal-duration-factor:var(--calcite-duration-factor, 1);--calcite-internal-animation-timing-fast:calc(100ms * var(--calcite-internal-duration-factor));--calcite-internal-animation-timing-medium:calc(200ms * var(--calcite-internal-duration-factor));--calcite-internal-animation-timing-slow:calc(300ms * var(--calcite-internal-duration-factor))}.calcite-animate{opacity:0;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:var(--calcite-animation-timing);animation-duration:var(--calcite-animation-timing)}.calcite-animate__in{-webkit-animation-name:in;animation-name:in}.calcite-animate__in-down{-webkit-animation-name:in-down;animation-name:in-down}.calcite-animate__in-up{-webkit-animation-name:in-up;animation-name:in-up}.calcite-animate__in-scale{-webkit-animation-name:in-scale;animation-name:in-scale}:root{--calcite-popper-transition:var(--calcite-animation-timing)}:host([hidden]){display:none}:host{display:block}.svg{fill:currentColor;stroke:transparent;margin:0px;display:block;height:100%;width:100%;padding:0px}.svg .graph-path--highlight{fill:var(--calcite-ui-brand);opacity:0.5}";
  108. const Graph = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
  109. constructor() {
  110. super();
  111. this.__registerHost();
  112. this.__attachShadow();
  113. //--------------------------------------------------------------------------
  114. //
  115. // Properties
  116. //
  117. //--------------------------------------------------------------------------
  118. /**
  119. * Array of tuples describing a single data point ([x, y])
  120. * These data points should be sorted by x-axis value
  121. */
  122. this.data = [];
  123. //--------------------------------------------------------------------------
  124. //
  125. // Private State/Props
  126. //
  127. //--------------------------------------------------------------------------
  128. this.graphId = `calcite-graph-${guid()}`;
  129. this.resizeObserver = createObserver("resize", () => forceUpdate(this));
  130. }
  131. //--------------------------------------------------------------------------
  132. //
  133. // Lifecycle
  134. //
  135. //--------------------------------------------------------------------------
  136. connectedCallback() {
  137. var _a;
  138. (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.observe(this.el);
  139. }
  140. disconnectedCallback() {
  141. var _a;
  142. (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
  143. }
  144. render() {
  145. const { data, colorStops, el, highlightMax, highlightMin, min, max } = this;
  146. const id = this.graphId;
  147. const { clientHeight: height, clientWidth: width } = el;
  148. // if we have no data, return empty svg
  149. if (!data || data.length === 0) {
  150. return (h("svg", { class: "svg", height: height, preserveAspectRatio: "none", viewBox: `0 0 ${width} ${height}`, width: width }));
  151. }
  152. const { min: rangeMin, max: rangeMax } = range(data);
  153. let currentMin = rangeMin;
  154. let currentMax = rangeMax;
  155. if (min < rangeMin[0] || min > rangeMin[0]) {
  156. currentMin = [min, 0];
  157. }
  158. if (max > rangeMax[0] || max < rangeMax[0]) {
  159. currentMax = [max, rangeMax[1]];
  160. }
  161. const t = translate({ min: currentMin, max: currentMax, width, height });
  162. const [hMinX] = t([highlightMin, currentMax[1]]);
  163. const [hMaxX] = t([highlightMax, currentMax[1]]);
  164. const areaPath = area({ data, min: rangeMin, max: rangeMax, t });
  165. const fill = colorStops ? `url(#linear-gradient-${id})` : undefined;
  166. return (h("svg", { class: "svg", height: height, preserveAspectRatio: "none", viewBox: `0 0 ${width} ${height}`, width: width }, colorStops ? (h("defs", null, h("linearGradient", { id: `linear-gradient-${id}`, x1: "0", x2: "1", y1: "0", y2: "0" }, colorStops.map(({ offset, color, opacity }) => (h("stop", { offset: `${offset * 100}%`, "stop-color": color, "stop-opacity": opacity })))))) : null, highlightMin !== undefined ? ([
  167. h("mask", { height: "100%", id: `${id}1`, width: "100%", x: "0%", y: "0%" }, h("path", { d: `
  168. M 0,0
  169. L ${hMinX - 1},0
  170. L ${hMinX - 1},${height}
  171. L 0,${height}
  172. Z
  173. `, fill: "white" })),
  174. h("mask", { height: "100%", id: `${id}2`, width: "100%", x: "0%", y: "0%" }, h("path", { d: `
  175. M ${hMinX + 1},0
  176. L ${hMaxX - 1},0
  177. L ${hMaxX - 1},${height}
  178. L ${hMinX + 1}, ${height}
  179. Z
  180. `, fill: "white" })),
  181. h("mask", { height: "100%", id: `${id}3`, width: "100%", x: "0%", y: "0%" }, h("path", { d: `
  182. M ${hMaxX + 1},0
  183. L ${width},0
  184. L ${width},${height}
  185. L ${hMaxX + 1}, ${height}
  186. Z
  187. `, fill: "white" })),
  188. h("path", { class: "graph-path", d: areaPath, fill: fill, mask: `url(#${id}1)` }),
  189. h("path", { class: "graph-path--highlight", d: areaPath, fill: fill, mask: `url(#${id}2)` }),
  190. h("path", { class: "graph-path", d: areaPath, fill: fill, mask: `url(#${id}3)` })
  191. ]) : (h("path", { class: "graph-path", d: areaPath, fill: fill }))));
  192. }
  193. get el() { return this; }
  194. static get style() { return graphCss; }
  195. }, [1, "calcite-graph", {
  196. "data": [16],
  197. "colorStops": [16],
  198. "highlightMin": [2, "highlight-min"],
  199. "highlightMax": [2, "highlight-max"],
  200. "min": [2],
  201. "max": [2]
  202. }]);
  203. function defineCustomElement() {
  204. if (typeof customElements === "undefined") {
  205. return;
  206. }
  207. const components = ["calcite-graph"];
  208. components.forEach(tagName => { switch (tagName) {
  209. case "calcite-graph":
  210. if (!customElements.get(tagName)) {
  211. customElements.define(tagName, Graph);
  212. }
  213. break;
  214. } });
  215. }
  216. defineCustomElement();
  217. export { Graph as G, defineCustomElement as d };