billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
273 lines (230 loc) • 7 kB
text/typescript
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
import {
scaleLinear as d3ScaleLinear,
scaleLog as d3ScaleLog,
scaleSymlog as d3ScaleSymlog,
scaleTime as d3ScaleTime,
scaleUtc as d3ScaleUtc
} from "d3-scale";
import {isString, isValue, parseDate} from "../../module/util";
import type {IDataRow, IGridData} from "../data/IData";
/**
* 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
*/
export function getScale<T = IDataRow["x"]>(type = "linear", min?: T, max?: T): any {
const scale = ({
linear: d3ScaleLinear,
log: d3ScaleSymlog,
_log: d3ScaleLog,
time: d3ScaleTime,
utc: d3ScaleUtc
})[type]();
scale.type = type;
/_?log/.test(type) && scale.clamp(true);
return scale.range([min ?? 0, max ?? 1]);
}
export default {
/**
* 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: number, max: number, domain: number[], offset: Function) {
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
* @returns {Function} Scale function
* @private
*/
getYScale(id: "y" | "y2", min: number, max: number, domain: number[]): Function {
const $$ = this;
const scale = getScale($$.axis.getAxisType(id), 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: string, isSub = false): Function {
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: Function | any, offsetValue): Function {
const $$ = this;
const offset = offsetValue || (() => $$.axis.x.tickOffset());
const isInverted = $$.config.axis_x_inverted;
/**
* Get scaled value
* @param {object} d Data object
* @returns {number}
* @private
*/
const scale = function(d: IDataRow): number {
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: boolean, updateXDomain = true): void {
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,
subX: isRotated ? 1 : 0,
subY: isRotated ? 0 : height2
};
const max = {
x: isRotated ? height : width,
y: isRotated ? width : resettedPadding,
subX: isRotated ? height : width,
subY: isRotated ? width2 : 1
};
// update scales
// x Axis
const xDomain = updateXDomain && scale.x?.orgDomain();
const xSubDomain = updateXDomain && org.xDomain;
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.subY = $$.getYScale(
"y",
min.subY,
max.subY,
scale.subY ? scale.subY.domain() : config.axis_y_default
);
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.subY2 = $$.getYScale(
"y2",
min.subY,
max.subY,
scale.subY2 ? scale.subY2.domain() : config.axis_y2_default
);
axis.setAxis("y2", scale.y2, config.axis_y2_tick_outer, isInit);
}
} else if (hasTreemap) {
const padding = $$.getCurrentPadding();
scale.x = d3ScaleLinear().rangeRound([padding.left, current.width - padding.right]);
scale.y = d3ScaleLinear().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: IDataRow): number | null {
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: IGridData): number {
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: IGridData): number {
const $$ = this;
const {scale: {y, y2}} = $$;
const yScale = d.axis && d.axis === "y2" ? y2 : y;
return yScale($$.getBaseValue(d));
},
subxx(d: IDataRow): number | null {
return d ? this.scale.subX(d.x) : null;
}
};