billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
222 lines (221 loc) • 7.64 kB
JavaScript
/*!
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*
* billboard.js, JavaScript chart library
* https://naver.github.io/billboard.js/
*
* @version 4.1.1
*/
import { isNumber } from "../../../module/util/type-checks.js";
//#region src/ChartInternal/shape/core/barRadius.ts
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
const RE_PATH_NUMBER = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;
const RE_PATH_TOKEN = /([aAcChHlLmMqQsStTvVzZ])|([-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?)/g;
/**
* Collect the ordinal indexes of numeric tokens that are SVG arc-command flags.
* Arc parameters repeat in groups of 7: rx ry x-rotation large-arc sweep x y.
* Positions 2(x-rotation), 3(large-arc) and 4(sweep) must stay integers.
* @param {string} path Path string to scan
* @returns {Set} Ordinal indexes (in number order) of arc flag tokens
* @private
*/
function getArcFlagTokenIndexes(path) {
const flags = /* @__PURE__ */ new Set();
let numIndex = -1;
let argIndex = 0;
let inArc = false;
let token;
RE_PATH_TOKEN.lastIndex = 0;
while (token = RE_PATH_TOKEN.exec(path)) if (token[1]) {
inArc = token[1] === "a" || token[1] === "A";
argIndex = 0;
} else {
numIndex++;
if (inArc) {
const pos = argIndex % 7;
if (pos === 2 || pos === 3 || pos === 4) flags.add(numIndex);
argIndex++;
}
}
return flags;
}
/**
* Build a path interpolator that behaves like d3's string interpolator but
* snaps SVG arc flags to valid integers, preventing malformed paths during
* bar transitions (ex. chart.load() with bar.radius). See #4166.
* @param {string} a Start path
* @param {string} b Target path
* @returns {function} Interpolator returning a valid path for progress t
* @private
*/
function getBarPathInterpolator(a, b) {
const flagSet = getArcFlagTokenIndexes(b);
const segments = [];
const interpolators = [];
let bIndex = 0;
let segIndex = -1;
let numIndex = -1;
let am;
let bm;
const append = (text) => {
if (segments[segIndex]) segments[segIndex] += text;
else segments[++segIndex] = text;
};
RE_PATH_NUMBER.lastIndex = 0;
const reA = RE_PATH_NUMBER;
const reB = new RegExp(RE_PATH_NUMBER.source, "g");
a += "";
b += "";
while ((am = reA.exec(a)) && (bm = reB.exec(b))) {
numIndex++;
if (bm.index > bIndex) append(b.slice(bIndex, bm.index));
if (am[0] === bm[0]) append(bm[0]);
else {
const from = +am[0];
const to = +bm[0];
const isFlag = flagSet.has(numIndex);
segments[++segIndex] = null;
interpolators.push({
index: segIndex,
fn: isFlag ? (t) => Math.round(from + (to - from) * t) : (t) => from + (to - from) * t
});
}
bIndex = reB.lastIndex;
}
if (bIndex < b.length) append(b.slice(bIndex));
if (!interpolators.length) {
const result = segments.join("");
return () => result;
}
return (t) => {
for (let i = 0, len = interpolators.length; i < len; i++) {
const { index, fn } = interpolators[i];
segments[index] = String(fn(t));
}
return segments.join("");
};
}
/**
* Get the configured bar corner radius resolver.
* @param {object} $$ ChartInternal instance
* @returns {function|null} Radius resolver
* @private
*/
function getBarRadiusResolver($$) {
const { bar_radius: radius, bar_radius_ratio: ratio } = $$.config;
return isNumber(radius) && radius > 0 ? () => radius : isNumber(ratio) ? (width) => width * ratio : null;
}
/**
* Get stacked bar rows that should receive rounded bar-end corners.
* @param {object} $$ ChartInternal instance
* @returns {Set} Radius row keys
* @private
*/
function getStackingBarRadiusSet($$) {
const { config, data } = $$;
const set = /* @__PURE__ */ new Set();
if (!config.data_groups.length) return set;
const orderedTargets = $$.orderTargets($$.filterTargetsToShow(data.targets.filter($$.isBarType, $$)));
for (const group of config.data_groups) {
const ids = new Set(group);
const lastPosByIndex = /* @__PURE__ */ new Map();
const lastNegByIndex = /* @__PURE__ */ new Map();
for (const target of orderedTargets) {
if (!ids.has(target.id)) continue;
for (const d of target.values) {
if (d.value === null || d.value === 0) continue;
(d.value > 0 ? lastPosByIndex : lastNegByIndex).set(d.index, target.id);
}
}
for (const [index, id] of lastPosByIndex) set.add(`${id}:${index}`);
for (const [index, id] of lastNegByIndex) set.add(`${id}:${index}`);
}
return set;
}
/**
* Get canvas/SVG corner flags for the rounded bar edge.
* @param {boolean} isRotated Whether axis is rotated
* @param {boolean} isNegative Whether the bar direction is negative
* @param {number} radius Radius value
* @returns {number|object} Corner radius map
* @private
*/
function getBarRadiusCorners(isRotated, isNegative, radius) {
if (!radius) return 0;
return isRotated ? isNegative ? {
tl: radius,
bl: radius
} : {
tr: radius,
br: radius
} : isNegative ? {
br: radius,
bl: radius
} : {
tl: radius,
tr: radius
};
}
/**
* Get SVG clip-path inset for radius arcs that exceed the bar shape.
* @param {Array} init Initial render coordinate
* @param {number} pos Rounded edge position
* @param {boolean} isRotated Whether axis is rotated
* @param {boolean} isNegative Whether the bar direction is negative
* @returns {string|null} Clip-path value
* @private
*/
function getBarRadiusClipPath(init, pos, isRotated, isNegative) {
let clipPath = "";
if (isRotated) {
if (isNegative && init[0] < pos) clipPath = `0 ${pos - init[0]}px 0 0`;
else if (!isNegative && init[0] > pos) clipPath = `0 0 0 ${init[0] - pos}px`;
} else if (isNegative && init[1] > pos) clipPath = `${init[1] - pos}px 0 0 0`;
else if (!isNegative && init[1] < pos) clipPath = `0 0 ${pos - init[1]}px 0`;
return clipPath ? `inset(${clipPath})` : null;
}
/**
* Get shared rounded bar geometry that renderers can translate to their own primitives.
* @param {object} $$ ChartInternal instance
* @param {object} d Data row
* @param {Array} points Shared bar points
* @param {function|null} getRadius Radius resolver
* @param {Set} stackingRadiusSet Stacked radius row keys
* @param {function} isStackingRadiusData Optional fallback for hidden SVG bars
* @returns {object} Rounded bar geometry
* @private
*/
function getBarRadiusInfo($$, d, points, getRadius, stackingRadiusSet, isStackingRadiusData) {
const { config, state } = $$;
const isRotated = config.axis_rotated;
const indexX = +isRotated;
const indexY = +!indexX;
const isUnderZero = d.value < 0;
const isInverted = config[`axis_${$$.axis.getId(d.id)}_inverted`];
const isNegative = !isInverted && isUnderZero || isInverted && !isUnderZero;
const isGrouped = $$.isGrouped(d.id);
const isRadiusData = getRadius && isGrouped && d.value !== 0 ? state.hiddenTargetIds.has(d.id) && isStackingRadiusData ? isStackingRadiusData(d) : stackingRadiusSet.has(`${d.id}:${d.index}`) : false;
const init = [points[0][indexX], points[0][indexY]];
let radius = 0;
if (getRadius) {
const index = isRotated ? indexY : indexX;
const barW = Math.abs(points[2][index] - points[0][index]);
radius = Math.max(0, !isGrouped || isRadiusData ? getRadius(barW) : 0);
}
const pos = isRotated ? points[1][indexX] + (isNegative ? radius : -radius) : points[1][indexY] + (isNegative ? -radius : radius);
return {
radius,
corners: getBarRadiusCorners(isRotated, isNegative, radius),
clipPath: radius ? getBarRadiusClipPath(init, pos, isRotated, isNegative) : null,
indexX,
indexY,
isNegative,
pos
};
}
//#endregion
export { getBarPathInterpolator, getBarRadiusInfo, getBarRadiusResolver, getStackingBarRadiusSet };