UNPKG

billboard.js

Version:

Re-usable easy interface JavaScript chart library, based on D3 v4+

343 lines (342 loc) • 14.1 kB
/*! * 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 };