billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
229 lines (226 loc) • 8.53 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
*/
import { scaleUtc, scaleTime, scaleLog, scaleSymlog, scaleLinear } from 'd3-scale';
import { parseDate } from '../../module/util/object.js';
import { isString, isValue } from '../../module/util/type-checks.js';
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
/**
* Get scale
* @param {string} [type='linear'] Scale type
* @param {number|Date} [min] Min range
* @param {number|Date} [max] Max range
* @returns {d3.scaleLinear|d3.scaleTime} scale
* @private
*/
function getScale(type = "linear", min, max) {
const scale = ({
linear: scaleLinear,
log: scaleSymlog,
_log: scaleLog,
time: scaleTime,
utc: scaleUtc
})[type]();
scale.type = type;
/_?log/.test(type) && scale.clamp(true);
return scale.range([min ?? 0, max ?? 1]);
}
var scale = {
/**
* Get x Axis scale function
* @param {number} min Min range value
* @param {number} max Max range value
* @param {Array} domain Domain value
* @param {function} offset The offset getter to be sum
* @returns {function} scale
* @private
*/
getXScale(min, max, domain, offset) {
const $$ = this;
const scale = ($$.state.loading !== "append" && $$.scale.zoom) ||
getScale($$.axis.getAxisType("x"), min, max);
return $$.getCustomizedXScale(domain ? scale.domain(domain) : scale, offset);
},
/**
* Get y Axis scale function
* @param {string} id Axis id: 'y' or 'y2'
* @param {number} min Min value
* @param {number} max Max value
* @param {Array} domain Domain value
* @param {object} [existing] Existing scale function to be updated
* @returns {function} Scale function
* @private
*/
getYScale(id, min, max, domain, existing) {
const $$ = this;
const type = $$.axis.getAxisType(id);
// Reuse existing scale if type hasn't changed — just update range
if (existing && existing.type === type) {
existing.range([min, max]);
domain && existing.domain(domain);
return existing;
}
const scale = getScale(type, min, max);
domain && scale.domain(domain);
return scale;
},
/**
* Get y Axis scale
* @param {string} id Axis id
* @param {boolean} isSub Weather is sub Axis
* @returns {function} Scale function
* @private
*/
getYScaleById(id, isSub = false) {
const isY2 = this.axis?.getId(id) === "y2";
const key = isSub ? (isY2 ? "subY2" : "subY") : (isY2 ? "y2" : "y");
return this.scale[key];
},
/**
* Get customized x axis scale
* @param {d3.scaleLinear|d3.scaleTime} scaleValue Scale function
* @param {function} offsetValue Offset getter to be sum
* @returns {function} Scale function
* @private
*/
getCustomizedXScale(scaleValue, offsetValue) {
const $$ = this;
const offset = () => {
const value = offsetValue ? offsetValue() : $$.axis.x.tickOffset();
return value || ($$.axis.isCategorized() ? (scaleValue(1) - scaleValue(0)) / 2 : 0);
};
const isInverted = $$.config.axis_x_inverted;
/**
* Get scaled value
* @param {object} d Data object
* @returns {number}
* @private
*/
const scale = function (d) {
return scaleValue(d) + offset();
};
// copy original scale methods
for (const key in scaleValue) {
scale[key] = scaleValue[key];
}
scale.orgDomain = () => scaleValue.domain();
scale.orgScale = () => scaleValue;
// define custom domain() for categorized axis
if ($$.axis.isCategorized()) {
scale.domain = function (domainValue) {
let domain = domainValue;
if (!arguments.length) {
domain = this.orgDomain();
return isInverted ? [domain[0] + 1, domain[1]] : [domain[0], domain[1] + 1];
}
scaleValue.domain(domain);
return scale;
};
}
return scale;
},
/**
* Update scale
* @param {boolean} isInit Param is given at the init rendering
* @param {boolean} updateXDomain If update x domain
* @private
*/
updateScales(isInit, updateXDomain = true) {
const $$ = this;
const { axis, config, format, org, scale, state: { current, width, height, width2, height2, hasAxis, hasTreemap } } = $$;
if (hasAxis) {
const isRotated = config.axis_rotated;
const resettedPadding = $$.getResettedPadding(1);
// update edges
const min = {
x: isRotated ? resettedPadding : 0,
y: isRotated ? 0 : height,
subY: isRotated ? 0 : height2
};
const max = {
x: isRotated ? height : width,
y: isRotated ? width : resettedPadding,
subY: isRotated ? width2 : 1
};
// update scales
// x Axis
const xDomain = updateXDomain ?
scale.x?.orgDomain() :
(scale.zoom ? undefined : scale.x?.domain?.());
const xSubDomain = updateXDomain ?
org.xDomain :
(scale.zoom ? undefined : scale.subX?.domain?.());
scale.x = $$.getXScale(min.x, max.x, xDomain, () => axis.x.tickOffset());
scale.subX = $$.getXScale(min.x, max.x, xSubDomain, d => (d % 1 ? 0 : (axis.subX ?? axis.x).tickOffset()));
format.xAxisTick = axis.getXAxisTickFormat();
format.subXAxisTick = axis.getXAxisTickFormat(true);
axis.setAxis("x", scale.x, config.axis_x_tick_outer, isInit);
if (config.subchart_show) {
axis.setAxis("subX", scale.subX, config.axis_x_tick_outer, isInit);
}
// y Axis
scale.y = $$.getYScale("y", min.y, max.y, scale.y ? scale.y.domain() : config.axis_y_default, scale.y);
scale.subY = $$.getYScale("y", min.subY, max.subY, scale.subY ? scale.subY.domain() : config.axis_y_default, scale.subY);
axis.setAxis("y", scale.y, config.axis_y_tick_outer, isInit);
// y2 Axis
if (config.axis_y2_show) {
scale.y2 = $$.getYScale("y2", min.y, max.y, scale.y2 ? scale.y2.domain() : config.axis_y2_default, scale.y2);
scale.subY2 = $$.getYScale("y2", min.subY, max.subY, scale.subY2 ? scale.subY2.domain() : config.axis_y2_default, scale.subY2);
axis.setAxis("y2", scale.y2, config.axis_y2_tick_outer, isInit);
}
}
else if (hasTreemap) {
const padding = $$.getCurrentPadding();
scale.x = scaleLinear().rangeRound([padding.left, current.width - padding.right]);
scale.y = scaleLinear().rangeRound([padding.top, current.height - padding.bottom]);
}
else {
// update for arc
$$.updateArc?.();
}
},
/**
* Get the zoom or unzoomed scaled value
* @param {Date|number|object} d Data value
* @returns {number|null}
* @private
*/
xx(d) {
const $$ = this;
const { config, scale: { x, zoom } } = $$;
const fn = config.zoom_enabled && zoom ? zoom : x;
return d ? fn(isValue(d.x) ? d.x : d) : null;
},
xv(d) {
const $$ = this;
const { axis, config, scale: { x, zoom } } = $$;
const fn = config.zoom_enabled && zoom ? zoom : x;
let value = $$.getBaseValue(d);
if (axis.isTimeSeries()) {
value = parseDate.call($$, value);
}
else if (axis.isCategorized() && isString(value)) {
value = config.axis_x_categories.indexOf(value);
}
return fn(value);
},
yv(d) {
const $$ = this;
const { scale: { y, y2 } } = $$;
const yScale = d.axis && d.axis === "y2" ? y2 : y;
return yScale($$.getBaseValue(d));
},
subxx(d) {
return d ? this.scale.subX(d.x) : null;
}
};
export { scale as default, getScale };