billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
343 lines (342 loc) • 14.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.1.1
*/
import { document as doc } from "../../module/browser.js";
import { ceil10, isEmpty, isNumber, isString, isUndefined } from "../../module/util/type-checks.js";
import { capitalize } from "../../module/util/object.js";
import { getBoundingRect } from "../../module/util/dom.js";
import { $AXIS, $SUBCHART } from "../../config/classes.js";
import { KEY } from "../../module/Cache.js";
//#region src/ChartInternal/internals/size.ts
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
var size_default = {
/**
* Update container size
* @private
*/
setContainerSize() {
const $$ = this;
const { state } = $$;
state.current.width = $$.getCurrentWidth();
state.current.height = $$.getCurrentHeight();
},
getCurrentWidth() {
const $$ = this;
return $$.config.size_width || $$.getParentRectValue("width");
},
getCurrentHeight() {
const $$ = this;
const { config, state, $el } = $$;
let h = config.size_height;
if (!h) {
const chartNode = state.isCanvasMode ? $el.chart?.node() : null;
const minHeight = chartNode?.style.minHeight;
minHeight && (chartNode.style.minHeight = "");
h = $$.getParentHeight();
minHeight && (chartNode.style.minHeight = minHeight);
const surface = chartNode ? parseFloat($$.canvasEngine?.canvas.style.height) || 0 : 0;
const reserve = state.current.height - surface;
if (reserve > 0 && Math.round(h) === Math.round(surface)) h += reserve;
}
return h > 0 ? h : 320 / ($$.hasType("gauge") && !config.gauge_fullCircle ? 2 : 1);
},
/**
* Get the parent rect element's size
* @param {string} key property/attribute name
* @returns {number}
* @private
*/
getParentRectValue(key) {
const offsetName = `offset${capitalize(key)}`;
let parent = this.$el.chart.node();
let v = 0;
while (v < 30 && parent && parent.tagName !== "BODY") {
try {
v = getBoundingRect(parent, true)[key];
} catch {
if (offsetName in parent) v = parent[offsetName];
}
parent = parent.parentNode;
}
const bodySize = doc.body[offsetName];
v > bodySize && (v = bodySize);
return v;
},
getParentHeight() {
const h = this.$el.chart.style("height");
let height = 0;
if (h) height = /px$/.test(h) ? parseInt(h, 10) : this.getParentRectValue("height");
return height;
},
getSvgLeft(withoutRecompute) {
const $$ = this;
const { cache, config, state: { hasAxis }, $el } = $$;
if (withoutRecompute) {
const cached = cache.get(KEY.svgLeft);
if (cached !== null) return cached;
}
const isRotated = config.axis_rotated;
const hasLeftAxisRect = isRotated || !isRotated && !config.axis_y_inner;
const leftAxisClass = isRotated ? $AXIS.axisX : $AXIS.axisY;
const leftAxis = $el.main.select(`.${leftAxisClass}`).node();
const leftLabel = hasAxis && config[`axis_${isRotated ? "x" : "y"}_label`];
const label = hasAxis && (isString(leftLabel) || isString(leftLabel.text) || /^inner-/.test(leftLabel?.position)) ? $el.main.select(`.${leftAxisClass}-label`) : null;
const labelNode = label && !label.empty() ? label.node() : null;
const forceEval = !withoutRecompute;
const rects = {
label: labelNode ? getBoundingRect(labelNode, forceEval) : null,
leftAxis: leftAxis && hasLeftAxisRect ? getBoundingRect(leftAxis, forceEval) : null,
chart: getBoundingRect($el.chart.node(), forceEval)
};
const labelWidth = rects.label?.left ?? 0;
const svgRect = rects.leftAxis ?? { right: 0 };
const chartRectLeft = rects.chart.left + labelWidth;
const hasArc = $$.hasArcType();
const svgLeft = svgRect.right - chartRectLeft - (hasArc ? 0 : $$.getCurrentPaddingByDirection("left", withoutRecompute));
const result = svgLeft > 0 ? svgLeft : 0;
cache.add(KEY.svgLeft, result);
return result;
},
/**
* Stretch the rendered surface to the container's current size, without redrawing.
* Gives an immediate size feedback while the delayed resize is pending.
* The drawn content keeps its previous geometry until the redraw takes place.
* @private
*/
previewResize() {
const $$ = this;
const { state, $el } = $$;
const { current } = state;
const width = $$.getCurrentWidth();
const height = $$.getCurrentHeight();
if (!width || !height || !current.width || !current.height || width === current.width && height === current.height) return;
if (state.isCanvasMode) {
const canvas = $$.canvasEngine?.canvas;
if (!canvas) return;
$el.chart.style("min-height", `${height}px`);
canvas.style.width = `${width}px`;
canvas.style.height = `${Math.max(0, height - ($$.getCanvasBottomLegendHeight?.() ?? 0))}px`;
} else if ($el.svg) $el.svg.attr("viewBox", `0 0 ${current.width} ${current.height}`).attr("preserveAspectRatio", "none").attr("width", width).attr("height", height);
else return;
state.resizePreview = true;
},
/**
* Drop the stretched state set by previewResize(), to render on exact pixels again.
* @private
*/
clearResizePreview() {
const { config, state, $el } = this;
if (!state.resizePreview) return;
state.resizePreview = false;
if (!state.isCanvasMode && $el.svg && config.resize_auto !== "viewBox") $el.svg.attr("viewBox", null).attr("preserveAspectRatio", null);
},
updateDimension(withoutAxis) {
const $$ = this;
const { config, state: { hasAxis, isCanvasMode }, $el } = $$;
if (hasAxis && !withoutAxis && $$.axis.x && config.axis_rotated) $$.axis.subX?.create($el.axis.subX);
$$.updateScales(withoutAxis);
if (isCanvasMode) {
$$.resizeCanvas?.();
return;
}
$$.updateSvgSize();
$$.transformAll(false);
},
updateSvgSize() {
const $$ = this;
const { config, state: { clip, current, hasAxis, width, height }, $el: { svg } } = $$;
if (!svg) {
$$.resizeCanvas?.();
return;
}
$$.clearResizePreview();
if (config.resize_auto === "viewBox") svg.attr("viewBox", `0 0 ${current.width} ${current.height}`);
else svg.attr("width", current.width).attr("height", current.height);
if (hasAxis) {
const brush = svg.select(`.${$SUBCHART.brush} .overlay`);
const brushSize = {
width: 0,
height: 0
};
if (brush.size()) {
brushSize.width = +brush.attr("width");
brushSize.height = +brush.attr("height");
}
svg.selectAll([`#${clip.id}`, `#${clip.idGrid}`]).select("rect").attr("width", width).attr("height", height);
svg.select(`#${clip.idXAxis}`).select("rect").call($$.setXAxisClipPath.bind($$));
svg.select(`#${clip.idYAxis}`).select("rect").call($$.setYAxisClipPath.bind($$));
clip.idSubchart && svg.select(`#${clip.idSubchart}`).select("rect").attr("width", width).attr("height", brushSize.height);
}
},
/**
* Get padding by the direction.
* @param {string} type "top" | "bottom" | "left" | "right"
* @param {boolean} [withoutRecompute=false] If set true, do not recompute the padding value.
* @param {boolean} [withXAxisTickTextOverflow=false] If set true, calculate x axis tick text overflow.
* @returns {number} padding value
* @private
*/
getCurrentPaddingByDirection(type, withoutRecompute = false, withXAxisTickTextOverflow = false) {
const $$ = this;
const { config, $el, state: { hasAxis, isCanvasMode } } = $$;
const isRotated = config.axis_rotated;
const isFitPadding = config.padding?.mode === "fit";
const paddingOption = isNumber(config[`padding_${type}`]) ? config[`padding_${type}`] : void 0;
const axisId = hasAxis ? {
top: isRotated ? "y2" : null,
bottom: isRotated ? "y" : "x",
left: isRotated ? "x" : "y",
right: isRotated ? null : "y2"
}[type] : null;
const isLeftRight = /^(left|right)$/.test(type);
const isAxisInner = axisId && config[`axis_${axisId}_inner`];
const isAxisShow = axisId && config[`axis_${axisId}_show`];
const axesLen = axisId ? config[`axis_${axisId}_axes`].length : 0;
let axisSize = axisId ? isLeftRight ? $$.getAxisWidthByAxisId(axisId, withoutRecompute) : $$.getHorizontalAxisHeight(axisId) : 0;
const defaultPadding = 20;
let gap = 0;
if (!isFitPadding && isLeftRight) axisSize = ceil10(axisSize);
let padding = hasAxis && isLeftRight && (isAxisInner || isUndefined(paddingOption) && !isAxisShow) ? 0 : isFitPadding ? (isAxisShow ? axisSize : 0) + (paddingOption ?? 0) : isUndefined(paddingOption) ? axisSize : paddingOption;
if (isLeftRight && hasAxis) {
if (axisId && (isFitPadding || isAxisInner) && config[`axis_${axisId}_label`].text) padding += $$.axis.getAxisLabelPosition(axisId).isOuter ? defaultPadding : 0;
if (type === "right") {
padding += isRotated ? !isFitPadding && isUndefined(paddingOption) ? 10 : 2 : !isAxisShow || isAxisInner ? isFitPadding ? 2 : 1 : 0;
padding += withXAxisTickTextOverflow ? $$.axis.getXAxisTickTextY2Overflow(defaultPadding) : 0;
} else if (type === "left" && isRotated && isUndefined(paddingOption)) padding = !config.axis_x_show ? 1 : isFitPadding ? axisSize : Math.max(axisSize, 40);
} else if (type === "top") {
if ($el.title && $el.title.node() || isCanvasMode && config.title_text) padding += $$.getTitlePadding();
gap = isRotated && !isAxisInner ? axesLen : 0;
} else if (type === "bottom" && hasAxis && isRotated && !isAxisShow) padding += 1;
return padding + axisSize * axesLen - gap;
},
getCurrentPadding(withXAxisTickTextOverflow = false) {
const $$ = this;
const [top, bottom, left, right] = [
"top",
"bottom",
"left",
"right"
].map((v) => $$.getCurrentPaddingByDirection(v, null, withXAxisTickTextOverflow));
return {
top,
bottom,
left,
right
};
},
/**
* Get resetted padding values when 'padding=false' option is set
* https://github.com/naver/billboard.js/issues/2367
* @param {number|object} v Padding values to be resetted
* @returns {number|object} Padding value
* @private
*/
getResettedPadding(v) {
const { config } = this;
const isNum = isNumber(v);
let p = isNum ? 0 : {};
if (config.padding === false) !isNum && Object.keys(v).forEach((key) => {
p[key] = !isEmpty(config.data_labels) && config.data_labels !== false && key === "top" ? v[key] : 0;
});
else p = v;
return p;
},
/**
* Update size values
* @param {boolean} isInit If is called at initialization
* @private
*/
updateSizes(isInit) {
const $$ = this;
const { config, state, $el: { legend } } = $$;
const isRotated = config.axis_rotated;
const isNonAxis = $$.hasArcType() || state.hasFunnel || state.hasTreemap;
const isFitPadding = config.padding?.mode === "fit";
!isInit && $$.setContainerSize();
const legendWidth = legend ? $$.getLegendWidth() : 0;
const legendHeight = legend ? $$.getLegendHeight() : 0;
const currLegend = {
width: legendWidth,
height: legendHeight
};
if (!isNonAxis && config.axis_x_show && config.axis_x_tick_autorotate) $$.updateXAxisTickClip();
const legendSize = {
right: config.legend_show && state.isLegendRight ? legendWidth + (isFitPadding ? 0 : 20) : 0,
bottom: !config.legend_show || state.isLegendRight || state.isLegendInset ? 0 : legendHeight
};
const xAxisHeight = isRotated || isNonAxis ? 0 : $$.getHorizontalAxisHeight("x");
const subchartXAxisHeight = config.subchart_axis_x_show && config.subchart_axis_x_tick_text_show ? xAxisHeight : 30;
const subchartHeight = config.subchart_show && !isNonAxis ? config.subchart_size_height + subchartXAxisHeight : 0;
const gaugeHeight = $$.hasType("gauge") && config.arc_needle_show && !config.gauge_fullCircle && !config.gauge_label_show ? 10 : 0;
const padding = $$.getCurrentPadding(true);
state.margin = !isNonAxis && isRotated ? {
top: padding.top,
right: isNonAxis ? 0 : padding.right + legendSize.right,
bottom: legendSize.bottom + padding.bottom,
left: subchartHeight + (isNonAxis ? 0 : padding.left)
} : {
top: (isFitPadding ? 0 : 4) + padding.top,
right: isNonAxis ? 0 : padding.right + legendSize.right,
bottom: gaugeHeight + subchartHeight + legendSize.bottom + padding.bottom,
left: isNonAxis ? 0 : padding.left
};
state.margin = $$.getResettedPadding(state.margin);
state.margin2 = isRotated ? {
top: state.margin.top,
right: NaN,
bottom: 20 + legendSize.bottom,
left: $$.state.rotatedPadding.left
} : {
top: state.current.height - subchartHeight - legendSize.bottom,
right: NaN,
bottom: subchartXAxisHeight + legendSize.bottom,
left: state.margin.left
};
state.margin3 = {
top: 0,
right: NaN,
bottom: 0,
left: 0
};
$$.updateSizeForLegend?.(currLegend);
state.width = state.current.width - state.margin.left - state.margin.right;
state.height = state.current.height - state.margin.top - state.margin.bottom;
if (state.width < 0) state.width = 0;
if (state.height < 0) state.height = 0;
state.width2 = isRotated ? state.margin.left - state.rotatedPadding.left - state.rotatedPadding.right : state.width;
state.height2 = isRotated ? state.height : state.current.height - state.margin2.top - state.margin2.bottom;
if (state.width2 < 0) state.width2 = 0;
if (state.height2 < 0) state.height2 = 0;
if ($$.hasArcType()) {
const hasGauge = $$.hasType("gauge");
const isLegendRight = config.legend_show && state.isLegendRight;
const textWidth = (state.hasRadar && $$.cache.get(KEY.radarTextWidth)) ?? 0;
state.arcWidth = state.width - (isLegendRight ? currLegend.width + 10 : 0) - textWidth;
state.arcHeight = state.height - (isLegendRight && !hasGauge ? 0 : 10);
if (config.arc_rangeText_values?.length) {
if (hasGauge) {
state.arcWidth -= 25;
state.arcHeight -= 10;
state.margin.left += 10;
} else {
state.arcHeight -= 20;
state.margin.top += 10;
}
}
if (hasGauge && !config.gauge_fullCircle) state.arcHeight += state.height - $$.getPaddingBottomForGauge();
$$.updateRadius?.();
}
if (state.isLegendRight && isNonAxis) state.margin3.left = state.arcWidth / 2 + state.radiusExpanded * 1.1;
}
};
//#endregion
export { size_default as default };