billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
375 lines (306 loc) • 10.7 kB
text/typescript
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
import {document} from "../../module/browser";
import {$AXIS, $SUBCHART} from "../../config/classes";
import {isValue, ceil10, capitalize, isNumber, isEmpty} from "../../module/util";
export default {
/**
* Update container size
* @private
*/
setContainerSize(): void {
const $$ = this;
const {state} = $$;
state.current.width = $$.getCurrentWidth();
state.current.height = $$.getCurrentHeight();
},
getCurrentWidth(): number {
const $$ = this;
return $$.config.size_width || $$.getParentWidth();
},
getCurrentHeight(): number {
const $$ = this;
const {config} = $$;
const h = config.size_height || $$.getParentHeight();
return h > 0 ? h : 320 / ($$.hasType("gauge") && !config.gauge_fullCircle ? 2 : 1);
},
getCurrentPaddingTop(): number {
const $$ = this;
const {config, state: {hasAxis}, $el} = $$;
const axesLen = hasAxis ? config.axis_y2_axes.length : 0;
let padding = isValue(config.padding_top) ?
config.padding_top : 0;
if ($el.title && $el.title.node()) {
padding += $$.getTitlePadding();
}
if (axesLen && config.axis_rotated) {
padding += $$.getHorizontalAxisHeight("y2") * axesLen;
}
return padding;
},
getCurrentPaddingBottom(): number {
const $$ = this;
const {config, state: {hasAxis}} = $$;
const axisId = config.axis_rotated ? "y" : "x";
const axesLen = hasAxis ? config[`axis_${axisId}_axes`].length : 0;
const padding = isValue(config.padding_bottom) ?
config.padding_bottom : 0;
return padding + (
axesLen ? $$.getHorizontalAxisHeight(axisId) * axesLen : 0
);
},
getCurrentPaddingLeft(withoutRecompute?: boolean): number {
const $$ = this;
const {config, state: {hasAxis}} = $$;
const isRotated = config.axis_rotated;
const axisId = isRotated ? "x" : "y";
const axesLen = hasAxis ? config[`axis_${axisId}_axes`].length : 0;
const axisWidth = hasAxis ? $$.getAxisWidthByAxisId(axisId, withoutRecompute) : 0;
let padding;
if (isValue(config.padding_left)) {
padding = config.padding_left;
} else if (hasAxis && isRotated) {
padding = !config.axis_x_show ?
1 : Math.max(ceil10(axisWidth), 40);
} else if (hasAxis && (!config.axis_y_show || config.axis_y_inner)) { // && !config.axis_rotated
padding = $$.axis.getAxisLabelPosition("y").isOuter ? 30 : 1;
} else {
padding = ceil10(axisWidth);
}
return padding + (axisWidth * axesLen);
},
getCurrentPaddingRight(withXAxisTickTextOverflow = false): number {
const $$ = this;
const {config, state: {hasAxis}} = $$;
const defaultPadding = 10;
const legendWidthOnRight = $$.state.isLegendRight ? $$.getLegendWidth() + 20 : 0;
const axesLen = hasAxis ? config.axis_y2_axes.length : 0;
const axisWidth = hasAxis ? $$.getAxisWidthByAxisId("y2") : 0;
const xAxisTickTextOverflow = withXAxisTickTextOverflow ?
$$.axis.getXAxisTickTextY2Overflow(defaultPadding) : 0;
let padding;
if (isValue(config.padding_right)) {
padding = config.padding_right + 1; // 1 is needed not to hide tick line
} else if ($$.axis && config.axis_rotated) {
padding = defaultPadding + legendWidthOnRight;
} else if ($$.axis && (!config.axis_y2_show || config.axis_y2_inner)) { // && !config.axis_rotated
padding = Math.max(
2 + legendWidthOnRight + ($$.axis.getAxisLabelPosition("y2").isOuter ? 20 : 0),
xAxisTickTextOverflow
);
} else {
padding = Math.max(ceil10(axisWidth) + legendWidthOnRight, xAxisTickTextOverflow);
}
return padding + (axisWidth * axesLen);
},
/**
* Get the parent rect element's size
* @param {string} key property/attribute name
* @returns {number}
* @private
*/
getParentRectValue(key): number {
const offsetName = `offset${capitalize(key)}`;
let parent = this.$el.chart.node();
let v = 0;
while (v < 30 && parent && parent.tagName !== "BODY") {
try {
v = parent.getBoundingClientRect()[key];
} catch (e) {
if (offsetName in parent) {
// In IE in certain cases getBoundingClientRect
// will cause an "unspecified error"
v = parent[offsetName];
}
}
parent = parent.parentNode;
}
// Sometimes element's dimension value is incorrect(ex. flex container)
// In this case, use body's offset instead.
const bodySize = document.body[offsetName];
v > bodySize && (v = bodySize);
return v;
},
getParentWidth(): number {
return this.getParentRectValue("width");
},
getParentHeight(): number {
const h: string = this.$el.chart.style("height");
let height = 0;
if (h) {
height = /px$/.test(h) ?
parseInt(h, 10) :
this.getParentRectValue("height");
}
return height;
},
getSvgLeft(withoutRecompute?: boolean): number {
const $$ = this;
const {config, $el} = $$;
const hasLeftAxisRect = config.axis_rotated || (!config.axis_rotated && !config.axis_y_inner);
const leftAxisClass = config.axis_rotated ? $AXIS.axisX : $AXIS.axisY;
const leftAxis = $el.main.select(`.${leftAxisClass}`).node();
const svgRect = leftAxis && hasLeftAxisRect ? leftAxis.getBoundingClientRect() : {right: 0};
const chartRect = $el.chart.node().getBoundingClientRect();
const hasArc = $$.hasArcType();
const svgLeft = svgRect.right - chartRect.left -
(hasArc ? 0 : $$.getCurrentPaddingLeft(withoutRecompute));
return svgLeft > 0 ? svgLeft : 0;
},
updateDimension(withoutAxis?: boolean): void {
const $$ = this;
const {config, state: {hasAxis}, $el} = $$;
if (hasAxis && !withoutAxis && $$.axis.x && config.axis_rotated) {
$$.axis.subX?.create($el.axis.subX);
}
// pass 'withoutAxis' param to not animate at the init rendering
$$.updateScales(withoutAxis);
$$.updateSvgSize();
$$.transformAll(false);
},
updateSvgSize(): void {
const $$ = this;
const {state: {clip, current, hasAxis, width, height}, $el: {svg}} = $$;
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 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<T = number | {[key: string]: string;}>(v: T): T {
const $$ = this;
const {config} = $$;
const isNum = isNumber(v);
let p = isNum ? 0 : {};
if (config.padding === false) {
!isNum && Object.keys(v).forEach(key => {
// when data.lables=true, do not reset top padding
p[key] = (
!isEmpty(config.data_labels) &&
config.data_labels !== false &&
key === "top"
) ? v[key] : 0;
});
} else {
p = v;
}
return p as T;
},
/**
* Update size values
* @param {boolean} isInit If is called at initialization
* @private
*/
updateSizes(isInit?: boolean): void {
const $$ = this;
const {config, state, $el: {legend}} = $$;
const isRotated = config.axis_rotated;
const hasArc = $$.hasArcType();
!isInit && $$.setContainerSize();
const currLegend = {
width: legend ? $$.getLegendWidth() : 0,
height: legend ? $$.getLegendHeight() : 0
};
if (!hasArc && config.axis_x_show && config.axis_x_tick_autorotate) {
$$.updateXAxisTickClip();
}
const legendHeightForBottom = state.isLegendRight || state.isLegendInset ? 0 : currLegend.height;
const xAxisHeight = isRotated || hasArc ? 0 : $$.getHorizontalAxisHeight("x");
const subchartXAxisHeight = config.subchart_axis_x_show && config.subchart_axis_x_tick_text_show ?
xAxisHeight : 30;
const subchartHeight = config.subchart_show && !hasArc ?
(config.subchart_size_height + subchartXAxisHeight) : 0;
// for main
state.margin = !hasArc && isRotated ? {
top: $$.getHorizontalAxisHeight("y2") + $$.getCurrentPaddingTop(),
right: hasArc ? 0 : $$.getCurrentPaddingRight(true),
bottom: $$.getHorizontalAxisHeight("y") + legendHeightForBottom + $$.getCurrentPaddingBottom(),
left: subchartHeight + (hasArc ? 0 : $$.getCurrentPaddingLeft())
} : {
top: 4 + $$.getCurrentPaddingTop(), // for top tick text
right: hasArc ? 0 : $$.getCurrentPaddingRight(true),
bottom: xAxisHeight + subchartHeight + legendHeightForBottom + $$.getCurrentPaddingBottom(),
left: hasArc ? 0 : $$.getCurrentPaddingLeft()
};
state.margin = $$.getResettedPadding(state.margin);
// for subchart
state.margin2 = isRotated ? {
top: state.margin.top,
right: NaN,
bottom: 20 + legendHeightForBottom,
left: $$.state.rotatedPadding.left
} : {
top: state.current.height - subchartHeight - legendHeightForBottom,
right: NaN,
bottom: subchartXAxisHeight + legendHeightForBottom,
left: state.margin.left
};
// for legend
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;
}
// for arc
const hasGauge = $$.hasType("gauge");
const isLegendRight = config.legend_show && state.isLegendRight;
state.arcWidth = state.width - (isLegendRight ? currLegend.width + 10 : 0);
state.arcHeight = state.height - (isLegendRight && !hasGauge ? 0 : 10);
if (hasGauge && !config.gauge_fullCircle) {
state.arcHeight += state.height - $$.getPaddingBottomForGauge();
}
$$.updateRadius?.();
if (state.isLegendRight && hasArc) {
state.margin3.left = state.arcWidth / 2 + state.radiusExpanded * 1.1;
}
}
};