infinity-trend
Version:
A React component designed for effortlessly creating smooth, responsive trend and line graphs.
651 lines (593 loc) • 24.6 kB
JavaScript
/*!
* 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__) {
;
/**
* 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__) {
;
/**
* 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__) {
;
// 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"];
});