billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
187 lines (155 loc) • 4.88 kB
text/typescript
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
import {select as d3Select} from "d3-selection"; // selection
import type {RegionOptions} from "../../../types/options";
import type {AxisType} from "../../../types/types";
import {$REGION} from "../../config/classes";
import {getBoundingRect, isString, isValue, parseDate} from "../../module/util";
export default {
initRegion(): void {
const $$ = this;
const {$el} = $$;
$el.region.main = $el.main
.insert("g", ":first-child")
.attr("clip-path", $$.state.clip.path)
.attr("class", $REGION.regions);
},
updateRegion(): void {
const $$ = this;
const {config, $el: {region}, $T} = $$;
if (!region.main) {
$$.initRegion();
}
// hide if arc type
region.main.style("visibility", $$.hasArcType() ? "hidden" : null);
// select <g> element
const regions = region.main
.selectAll(`.${$REGION.region}`)
.data(config.regions);
$T(regions.exit())
.style("opacity", "0")
.remove();
const regionsEnter = regions
.enter()
.append("g");
regionsEnter
.append("rect")
.style("fill-opacity", "0");
region.list = regionsEnter
.merge(regions)
.attr("class", $$.classRegion.bind($$));
region.list.each(function(d) {
const g = d3Select(this);
if (g.select("text").empty() && d.label?.text) {
d3Select(this).append("text")
.style("opacity", "0");
}
});
},
redrawRegion(withTransition: boolean) {
const $$ = this;
const {$el: {region}, $T} = $$;
const regionX = $$.regionX.bind($$);
const regionY = $$.regionY.bind($$);
const attr = ["width", "height"];
let regions = region.list.select("rect");
let label = region.list.selectAll("text");
regions = $T(regions, withTransition)
.attr("x", regionX)
.attr("y", regionY)
.attr("width", $$.regionWidth.bind($$))
.attr("height", $$.regionHeight.bind($$));
label = $T(label, withTransition)
.text(d => d.label?.text)
.attr("transform", ({label}) => label.rotated ? ` rotate(-90)` : null)
.attr("transform", function(d) {
const {x = 0, y = 0, center = false, rotated = false} = d.label ?? {};
const rect = this.previousElementSibling;
const pos = {x: 0, y: 0};
if (isString(center)) {
["x", "y"].forEach((v, i) => {
if (center.indexOf(v) > -1) {
pos[v] =
(+rect.getAttribute(attr[i]) - getBoundingRect(this)[attr[i]]) / 2;
}
});
}
return `translate(${regionX(d) + pos.x + x}, ${regionY(d) + pos.y + y})${
rotated ? ` rotate(-90)` : ``
}`;
})
.attr("text-anchor", ({label}) => label?.rotated ? "end" : null)
.attr("dy", "1em")
.style("fill", ({label}) => label?.color ?? null);
return [
regions
.style("fill-opacity", d => (isValue(d.opacity) ? d.opacity : null))
.on("end", function() {
// remove unnecessary rect after transition
d3Select(this.parentNode)
.selectAll("rect:not([x])")
.remove();
}),
label.style("opacity", null)
];
},
regionX(d: RegionOptions): number {
return this.getRegionSize("x", d);
},
regionY(d: RegionOptions): number {
return this.getRegionSize("y", d);
},
regionWidth(d: RegionOptions): number {
return this.getRegionSize("width", d);
},
regionHeight(d: RegionOptions): number {
return this.getRegionSize("height", d);
},
/**
* Get Region size according start/end position
* @param {string} type Type string
* @param {ojbect} d Data object
* @returns {number}
* @private
*/
getRegionSize(type: AxisType | "width" | "height", d: RegionOptions): number {
const $$ = this;
const {config, scale, state} = $$;
const isRotated = config.axis_rotated;
const isAxisType = /(x|y|y2)/.test(type);
const isType = isAxisType ? type === "x" : type === "width";
const start = !isAxisType && $$[isType ? "regionX" : "regionY"](d);
let key = isAxisType ? "start" : "end";
let pos = isAxisType ? 0 : state[type];
let currScale;
if (d.axis === "y" || d.axis === "y2") {
if (!isAxisType && !isType) {
key = "start";
} else if (isAxisType && !isType) {
key = "end";
}
if ((isType ? isRotated : !isRotated) && key in d) {
currScale = scale[d.axis];
}
} else if ((isType ? !isRotated : isRotated) && key in d) {
currScale = scale.zoom || scale.x;
}
if (currScale) {
let offset = 0;
pos = d[key];
if ($$.axis.isTimeSeries(d.axis)) {
pos = parseDate.call($$, pos);
} else if (/(x|width)/.test(type) && $$.axis.isCategorized() && isNaN(pos)) {
pos = config.axis_x_categories.indexOf(pos);
offset = $$.axis.x.tickOffset() * (key === "start" ? -1 : 1);
}
pos = currScale(pos) + offset;
}
return isAxisType ? pos : pos < start ? 0 : pos - start;
},
isRegionOnX(d: RegionOptions): boolean {
return !d.axis || d.axis === "x";
}
};