UNPKG

infinity-trend

Version:

A React component designed for effortlessly creating smooth, responsive trend and line graphs.

651 lines (593 loc) 24.6 kB
/*! * infinity-trend v0.0.3 - https://infinity-development.github.io/infinity-trend/ * MIT Licensed */ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(require("react")); else if(typeof define === 'function' && define.amd) define(["react"], factory); else if(typeof exports === 'object') exports["Trend"] = factory(require("react")); else root["Trend"] = factory(root["React"]); })(window, function(__WEBPACK_EXTERNAL_MODULE__1__) { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); /******/ } /******/ }; /******/ /******/ // define __esModule on exports /******/ __webpack_require__.r = function(exports) { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ /******/ // create a fake namespace object /******/ // mode & 1: value is a module id, require it /******/ // mode & 2: merge all properties of value into the ns /******/ // mode & 4: return value when already ns object /******/ // mode & 8|1: behave like require /******/ __webpack_require__.t = function(value, mode) { /******/ if(mode & 1) value = __webpack_require__(value); /******/ if(mode & 8) return value; /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; /******/ var ns = Object.create(null); /******/ __webpack_require__.r(ns); /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); /******/ return ns; /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 2); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { /** * Copyright (c) 2013-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ if (false) { var throwOnDirectAccess, ReactIs; } else { // By explicitly using `prop-types` you are opting into new production behavior. // http://fb.me/prop-types-in-prod module.exports = __webpack_require__(3)(); } /***/ }), /* 1 */ /***/ (function(module, exports) { module.exports = __WEBPACK_EXTERNAL_MODULE__1__; /***/ }), /* 2 */ /***/ (function(module, exports, __webpack_require__) { module.exports = __webpack_require__(5); /***/ }), /* 3 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /** * Copyright (c) 2013-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var ReactPropTypesSecret = __webpack_require__(4); function emptyFunction() {} function emptyFunctionWithReset() {} emptyFunctionWithReset.resetWarningCache = emptyFunction; module.exports = function() { function shim(props, propName, componentName, location, propFullName, secret) { if (secret === ReactPropTypesSecret) { // It is still safe when called from React. return; } var err = new Error( 'Calling PropTypes validators directly is not supported by the `prop-types` package. ' + 'Use PropTypes.checkPropTypes() to call them. ' + 'Read more at http://fb.me/use-check-prop-types' ); err.name = 'Invariant Violation'; throw err; }; shim.isRequired = shim; function getShim() { return shim; }; // Important! // Keep this list in sync with production version in `./factoryWithTypeCheckers.js`. var ReactPropTypes = { array: shim, bigint: shim, bool: shim, func: shim, number: shim, object: shim, string: shim, symbol: shim, any: shim, arrayOf: getShim, element: shim, elementType: shim, instanceOf: getShim, node: shim, objectOf: getShim, oneOf: getShim, oneOfType: getShim, shape: getShim, exact: getShim, checkPropTypes: emptyFunctionWithReset, resetWarningCache: emptyFunction }; ReactPropTypes.PropTypes = ReactPropTypes; return ReactPropTypes; }; /***/ }), /* 4 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /** * Copyright (c) 2013-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; module.exports = ReactPropTypesSecret; /***/ }), /* 5 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; // ESM COMPAT FLAG __webpack_require__.r(__webpack_exports__); // EXPORTS __webpack_require__.d(__webpack_exports__, "default", function() { return /* reexport */ components_Trend_Trend; }); // EXTERNAL MODULE: external {"root":"React","commonjs2":"react","commonjs":"react","amd":"react"} var external_root_React_commonjs2_react_commonjs_react_amd_react_ = __webpack_require__(1); var external_root_React_commonjs2_react_commonjs_react_amd_react_default = /*#__PURE__*/__webpack_require__.n(external_root_React_commonjs2_react_commonjs_react_amd_react_); // EXTERNAL MODULE: ./node_modules/prop-types/index.js var prop_types = __webpack_require__(0); var prop_types_default = /*#__PURE__*/__webpack_require__.n(prop_types); // CONCATENATED MODULE: ./src/utils/index.js function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } var range = function range(to) { return [].concat(Array(to).keys()); }; var pick = function pick(obj, keys) { return keys.reduce(function (acc, key) { var _extends2; return _extends({}, acc, (_extends2 = {}, _extends2[key] = obj[key], _extends2)); }, {}); }; var omit = function omit(obj, keys) { return Object.keys(obj).reduce(function (acc, key) { var _extends3; if (keys.indexOf(key) !== -1) { return acc; } return _extends({}, acc, (_extends3 = {}, _extends3[key] = obj[key], _extends3)); }, {}); }; // CONCATENATED MODULE: ./src/helpers/math.helpers.js /* eslint-disable no-restricted-properties */ /** normalize * This lets us translate a value from one scale to another. * * @param {Number} value - Our initial value to translate * @param {Number} min - the current minimum value possible * @param {Number} max - the current maximum value possible * @param {Number} scaleMin - the min value of the scale we're translating to * @param {Number} scaleMax - the max value of the scale we're translating to * * @returns {Number} the value on its new scale */ var normalize = function normalize(_ref) { var value = _ref.value, min = _ref.min, max = _ref.max, _ref$scaleMin = _ref.scaleMin, scaleMin = _ref$scaleMin === void 0 ? 0 : _ref$scaleMin, _ref$scaleMax = _ref.scaleMax, scaleMax = _ref$scaleMax === void 0 ? 1 : _ref$scaleMax; // If the `min` and `max` are the same value, it means our dataset is flat. // For now, let's assume that flat data should be aligned to the bottom. if (min === max) { return scaleMin; } return scaleMin + (value - min) * (scaleMax - scaleMin) / (max - min); }; /** moveTo * the coordinate that lies at a midpoint between 2 lines, based on the radius * * @param {Object} to - Our initial point * @param {Number} to.x - The x value of our initial point * @param {Number} to.y - The y value of our initial point * @param {Object} from - Our final point * @param {Number} from.x - The x value of our final point * @param {Number} from.y - The y value of our final point * @param {Number} radius - The distance away from the final point * * @returns {Object} an object holding the x/y coordinates of the midpoint. */ var moveTo = function moveTo(to, from, radius) { var vector = { x: to.x - from.x, y: to.y - from.y }; var length = Math.sqrt(vector.x * vector.x + vector.y * vector.y); var unitVector = { x: vector.x / length, y: vector.y / length }; return { x: from.x + unitVector.x * radius, y: from.y + unitVector.y * radius }; }; /** getDistanceBetween * Simple formula derived from pythagoras to calculate the distance between * 2 points on a plane. * * @param {Object} p1 - Our initial point * @param {Number} p1.x - The x value of our initial point * @param {Number} p1.y - The y value of our initial point * @param {Object} p2 - Our final point * @param {Number} p2.x - The x value of our final point * @param {Number} p2.y - The y value of our final point * * @returns {Number} the distance between the points. */ var getDistanceBetween = function getDistanceBetween(p1, p2) { return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); }; /** checkForCollinearPoints * Figure out if the midpoint fits perfectly on a line between the two others. * * @param {Object} p1 - Our initial point * @param {Number} p1.x - The x value of our initial point * @param {Number} p1.y - The y value of our initial point * @param {Object} p2 - Our mid-point * @param {Number} p2.x - The x value of our mid-point * @param {Number} p2.y - The y value of our mid-point * @param {Object} p3 - Our final point * @param {Number} p3.x - The x value of our final point * @param {Number} p3.y - The y value of our final point * @returns {Boolean} whether or not p2 sits on the line between p1 and p3. */ var checkForCollinearPoints = function checkForCollinearPoints(p1, p2, p3) { return (p1.y - p2.y) * (p1.x - p3.x) === (p1.y - p3.y) * (p1.x - p2.x); }; // CONCATENATED MODULE: ./src/helpers/DOM.helpers.js var buildLinearPath = function buildLinearPath(data) { return data.reduce(function (path, _ref, index) { var x = _ref.x, y = _ref.y; // The very first instruction needs to be a "move". // The rest will be a "line". var isFirstInstruction = index === 0; var instruction = isFirstInstruction ? 'M' : 'L'; return "" + path + instruction + " " + x + "," + y + "\n"; }, ''); }; var DOM_helpers_buildSmoothPath = function buildSmoothPath(data, _ref2) { var radius = _ref2.radius; var firstPoint = data[0], otherPoints = data.slice(1); return otherPoints.reduce(function (path, point, index) { var next = otherPoints[index + 1]; var prev = otherPoints[index - 1] || firstPoint; var isCollinear = next && checkForCollinearPoints(prev, point, next); if (!next || isCollinear) { // The very last line in the sequence can just be a regular line. return path + "\nL " + point.x + "," + point.y; } var distanceFromPrev = getDistanceBetween(prev, point); var distanceFromNext = getDistanceBetween(next, point); var threshold = Math.min(distanceFromPrev, distanceFromNext); var isTooCloseForRadius = threshold / 2 < radius; var radiusForPoint = isTooCloseForRadius ? threshold / 2 : radius; var before = moveTo(prev, point, radiusForPoint); var after = moveTo(next, point, radiusForPoint); return [path, "L " + before.x + "," + before.y, "S " + point.x + "," + point.y + " " + after.x + "," + after.y].join('\n'); }, "M " + firstPoint.x + "," + firstPoint.y); }; // Taken from Khan Academy's Aphrodite // https://github.com/Khan/aphrodite/blob/master/src/inject.js var styleTag; var injectStyleTag = function injectStyleTag(cssContents) { if (styleTag == null) { // Try to find a style tag with the `data-infinity-trend` attribute first. styleTag = document.querySelector('style[data-infinity-trend]'); // If that doesn't work, generate a new style tag. if (styleTag == null) { // Taken from // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript var head = document.head || document.getElementsByTagName('head')[0]; styleTag = document.createElement('style'); styleTag.type = 'text/css'; styleTag.setAttribute('data-infinity-trend', ''); head.appendChild(styleTag); } } styleTag.appendChild(document.createTextNode(cssContents)); }; // CONCATENATED MODULE: ./src/helpers/misc.helpers.js // eslint-disable-next-line no-restricted-properties var generateId = function generateId() { return Math.round(Math.random() * Math.pow(10, 16)); }; // CONCATENATED MODULE: ./src/components/Trend/Trend.helpers.js var Trend_helpers_normalizeDataset = function normalizeDataset(data, _ref) { var minX = _ref.minX, maxX = _ref.maxX, minY = _ref.minY, maxY = _ref.maxY; // For the X axis, we want to normalize it based on its index in the array. // For the Y axis, we want to normalize it based on the element's value. // // X axis is easy: just evenly-space each item in the array. // For the Y axis, we first need to find the min and max of our array, // and then normalize those values between 0 and 1. var boundariesX = { min: 0, max: data.length - 1 }; var boundariesY = { min: Math.min.apply(Math, data), max: Math.max.apply(Math, data) }; var normalizedData = data.map(function (point, index) { return { x: normalize({ value: index, min: boundariesX.min, max: boundariesX.max, scaleMin: minX, scaleMax: maxX }), y: normalize({ value: point, min: boundariesY.min, max: boundariesY.max, scaleMin: minY, scaleMax: maxY }) }; }); // According to the SVG spec, paths with a height/width of `0` can't have // linear gradients applied. This means that our lines are invisible when // the dataset is flat (eg. [0, 0, 0, 0]). // // The hacky solution is to apply a very slight offset to the first point of // the dataset. As ugly as it is, it's the best solution we can find (there // are ways within the SVG spec of changing it, but not without causing // breaking changes). if (boundariesY.min === boundariesY.max) { // eslint-disable-next-line no-param-reassign normalizedData[0].y += 0.0001; } return normalizedData; }; var generateAutoDrawCss = function generateAutoDrawCss(_ref2) { var id = _ref2.id, lineLength = _ref2.lineLength, duration = _ref2.duration, easing = _ref2.easing; // We do the animation using the dash array/offset trick // https://css-tricks.com/svg-line-animation-works/ var autodrawKeyframeAnimation = "\n @keyframes infinity-trend-autodraw-" + id + " {\n 0% {\n stroke-dasharray: " + lineLength + ";\n stroke-dashoffset: " + lineLength + "\n }\n 100% {\n stroke-dasharray: " + lineLength + ";\n stroke-dashoffset: 0;\n }\n 100% {\n stroke-dashoffset: '';\n stroke-dasharray: '';\n }\n }\n "; // One unfortunate side-effect of the auto-draw is that the line is // actually 1 big dash, the same length as the line itself. If the // line length changes (eg. radius change, new data), that dash won't // be the same length anymore. We can fix that by removing those // properties once the auto-draw is completed. var cleanupKeyframeAnimation = "\n @keyframes infinity-trend-autodraw-cleanup-" + id + " {\n to {\n stroke-dasharray: '';\n stroke-dashoffset: '';\n }\n }\n "; return "\n " + autodrawKeyframeAnimation + "\n\n " + cleanupKeyframeAnimation + "\n\n #infinity-trend-" + id + " {\n animation:\n infinity-trend-autodraw-" + id + " " + duration + "ms " + easing + ",\n infinity-trend-autodraw-cleanup-" + id + " 1ms " + duration + "ms\n ;\n }\n "; }; // CONCATENATED MODULE: ./src/components/Trend/Trend.js function Trend_extends() { return Trend_extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, Trend_extends.apply(null, arguments); } function _inheritsLoose(t, o) { t.prototype = Object.create(o.prototype), t.prototype.constructor = t, _setPrototypeOf(t, o); } function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); } var propTypes = { data: prop_types_default.a.arrayOf(prop_types_default.a.oneOfType([prop_types_default.a.number, prop_types_default.a.shape({ value: prop_types_default.a.number })]).isRequired).isRequired, smooth: prop_types_default.a.bool, autoDraw: prop_types_default.a.bool, autoDrawDuration: prop_types_default.a.number, autoDrawEasing: prop_types_default.a.string, width: prop_types_default.a.number, height: prop_types_default.a.number, padding: prop_types_default.a.number, radius: prop_types_default.a.number, gradient: prop_types_default.a.arrayOf(prop_types_default.a.string) }; var defaultProps = { radius: 10, stroke: 'black', padding: 8, strokeWidth: 1, autoDraw: false, autoDrawDuration: 2000, autoDrawEasing: 'ease' }; var Trend_Trend = /*#__PURE__*/function (_Component) { function Trend(props) { var _this; _this = _Component.call(this, props) || this; // Generate a random ID. This is important for distinguishing between // Trend components on a page, so that they can have different keyframe // animations. _this.trendId = generateId(); _this.gradientId = "infinity-trend-vertical-gradient-" + _this.trendId; return _this; } _inheritsLoose(Trend, _Component); var _proto = Trend.prototype; _proto.componentDidMount = function componentDidMount() { var _this$props = this.props, autoDraw = _this$props.autoDraw, autoDrawDuration = _this$props.autoDrawDuration, autoDrawEasing = _this$props.autoDrawEasing; if (autoDraw) { this.lineLength = this.path.getTotalLength(); var css = generateAutoDrawCss({ id: this.trendId, lineLength: this.lineLength, duration: autoDrawDuration, easing: autoDrawEasing }); injectStyleTag(css); } }; _proto.getDelegatedProps = function getDelegatedProps() { return omit(this.props, Object.keys(propTypes)); }; _proto.renderGradientDefinition = function renderGradientDefinition() { var gradient = this.props.gradient; return /*#__PURE__*/external_root_React_commonjs2_react_commonjs_react_amd_react_default.a.createElement("defs", null, /*#__PURE__*/external_root_React_commonjs2_react_commonjs_react_amd_react_default.a.createElement("linearGradient", { id: this.gradientId, x1: "0%", y1: "0%", x2: "0%", y2: "100%" }, gradient.slice().reverse().map(function (c, index) { return /*#__PURE__*/external_root_React_commonjs2_react_commonjs_react_amd_react_default.a.createElement("stop", { key: index, offset: normalize({ value: index, min: 0, // If we only supply a single colour, it will try to normalize // between 0 and 0, which will create NaN. By making the `max` // at least 1, we ensure single-color "gradients" work. max: gradient.length - 1 || 1 }), stopColor: c }); }))); }; _proto.render = function render() { var _this2 = this; var _this$props2 = this.props, data = _this$props2.data, smooth = _this$props2.smooth, width = _this$props2.width, height = _this$props2.height, padding = _this$props2.padding, radius = _this$props2.radius, gradient = _this$props2.gradient; // We need at least 2 points to draw a graph. if (!data || data.length < 2) { return null; } // `data` can either be an array of numbers: // [1, 2, 3] // or, an array of objects containing a value: // [ { value: 1 }, { value: 2 }, { value: 3 }] // // For now, we're just going to convert the second form to the first. // Later on, if/when we support tooltips, we may adjust. var plainValues = data.map(function (point) { return typeof point === 'number' ? point : point.value; }); // Our viewbox needs to be in absolute units, so we'll default to 300x75 // Our SVG can be a %, though; this is what makes it scalable. // By defaulting to percentages, the SVG will grow to fill its parent // container, preserving a 1/4 aspect ratio. var viewBoxWidth = width || 300; var viewBoxHeight = height || 75; var svgWidth = width || '100%'; var svgHeight = height || '25%'; var normalizedValues = Trend_helpers_normalizeDataset(plainValues, { minX: padding, maxX: viewBoxWidth - padding, // NOTE: Because SVGs are indexed from the top left, but most data is // indexed from the bottom left, we're inverting the Y min/max. minY: viewBoxHeight - padding, maxY: padding }); var path = smooth ? DOM_helpers_buildSmoothPath(normalizedValues, { radius: radius }) : buildLinearPath(normalizedValues); return /*#__PURE__*/external_root_React_commonjs2_react_commonjs_react_amd_react_default.a.createElement("svg", Trend_extends({ width: svgWidth, height: svgHeight, viewBox: "0 0 " + viewBoxWidth + " " + viewBoxHeight }, this.getDelegatedProps()), gradient && this.renderGradientDefinition(), /*#__PURE__*/external_root_React_commonjs2_react_commonjs_react_amd_react_default.a.createElement("path", { ref: function ref(elem) { _this2.path = elem; }, id: "infinity-trend-" + this.trendId, d: path, fill: "none", stroke: gradient ? "url(#" + this.gradientId + ")" : undefined })); }; return Trend; }(external_root_React_commonjs2_react_commonjs_react_amd_react_["Component"]); Trend_Trend.propTypes = propTypes; Trend_Trend.defaultProps = defaultProps; /* harmony default export */ var components_Trend_Trend = (Trend_Trend); // CONCATENATED MODULE: ./src/components/Trend/index.js // CONCATENATED MODULE: ./src/index.js /***/ }) /******/ ])["default"]; });