UNPKG

chartist

Version:

Simple, responsive charts

1,242 lines (1,227 loc) 163 kB
/** * This object contains all namespaces used within Chartist. */ const namespaces = { svg: "http://www.w3.org/2000/svg", xmlns: "http://www.w3.org/2000/xmlns/", xhtml: "http://www.w3.org/1999/xhtml", xlink: "http://www.w3.org/1999/xlink", ct: "http://gionkunz.github.com/chartist-js/ct" }; /** * Precision level used internally in Chartist for rounding. If you require more decimal places you can increase this number. */ const precision = 8; /** * A map with characters to escape for strings to be safely used as attribute values. */ const escapingMap = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" }; /** * Converts a number to a string with a unit. If a string is passed then this will be returned unmodified. * @return Returns the passed number value with unit. */ function ensureUnit(value, unit) { if (typeof value === "number") { return value + unit; } return value; } /** * Converts a number or string to a quantity object. * @return Returns an object containing the value as number and the unit as string. */ function quantity(input) { if (typeof input === "string") { const match = /^(\d+)\s*(.*)$/g.exec(input); return { value: match ? +match[1] : 0, unit: (match === null || match === void 0 ? void 0 : match[2]) || undefined }; } return { value: Number(input) }; } /** * Generates a-z from a number 0 to 26 * @param n A number from 0 to 26 that will result in a letter a-z * @return A character from a-z based on the input number n */ function alphaNumerate(n) { // Limit to a-z return String.fromCharCode(97 + n % 26); } const EPSILON = 2.221e-16; /** * Calculate the order of magnitude for the chart scale * @param value The value Range of the chart * @return The order of magnitude */ function orderOfMagnitude(value) { return Math.floor(Math.log(Math.abs(value)) / Math.LN10); } /** * Project a data length into screen coordinates (pixels) * @param axisLength The svg element for the chart * @param length Single data value from a series array * @param bounds All the values to set the bounds of the chart * @return The projected data length in pixels */ function projectLength(axisLength, length, bounds) { return length / bounds.range * axisLength; } /** * This helper function can be used to round values with certain precision level after decimal. This is used to prevent rounding errors near float point precision limit. * @param value The value that should be rounded with precision * @param [digits] The number of digits after decimal used to do the rounding * @returns Rounded value */ function roundWithPrecision(value, digits) { const precision$1 = Math.pow(10, digits || precision); return Math.round(value * precision$1) / precision$1; } /** * Pollard Rho Algorithm to find smallest factor of an integer value. There are more efficient algorithms for factorization, but this one is quite efficient and not so complex. * @param num An integer number where the smallest factor should be searched for * @returns The smallest integer factor of the parameter num. */ function rho(num) { if (num === 1) { return num; } function gcd(p, q) { if (p % q === 0) { return q; } else { return gcd(q, p % q); } } function f(x) { return x * x + 1; } let x1 = 2; let x2 = 2; let divisor; if (num % 2 === 0) { return 2; } do { x1 = f(x1) % num; x2 = f(f(x2)) % num; divisor = gcd(Math.abs(x1 - x2), num); }while (divisor === 1); return divisor; } /** * Calculate cartesian coordinates of polar coordinates * @param centerX X-axis coordinates of center point of circle segment * @param centerY X-axis coordinates of center point of circle segment * @param radius Radius of circle segment * @param angleInDegrees Angle of circle segment in degrees * @return Coordinates of point on circumference */ function polarToCartesian(centerX, centerY, radius, angleInDegrees) { const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0; return { x: centerX + radius * Math.cos(angleInRadians), y: centerY + radius * Math.sin(angleInRadians) }; } /** * Calculate and retrieve all the bounds for the chart and return them in one array * @param axisLength The length of the Axis used for * @param highLow An object containing a high and low property indicating the value range of the chart. * @param scaleMinSpace The minimum projected length a step should result in * @param onlyInteger * @return All the values to set the bounds of the chart */ function getBounds(axisLength, highLow, scaleMinSpace) { let onlyInteger = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : false; const bounds = { high: highLow.high, low: highLow.low, valueRange: 0, oom: 0, step: 0, min: 0, max: 0, range: 0, numberOfSteps: 0, values: [] }; bounds.valueRange = bounds.high - bounds.low; bounds.oom = orderOfMagnitude(bounds.valueRange); bounds.step = Math.pow(10, bounds.oom); bounds.min = Math.floor(bounds.low / bounds.step) * bounds.step; bounds.max = Math.ceil(bounds.high / bounds.step) * bounds.step; bounds.range = bounds.max - bounds.min; bounds.numberOfSteps = Math.round(bounds.range / bounds.step); // Optimize scale step by checking if subdivision is possible based on horizontalGridMinSpace // If we are already below the scaleMinSpace value we will scale up const length = projectLength(axisLength, bounds.step, bounds); const scaleUp = length < scaleMinSpace; const smallestFactor = onlyInteger ? rho(bounds.range) : 0; // First check if we should only use integer steps and if step 1 is still larger than scaleMinSpace so we can use 1 if (onlyInteger && projectLength(axisLength, 1, bounds) >= scaleMinSpace) { bounds.step = 1; } else if (onlyInteger && smallestFactor < bounds.step && projectLength(axisLength, smallestFactor, bounds) >= scaleMinSpace) { // If step 1 was too small, we can try the smallest factor of range // If the smallest factor is smaller than the current bounds.step and the projected length of smallest factor // is larger than the scaleMinSpace we should go for it. bounds.step = smallestFactor; } else { // Trying to divide or multiply by 2 and find the best step value let optimizationCounter = 0; for(;;){ if (scaleUp && projectLength(axisLength, bounds.step, bounds) <= scaleMinSpace) { bounds.step *= 2; } else if (!scaleUp && projectLength(axisLength, bounds.step / 2, bounds) >= scaleMinSpace) { bounds.step /= 2; if (onlyInteger && bounds.step % 1 !== 0) { bounds.step *= 2; break; } } else { break; } if (optimizationCounter++ > 1000) { throw new Error("Exceeded maximum number of iterations while optimizing scale step!"); } } } bounds.step = Math.max(bounds.step, EPSILON); function safeIncrement(value, increment) { // If increment is too small use *= (1+EPSILON) as a simple nextafter if (value === (value += increment)) { value *= 1 + (increment > 0 ? EPSILON : -EPSILON); } return value; } // Narrow min and max based on new step let newMin = bounds.min; let newMax = bounds.max; while(newMin + bounds.step <= bounds.low){ newMin = safeIncrement(newMin, bounds.step); } while(newMax - bounds.step >= bounds.high){ newMax = safeIncrement(newMax, -bounds.step); } bounds.min = newMin; bounds.max = newMax; bounds.range = bounds.max - bounds.min; const values = []; for(let i = bounds.min; i <= bounds.max; i = safeIncrement(i, bounds.step)){ const value = roundWithPrecision(i); if (value !== values[values.length - 1]) { values.push(value); } } bounds.values = values; return bounds; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function extend() { let target = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; for(var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++){ sources[_key - 1] = arguments[_key]; } for(let i = 0; i < sources.length; i++){ const source = sources[i]; const targetProto = Object.getPrototypeOf(target); for(const prop in source){ if (targetProto !== null && prop in targetProto) { continue; // prevent prototype pollution } const sourceProp = source[prop]; if (typeof sourceProp === "object" && sourceProp !== null && !(sourceProp instanceof Array)) { target[prop] = extend(target[prop], sourceProp); } else { target[prop] = sourceProp; } } } return target; } /** * Helps to simplify functional style code * @param n This exact value will be returned by the noop function * @return The same value that was provided to the n parameter */ const noop = (n)=>n ; function times(length, filler) { return Array.from({ length }, filler ? (_, i)=>filler(i) : ()=>void 0 ); } /** * Sum helper to be used in reduce functions */ const sum = (previous, current)=>previous + (current ? current : 0) ; /** * Map for multi dimensional arrays where their nested arrays will be mapped in serial. The output array will have the length of the largest nested array. The callback function is called with variable arguments where each argument is the nested array value (or undefined if there are no more values). * * For example: * @example * ```ts * const data = [[1, 2], [3], []]; * serialMap(data, cb); * * // where cb will be called 2 times * // 1. call arguments: (1, 3, undefined) * // 2. call arguments: (2, undefined, undefined) * ``` */ const serialMap = (array, callback)=>times(Math.max(...array.map((element)=>element.length )), (index)=>callback(...array.map((element)=>element[index] )) ) ; function safeHasProperty(target, property) { return target !== null && typeof target === "object" && Reflect.has(target, property); } function isNumeric(value) { return value !== null && isFinite(value); } /** * Returns true on all falsey values except the numeric value 0. */ function isFalseyButZero(value) { return !value && value !== 0; } function getNumberOrUndefined(value) { return isNumeric(value) ? Number(value) : undefined; } /** * Checks if value is array of arrays or not. */ function isArrayOfArrays(data) { if (!Array.isArray(data)) { return false; } return data.every(Array.isArray); } /** * Loop over array. */ function each(list, callback) { let reverse = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false; let index = 0; list[reverse ? "reduceRight" : "reduce"]((_, item, itemIndex)=>callback(item, index++, itemIndex) , void 0); } /** * Get meta data of a specific value in a series. */ function getMetaData(seriesData, index) { const value = Array.isArray(seriesData) ? seriesData[index] : safeHasProperty(seriesData, "data") ? seriesData.data[index] : null; return safeHasProperty(value, "meta") ? value.meta : undefined; } function isDataHoleValue(value) { return value === null || value === undefined || typeof value === "number" && isNaN(value); } /** * Checks if value is array of series objects. */ function isArrayOfSeries(value) { return Array.isArray(value) && value.every((_)=>Array.isArray(_) || safeHasProperty(_, "data") ); } /** * Checks if provided value object is multi value (contains x or y properties) */ function isMultiValue(value) { return typeof value === "object" && value !== null && (Reflect.has(value, "x") || Reflect.has(value, "y")); } /** * Gets a value from a dimension `value.x` or `value.y` while returning value directly if it's a valid numeric value. If the value is not numeric and it's falsey this function will return `defaultValue`. */ function getMultiValue(value) { let dimension = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : "y"; if (isMultiValue(value) && safeHasProperty(value, dimension)) { return getNumberOrUndefined(value[dimension]); } else { return getNumberOrUndefined(value); } } /** * Get highest and lowest value of data array. This Array contains the data that will be visualized in the chart. * @param data The array that contains the data to be visualized in the chart * @param options The Object that contains the chart options * @param dimension Axis dimension 'x' or 'y' used to access the correct value and high / low configuration * @return An object that contains the highest and lowest value that will be visualized on the chart. */ function getHighLow(data, options, dimension) { // TODO: Remove workaround for deprecated global high / low config. Axis high / low configuration is preferred options = { ...options, ...dimension ? dimension === "x" ? options.axisX : options.axisY : {} }; const highLow = { high: options.high === undefined ? -Number.MAX_VALUE : +options.high, low: options.low === undefined ? Number.MAX_VALUE : +options.low }; const findHigh = options.high === undefined; const findLow = options.low === undefined; // Function to recursively walk through arrays and find highest and lowest number function recursiveHighLow(sourceData) { if (isDataHoleValue(sourceData)) { return; } else if (Array.isArray(sourceData)) { for(let i = 0; i < sourceData.length; i++){ recursiveHighLow(sourceData[i]); } } else { const value = Number(dimension && safeHasProperty(sourceData, dimension) ? sourceData[dimension] : sourceData); if (findHigh && value > highLow.high) { highLow.high = value; } if (findLow && value < highLow.low) { highLow.low = value; } } } // Start to find highest and lowest number recursively if (findHigh || findLow) { recursiveHighLow(data); } // Overrides of high / low based on reference value, it will make sure that the invisible reference value is // used to generate the chart. This is useful when the chart always needs to contain the position of the // invisible reference value in the view i.e. for bipolar scales. if (options.referenceValue || options.referenceValue === 0) { highLow.high = Math.max(options.referenceValue, highLow.high); highLow.low = Math.min(options.referenceValue, highLow.low); } // If high and low are the same because of misconfiguration or flat data (only the same value) we need // to set the high or low to 0 depending on the polarity if (highLow.high <= highLow.low) { // If both values are 0 we set high to 1 if (highLow.low === 0) { highLow.high = 1; } else if (highLow.low < 0) { // If we have the same negative value for the bounds we set bounds.high to 0 highLow.high = 0; } else if (highLow.high > 0) { // If we have the same positive value for the bounds we set bounds.low to 0 highLow.low = 0; } else { // If data array was empty, values are Number.MAX_VALUE and -Number.MAX_VALUE. Set bounds to prevent errors highLow.high = 1; highLow.low = 0; } } return highLow; } function normalizeData(data) { let reverse = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false, multi = arguments.length > 2 ? arguments[2] : void 0, distributed = arguments.length > 3 ? arguments[3] : void 0; let labelCount; const normalized = { labels: (data.labels || []).slice(), series: normalizeSeries(data.series, multi, distributed) }; const inputLabelCount = normalized.labels.length; // If all elements of the normalized data array are arrays we're dealing with // multi series data and we need to find the largest series if they are un-even if (isArrayOfArrays(normalized.series)) { // Getting the series with the the most elements labelCount = Math.max(inputLabelCount, ...normalized.series.map((series)=>series.length )); normalized.series.forEach((series)=>{ series.push(...times(Math.max(0, labelCount - series.length))); }); } else { // We're dealing with Pie data so we just take the normalized array length labelCount = normalized.series.length; } // Padding the labels to labelCount with empty strings normalized.labels.push(...times(Math.max(0, labelCount - inputLabelCount), ()=>"" )); if (reverse) { reverseData(normalized); } return normalized; } /** * Reverses the series, labels and series data arrays. */ function reverseData(data) { var ref; (ref = data.labels) === null || ref === void 0 ? void 0 : ref.reverse(); data.series.reverse(); for (const series of data.series){ if (safeHasProperty(series, "data")) { series.data.reverse(); } else if (Array.isArray(series)) { series.reverse(); } } } function normalizeMulti(value, multi) { // We need to prepare multi value output (x and y data) let x; let y; // Single series value arrays are assumed to specify the Y-Axis value // For example: [1, 2] => [{x: undefined, y: 1}, {x: undefined, y: 2}] // If multi is a string then it's assumed that it specified which dimension should be filled as default if (typeof value !== "object") { const num = getNumberOrUndefined(value); if (multi === "x") { x = num; } else { y = num; } } else { if (safeHasProperty(value, "x")) { x = getNumberOrUndefined(value.x); } if (safeHasProperty(value, "y")) { y = getNumberOrUndefined(value.y); } } if (x === undefined && y === undefined) { return undefined; } return { x, y }; } function normalizePrimitive(value, multi) { if (isDataHoleValue(value)) { // We're dealing with a hole in the data and therefore need to return undefined // We're also returning undefined for multi value output return undefined; } if (multi) { return normalizeMulti(value, multi); } return getNumberOrUndefined(value); } function normalizeSingleSeries(series, multi) { if (!Array.isArray(series)) { // We are dealing with series object notation so we need to recurse on data property return normalizeSingleSeries(series.data, multi); } return series.map((value)=>{ if (safeHasProperty(value, "value")) { // We are dealing with value object notation so we need to recurse on value property return normalizePrimitive(value.value, multi); } return normalizePrimitive(value, multi); }); } function normalizeSeries(series, multi, distributed) { if (isArrayOfSeries(series)) { return series.map((_)=>normalizeSingleSeries(_, multi) ); } const normalizedSeries = normalizeSingleSeries(series, multi); if (distributed) { return normalizedSeries.map((value)=>[ value ] ); } return normalizedSeries; } /** * Splits a list of coordinates and associated values into segments. Each returned segment contains a pathCoordinates * valueData property describing the segment. * * With the default options, segments consist of contiguous sets of points that do not have an undefined value. Any * points with undefined values are discarded. * * **Options** * The following options are used to determine how segments are formed * ```javascript * var options = { * // If fillHoles is true, undefined values are simply discarded without creating a new segment. Assuming other options are default, this returns single segment. * fillHoles: false, * // If increasingX is true, the coordinates in all segments have strictly increasing x-values. * increasingX: false * }; * ``` * * @param pathCoordinates List of point coordinates to be split in the form [x1, y1, x2, y2 ... xn, yn] * @param valueData List of associated point values in the form [v1, v2 .. vn] * @param options Options set by user * @return List of segments, each containing a pathCoordinates and valueData property. */ function splitIntoSegments(pathCoordinates, valueData, options) { const finalOptions = { increasingX: false, fillHoles: false, ...options }; const segments = []; let hole = true; for(let i = 0; i < pathCoordinates.length; i += 2){ // If this value is a "hole" we set the hole flag if (getMultiValue(valueData[i / 2].value) === undefined) { // if(valueData[i / 2].value === undefined) { if (!finalOptions.fillHoles) { hole = true; } } else { if (finalOptions.increasingX && i >= 2 && pathCoordinates[i] <= pathCoordinates[i - 2]) { // X is not increasing, so we need to make sure we start a new segment hole = true; } // If it's a valid value we need to check if we're coming out of a hole and create a new empty segment if (hole) { segments.push({ pathCoordinates: [], valueData: [] }); // As we have a valid value now, we are not in a "hole" anymore hole = false; } // Add to the segment pathCoordinates and valueData segments[segments.length - 1].pathCoordinates.push(pathCoordinates[i], pathCoordinates[i + 1]); segments[segments.length - 1].valueData.push(valueData[i / 2]); } } return segments; } function serialize(data) { let serialized = ""; if (data === null || data === undefined) { return data; } else if (typeof data === "number") { serialized = "" + data; } else if (typeof data === "object") { serialized = JSON.stringify({ data: data }); } else { serialized = String(data); } return Object.keys(escapingMap).reduce((result, key)=>result.replaceAll(key, escapingMap[key]) , serialized); } function deserialize(data) { if (typeof data !== "string") { return data; } if (data === "NaN") { return NaN; } data = Object.keys(escapingMap).reduce((result, key)=>result.replaceAll(escapingMap[key], key) , data); // eslint-disable-next-line @typescript-eslint/no-explicit-any let parsedData = data; if (typeof data === "string") { try { parsedData = JSON.parse(data); parsedData = parsedData.data !== undefined ? parsedData.data : parsedData; } catch (e) { /* Ingore */ } } return parsedData; } /** * This helper class is to wrap multiple `Svg` elements into a list where you can call the `Svg` functions on all elements in the list with one call. This is helpful when you'd like to perform calls with `Svg` on multiple elements. * An instance of this class is also returned by `Svg.querySelectorAll`. */ class SvgList { call(method, args) { this.svgElements.forEach((element)=>Reflect.apply(element[method], element, args) ); return this; } attr() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("attr", args); } elem() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("elem", args); } root() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("root", args); } getNode() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("getNode", args); } foreignObject() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("foreignObject", args); } text() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("text", args); } empty() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("empty", args); } remove() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("remove", args); } addClass() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("addClass", args); } removeClass() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("removeClass", args); } removeAllClasses() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("removeAllClasses", args); } animate() { for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){ args[_key] = arguments[_key]; } return this.call("animate", args); } /** * @param nodeList An Array of SVG DOM nodes or a SVG DOM NodeList (as returned by document.querySelectorAll) */ constructor(nodeList){ this.svgElements = []; for(let i = 0; i < nodeList.length; i++){ this.svgElements.push(new Svg(nodeList[i])); } } } /** * This Object contains some standard easing cubic bezier curves. * Then can be used with their name in the `Svg.animate`. * You can also extend the list and use your own name in the `animate` function. * Click the show code button to see the available bezier functions. */ const easings = { easeInSine: [ 0.47, 0, 0.745, 0.715 ], easeOutSine: [ 0.39, 0.575, 0.565, 1 ], easeInOutSine: [ 0.445, 0.05, 0.55, 0.95 ], easeInQuad: [ 0.55, 0.085, 0.68, 0.53 ], easeOutQuad: [ 0.25, 0.46, 0.45, 0.94 ], easeInOutQuad: [ 0.455, 0.03, 0.515, 0.955 ], easeInCubic: [ 0.55, 0.055, 0.675, 0.19 ], easeOutCubic: [ 0.215, 0.61, 0.355, 1 ], easeInOutCubic: [ 0.645, 0.045, 0.355, 1 ], easeInQuart: [ 0.895, 0.03, 0.685, 0.22 ], easeOutQuart: [ 0.165, 0.84, 0.44, 1 ], easeInOutQuart: [ 0.77, 0, 0.175, 1 ], easeInQuint: [ 0.755, 0.05, 0.855, 0.06 ], easeOutQuint: [ 0.23, 1, 0.32, 1 ], easeInOutQuint: [ 0.86, 0, 0.07, 1 ], easeInExpo: [ 0.95, 0.05, 0.795, 0.035 ], easeOutExpo: [ 0.19, 1, 0.22, 1 ], easeInOutExpo: [ 1, 0, 0, 1 ], easeInCirc: [ 0.6, 0.04, 0.98, 0.335 ], easeOutCirc: [ 0.075, 0.82, 0.165, 1 ], easeInOutCirc: [ 0.785, 0.135, 0.15, 0.86 ], easeInBack: [ 0.6, -0.28, 0.735, 0.045 ], easeOutBack: [ 0.175, 0.885, 0.32, 1.275 ], easeInOutBack: [ 0.68, -0.55, 0.265, 1.55 ] }; function createAnimation(element, attribute, animationDefinition) { let createGuided = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : false, eventEmitter = arguments.length > 4 ? arguments[4] : void 0; const { easing , ...def } = animationDefinition; const attributeProperties = {}; let animationEasing; let timeout; // Check if an easing is specified in the definition object and delete it from the object as it will not // be part of the animate element attributes. if (easing) { // If already an easing Bézier curve array we take it or we lookup a easing array in the Easing object animationEasing = Array.isArray(easing) ? easing : easings[easing]; } // If numeric dur or begin was provided we assume milli seconds def.begin = ensureUnit(def.begin, "ms"); def.dur = ensureUnit(def.dur, "ms"); if (animationEasing) { def.calcMode = "spline"; def.keySplines = animationEasing.join(" "); def.keyTimes = "0;1"; } // Adding "fill: freeze" if we are in guided mode and set initial attribute values if (createGuided) { def.fill = "freeze"; // Animated property on our element should already be set to the animation from value in guided mode attributeProperties[attribute] = def.from; element.attr(attributeProperties); // In guided mode we also set begin to indefinite so we can trigger the start manually and put the begin // which needs to be in ms aside timeout = quantity(def.begin || 0).value; def.begin = "indefinite"; } const animate = element.elem("animate", { attributeName: attribute, ...def }); if (createGuided) { // If guided we take the value that was put aside in timeout and trigger the animation manually with a timeout setTimeout(()=>{ // If beginElement fails we set the animated attribute to the end position and remove the animate element // This happens if the SMIL ElementTimeControl interface is not supported or any other problems occurred in // the browser. (Currently FF 34 does not support animate elements in foreignObjects) try { // @ts-expect-error Try legacy API. animate._node.beginElement(); } catch (err) { // Set animated attribute to current animated value attributeProperties[attribute] = def.to; element.attr(attributeProperties); // Remove the animate element as it's no longer required animate.remove(); } }, timeout); } const animateNode = animate.getNode(); if (eventEmitter) { animateNode.addEventListener("beginEvent", ()=>eventEmitter.emit("animationBegin", { element: element, animate: animateNode, params: animationDefinition }) ); } animateNode.addEventListener("endEvent", ()=>{ if (eventEmitter) { eventEmitter.emit("animationEnd", { element: element, animate: animateNode, params: animationDefinition }); } if (createGuided) { // Set animated attribute to current animated value attributeProperties[attribute] = def.to; element.attr(attributeProperties); // Remove the animate element as it's no longer required animate.remove(); } }); } /** * Svg creates a new SVG object wrapper with a starting element. You can use the wrapper to fluently create sub-elements and modify them. */ class Svg { attr(attributes, ns) { if (typeof attributes === "string") { if (ns) { return this._node.getAttributeNS(ns, attributes); } else { return this._node.getAttribute(attributes); } } Object.keys(attributes).forEach((key)=>{ // If the attribute value is undefined we can skip this one if (attributes[key] === undefined) { return; } if (key.indexOf(":") !== -1) { const namespacedAttribute = key.split(":"); this._node.setAttributeNS(namespaces[namespacedAttribute[0]], key, String(attributes[key])); } else { this._node.setAttribute(key, String(attributes[key])); } }); return this; } /** * Create a new SVG element whose wrapper object will be selected for further operations. This way you can also create nested groups easily. * @param name The name of the SVG element that should be created as child element of the currently selected element wrapper * @param attributes An object with properties that will be added as attributes to the SVG element that is created. Attributes with undefined values will not be added. * @param className This class or class list will be added to the SVG element * @param insertFirst If this param is set to true in conjunction with a parent element the newly created element will be added as first child element in the parent element * @return Returns a Svg wrapper object that can be used to modify the containing SVG data */ elem(name, attributes, className) { let insertFirst = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : false; return new Svg(name, attributes, className, this, insertFirst); } /** * Returns the parent Chartist.SVG wrapper object * @return Returns a Svg wrapper around the parent node of the current node. If the parent node is not existing or it's not an SVG node then this function will return null. */ parent() { return this._node.parentNode instanceof SVGElement ? new Svg(this._node.parentNode) : null; } /** * This method returns a Svg wrapper around the root SVG element of the current tree. * @return The root SVG element wrapped in a Svg element */ root() { let node = this._node; while(node.nodeName !== "svg"){ if (node.parentElement) { node = node.parentElement; } else { break; } } return new Svg(node); } /** * Find the first child SVG element of the current element that matches a CSS selector. The returned object is a Svg wrapper. * @param selector A CSS selector that is used to query for child SVG elements * @return The SVG wrapper for the element found or null if no element was found */ querySelector(selector) { const foundNode = this._node.querySelector(selector); return foundNode ? new Svg(foundNode) : null; } /** * Find the all child SVG elements of the current element that match a CSS selector. The returned object is a Svg.List wrapper. * @param selector A CSS selector that is used to query for child SVG elements * @return The SVG wrapper list for the element found or null if no element was found */ querySelectorAll(selector) { const foundNodes = this._node.querySelectorAll(selector); return new SvgList(foundNodes); } /** * Returns the underlying SVG node for the current element. */ getNode() { return this._node; } /** * This method creates a foreignObject (see https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject) that allows to embed HTML content into a SVG graphic. With the help of foreignObjects you can enable the usage of regular HTML elements inside of SVG where they are subject for SVG positioning and transformation but the Browser will use the HTML rendering capabilities for the containing DOM. * @param content The DOM Node, or HTML string that will be converted to a DOM Node, that is then placed into and wrapped by the foreignObject * @param attributes An object with properties that will be added as attributes to the foreignObject element that is created. Attributes with undefined values will not be added. * @param className This class or class list will be added to the SVG element * @param insertFirst Specifies if the foreignObject should be inserted as first child * @return New wrapper object that wraps the foreignObject element */ foreignObject(content, attributes, className) { let insertFirst = arguments.length > 3 && arguments[3] !== void 0 ? arguments[3] : false; let contentNode; // If content is string then we convert it to DOM // TODO: Handle case where content is not a string nor a DOM Node if (typeof content === "string") { const container = document.createElement("div"); container.innerHTML = content; contentNode = container.firstChild; } else { contentNode = content; } if (contentNode instanceof Element) { // Adding namespace to content element contentNode.setAttribute("xmlns", namespaces.xmlns); } // Creating the foreignObject without required extension attribute (as described here // http://www.w3.org/TR/SVG/extend.html#ForeignObjectElement) const fnObj = this.elem("foreignObject", attributes, className, insertFirst); // Add content to foreignObjectElement fnObj._node.appendChild(contentNode); return fnObj; } /** * This method adds a new text element to the current Svg wrapper. * @param t The text that should be added to the text element that is created * @return The same wrapper object that was used to add the newly created element */ text(t) { this._node.appendChild(document.createTextNode(t)); return this; } /** * This method will clear all child nodes of the current wrapper object. * @return The same wrapper object that got emptied */ empty() { while(this._node.firstChild){ this._node.removeChild(this._node.firstChild); } return this; } /** * This method will cause the current wrapper to remove itself from its parent wrapper. Use this method if you'd like to get rid of an element in a given DOM structure. * @return The parent wrapper object of the element that got removed */ remove() { var ref; (ref = this._node.parentNode) === null || ref === void 0 ? void 0 : ref.removeChild(this._node); return this.parent(); } /** * This method will replace the element with a new element that can be created outside of the current DOM. * @param newElement The new Svg object that will be used to replace the current wrapper object * @return The wrapper of the new element */ replace(newElement) { var ref; (ref = this._node.parentNode) === null || ref === void 0 ? void 0 : ref.replaceChild(newElement._node, this._node); return newElement; } /** * This method will append an element to the current element as a child. * @param element The Svg element that should be added as a child * @param insertFirst Specifies if the element should be inserted as first child * @return The wrapper of the appended object */ append(element) { let insertFirst = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : false; if (insertFirst && this._node.firstChild) { this._node.insertBefore(element._node, this._node.firstChild); } else { this._node.appendChild(element._node); } return this; } /** * Returns an array of class names that are attached to the current wrapper element. This method can not be chained further. * @return A list of classes or an empty array if there are no classes on the current element */ classes() { const classNames = this._node.getAttribute("class"); return classNames ? classNames.trim().split(/\s+/) : []; } /** * Adds one or a space separated list of classes to the current element and ensures the classes are only existing once. * @param names A white space separated list of class names * @return The wrapper of the current element */ addClass(names) { this._node.setAttribute("class", this.classes().concat(names.trim().split(/\s+/)).filter(function(elem, pos, self) { return self.indexOf(elem) === pos; }).join(" ")); return this; } /** * Removes one or a space separated list of classes from the current element. * @param names A white space separated list of class names * @return The wrapper of the current element */ removeClass(names) { const removedClasses = names.trim().split(/\s+/); this._node.setAttribute("class", this.classes().filter((name)=>removedClasses.indexOf(name) === -1 ).join(" ")); return this; } /** * Removes all classes from the current element. * @return The wrapper of the current element */ removeAllClasses() { this._node.setAttribute("class", ""); return this; } /** * Get element height using `clientHeight` * @return The elements height in pixels */ height() { return this._node.clientHeight; } /** * Get element width using `clientWidth` * @return The elements width in pixels */ width() { return this._node.clientWidth; } /** * The animate function lets you animate the current element with SMIL animations. You can add animations for multiple attributes at the same time by using an animation definition object. This object should contain SMIL animation attributes. Please refer to http://www.w3.org/TR/SVG/animate.html for a detailed specification about the available animation attributes. Additionally an easing property can be passed in the animation definition object. This can be a string with a name of an easing function in `Svg.Easing` or an array with four numbers specifying a cubic Bézier curve. * **An animations object could look like this:** * ```javascript * element.animate({ * opacity: { * dur: 1000, * from: 0, * to: 1 * }, * x1: { * dur: '1000ms', * from: 100, * to: 200, * easing: 'easeOutQuart' * }, * y1: { * dur: '2s', * from: 0, * to: 100 * } * }); * ``` * **Automatic unit conversion** * For the `dur` and the `begin` animate attribute you can also omit a unit by passing a number. The number will automatically be converted to milli seconds. * **Guided mode** * The default behavior of SMIL animations with offset using the `begin` attribute is that the attribute will keep it's original value until the animation starts. Mostly this behavior is not desired as you'd like to have your element attributes already initialized with the animation `from` value even before the animation starts. Also if you don't specify `fill="freeze"` on an animate element or if you delete the animation after it's done (which is done in guided mode) the attribute will switch back to the initial value. This behavior is also not desired when performing simple one-time animations. For one-time animations you'd want to trigger animations immediately instead of relative to the document begin time. That's why in guided mode Svg will also use the `begin` property to schedule a timeout and manually start the animation after the timeout. If you're using multiple SMIL definition objects for an attribute (in an array), guided mode will be disabled for this attribute, even if you explicitly enabled it. * If guided mode is enabled the following behavior is added: * - Before the animation starts (even when delayed with `begin`) the animated attribute will be set already to the `from` value of the animation * - `begin` is explicitly set to `indefinite` so it can be started manually without relying on document begin time (creation) * - The animate element will be forced to use `fill="freeze"` * - The animation will be triggered with `beginElement()` in a timeout where `begin` of the definition object is interpreted in milli seconds. If no `begin` was specified the timeout is triggered immediately. * - After the animation the element attribute value will be set to the `to` value of the animation * - The animate element is deleted from the DOM * @param animations An animations object where the property keys are the attributes you'd like to animate. The properties should be objects again that contain the SMIL animation attributes (usually begin, dur, from, and to). The property begin and dur is auto converted (see Automatic unit conversion). You can also schedule multiple animations for the same attribute by passing an Array of SMIL definition objects. Attributes that contain an array of SMIL definition objects will not be executed in guided mode. * @param guided Specify if guided mode should be activated for this animation (see Guided mode). If not otherwise specified, guided mode will be activated. * @param eventEmitter If specified, this event emitter will be notified when an animation starts or ends. * @return The current element where the animation was added */ animate(animations) { let guided = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : true, eventEmitter = arguments.length > 2 ? arguments[2] : void 0; Object.keys(animations).forEach((attribute)=>{ const attributeAnimation = animations[attribute]; // If current attribute is an array of definition objects we create an animate for each and disable guided mode if (Array.isArray(attributeAnimation)) { attributeAnimation.forEach((animationDefinition)=>createAnimation(this, attribute, animationDefinition, false, eventEmitter) ); } else { createAnimation(this, attribute, attributeAnimation, guided, eventEmitter); } }); return this; } /** * @param name The name of the SVG element to create or an SVG dom element which should be wrapped into Svg * @param attributes An object with properties that will be added as attributes to the SVG element that is created. Attributes with undefined values will not be added. * @param className This class or class list will be added to the SVG element * @param parent The parent SVG wrapper object where this newly created wrapper and it's element will be attached to as child * @param insertFirst If this param is set to true in conjunction with a parent element the newly created element will be added as first child element in the parent element */ constructor(name, attributes, className, parent, insertFirst = false){ // If Svg is getting called with an SVG element we just return the wrapper if (name instanceof Element) { this._node = name; } else { this._node = document.createElementNS(namespaces.svg, name); // If this is an SVG element created then custom namespace if (name === "svg") { this.attr({ "xmlns:ct": namespaces.ct }); } } if (attributes) { this.attr(attributes); } if (className) { this.addClass(className); } if (parent) { if (insertFirst && parent._node.firstChild) { parent._node.insertBefore(this._node, parent._node.firstChild); } else { parent._node.appendChild(this._node); } } } } /** * @todo Only there for chartist <1 compatibility. Remove after deprecation warining. * @deprecated Use the animation module export `easings` directly. */ Svg.Easing = easings; /** * Create or reinitialize the SVG element for the chart * @param container The containing DOM Node object that will be used to plant the SVG element * @param width Set the width of the SVG element. Default is 100% * @param height Set the height of the SVG element. Default is 100% * @param className Specify a class to be added to the SVG element * @return The created/reinitialized SVG element */ function createSvg(container) { let width = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : "100%", height = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : "100%", className = arguments.length > 3 ? arguments[3] : void 0; if (!container) { throw new Error("Container ele