billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
344 lines (325 loc) • 10.1 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.0.1
* @requires billboard.js
* @summary billboard.js plugin
*/
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("d3-delaunay"));
else if(typeof define === 'function' && define.amd)
define("bb", ["d3-delaunay"], factory);
else if(typeof exports === 'object')
exports["bb"] = factory(require("d3-delaunay"));
else
root["bb"] = root["bb"] || {}, root["bb"]["plugin"] = root["bb"]["plugin"] || {}, root["bb"]["plugin"]["textoverlap"] = factory(root["d3"]);
})(this, function(__WEBPACK_EXTERNAL_MODULE__9__) {
return /******/ (function() { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ 9:
/***/ (function(module) {
module.exports = __WEBPACK_EXTERNAL_MODULE__9__;
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ !function() {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = function(exports, definition) {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ }();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ !function() {
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
/******/ }();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
!function() {
// EXPORTS
__webpack_require__.d(__webpack_exports__, {
"default": function() { return /* binding */ TextOverlap; }
});
// EXTERNAL MODULE: external {"commonjs":"d3-delaunay","commonjs2":"d3-delaunay","amd":"d3-delaunay","root":"d3"}
var external_commonjs_d3_delaunay_commonjs2_d3_delaunay_amd_d3_delaunay_root_d3_ = __webpack_require__(9);
;// ./src/module/polygon.ts
function polygonArea(polygon) {
const n = polygon.length;
let area = 0;
let b = polygon[n - 1];
for (let i = 0; i < n; i++) {
const a = b;
b = polygon[i];
area += a[1] * b[0] - a[0] * b[1];
}
return area / 2;
}
function polygonCentroid(polygon) {
const n = polygon.length;
let x = 0;
let y = 0;
let k = 0;
let b = polygon[n - 1];
for (let i = 0; i < n; i++) {
const a = b;
b = polygon[i];
const c = a[0] * b[1] - b[0] * a[1];
k += c;
x += (a[0] + b[0]) * c;
y += (a[1] + b[1]) * c;
}
k *= 3;
return [x / k, y / k];
}
;// ./src/module/util/type-checks.ts
const isValue = (v) => v || v === 0;
const isFunction = (v) => typeof v === "function";
const isString = (v) => typeof v === "string";
const isNumber = (v) => typeof v === "number";
const isUndefined = (v) => typeof v === "undefined";
const isDefined = (v) => typeof v !== "undefined";
const isBoolean = (v) => typeof v === "boolean";
const ceil10 = (v) => Math.ceil(v / 10) * 10;
const asHalfPixel = (n) => Math.ceil(n) + 0.5;
const diffDomain = (d) => d[1] - d[0];
const isObjectType = (v) => typeof v === "object";
const isEmptyObject = (obj) => {
for (const x in obj) {
return false;
}
return true;
};
const isEmpty = (o) => isUndefined(o) || o === null || isString(o) && o.length === 0 || isObjectType(o) && !(o instanceof Date) && isEmptyObject(o) || isNumber(o) && isNaN(o);
const notEmpty = (o) => !isEmpty(o);
const isArray = (arr) => Array.isArray(arr);
const isObject = (obj) => obj && !(obj == null ? void 0 : obj.nodeType) && isObjectType(obj) && !isArray(obj);
;// ./src/config/config.ts
function loadConfig(config) {
const thisConfig = this.config;
let target;
let keys;
let read;
const find = () => {
const key = keys.shift();
if (key && target && isObjectType(target) && key in target) {
target = target[key];
return find();
} else if (!key) {
return target;
}
return void 0;
};
Object.keys(thisConfig).forEach((key) => {
target = config;
keys = key.split("_");
read = find();
if (isDefined(read)) {
thisConfig[key] = read;
}
});
if (this.api) {
this.state.orgConfig = config;
}
}
;// ./src/Plugin/Plugin.ts
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
class Plugin {
/**
* Constructor
* @param {Any} options config option object
* @private
*/
constructor(options = {}) {
__publicField(this, "$$");
__publicField(this, "options");
__publicField(this, "config");
this.options = options;
}
/**
* Load plugin config from options
* @private
*/
loadConfig() {
loadConfig.call(this, this.options);
}
/**
* Lifecycle hook for 'beforeInit' phase.
* @private
*/
$beforeInit() {
}
/**
* Lifecycle hook for 'init' phase.
* @private
*/
$init() {
}
/**
* Lifecycle hook for 'afterInit' phase.
* @private
*/
$afterInit() {
}
/**
* Lifecycle hook for 'redraw' phase.
* @private
*/
$redraw() {
}
/**
* Lifecycle hook for 'willDestroy' phase.
* @private
*/
$willDestroy() {
Object.keys(this).forEach((key) => {
this[key] = null;
delete this[key];
});
}
}
__publicField(Plugin, "version", "4.0.1");
;// ./src/Plugin/textoverlap/Options.ts
class Options {
constructor() {
return {
/**
* Selector string for target text nodes within chart element.
* - **NOTE:** If no value is given, defaults to data label text elements.
* @name selector
* @memberof plugin-textoverlap
* @type {string}
* @default undefined
* @example
* // selector for data label text nodes
* selector: ".bb-texts text"
*/
selector: void 0,
/**
* Extent of label overlap prevention.
* @name extent
* @memberof plugin-textoverlap
* @type {number}
* @default 1
* @example
* extent: 1
*/
extent: 1,
/**
* Minimum area needed to show a data label.
* @name area
* @memberof plugin-textoverlap
* @type {number}
* @default 0
* @example
* area: 0
*/
area: 0
};
}
}
;// ./src/Plugin/textoverlap/index.ts
class TextOverlap extends Plugin {
constructor(options) {
super(options);
this.config = new Options();
return this;
}
$init() {
this.loadConfig();
}
$redraw() {
const { $$: { $el }, config: { selector } } = this;
const text = selector ? $el.main.selectAll(selector) : $el.text;
!text.empty() && this.preventLabelOverlap(text);
}
/**
* Generates the voronoi layout for data labels
* @param {Array} points Indices values
* @returns {object} Voronoi layout points and corresponding Data points
* @private
*/
generateVoronoi(points) {
const { $$ } = this;
const { scale } = $$;
const [min, max] = ["x", "y"].map((v) => scale[v].domain());
[min[1], max[0]] = [max[0], min[1]];
return external_commonjs_d3_delaunay_commonjs2_d3_delaunay_amd_d3_delaunay_root_d3_.Delaunay.from(points).voronoi([
...min,
...max
]);
}
/**
* Set text label's position to preventg overlap.
* @param {d3Selection} text target text selection
* @private
*/
preventLabelOverlap(text) {
const { extent, area } = this.config;
const points = text.data().map((v) => [v.index, v.value]);
const voronoi = this.generateVoronoi(points);
let i = 0;
text.each(function() {
const cell = voronoi.cellPolygon(i);
if (cell && this) {
const [x, y] = points[i];
const [cx, cy] = polygonCentroid(cell);
const cellArea = Math.abs(polygonArea(cell));
const angle = Math.round(Math.atan2(cy - y, cx - x) / Math.PI * 2);
const xTranslate = extent * (angle === 0 ? 1 : -1);
const yTranslate = angle === -1 ? -extent : extent + 5;
const txtAnchor = Math.abs(angle) === 1 ? "middle" : angle === 0 ? "start" : "end";
this.style.display = cellArea < area ? "none" : "";
this.setAttribute("text-anchor", txtAnchor);
this.setAttribute("dy", `0.${angle === 1 ? 71 : 35}em`);
this.setAttribute("transform", `translate(${xTranslate}, ${yTranslate})`);
}
i++;
});
}
}
}();
__webpack_exports__ = __webpack_exports__["default"];
/******/ return __webpack_exports__;
/******/ })()
;
});