billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
319 lines (277 loc) • 7.43 kB
text/typescript
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
import {
hierarchy as d3Hierarchy,
treemap as d3Treemap,
treemapBinary as d3TreemapBinary,
treemapDice as d3TreemapDice,
treemapResquarify as d3TreemapResquarify,
treemapSlice as d3TreemapSlice,
treemapSliceDice as d3TreemapSliceDice,
treemapSquarify as d3TreemapSquarify
} from "d3-hierarchy";
import {select as d3Select} from "d3-selection";
import type {d3Selection} from "../../../types/types";
import {$COMMON, $TREEMAP} from "../../config/classes";
import {getRandom, isFunction} from "../../module/util";
import type {IData, IDataRow, ITreemapData} from "../data/IData";
/**
* Get treemap elements' position
* @param {d3Selection} group Root selection
* @param {object} root Root data
* @private
*/
function position(group, root): void {
const $$ = this;
const {scale: {x, y}, state: {width}} = $$;
group.selectAll("g")
.attr("transform", d => (
`translate(${d === root ? "0,0" : `${x(d.x0)},${y(d.y0)}`})`
))
.select("rect")
.attr("width", d => (
d === root ? width : x(d.x1) - x(d.x0)
))
.attr("height", d => (
d === root ? 0 : y(d.y1) - y(d.y0)
));
}
/**
* Convert data for treemap hierarchy
* @param {object} data Data object
* @returns {Array} Array of data for treemap hierarchy
* @private
*/
function convertDataToTreemapData(data: IData[]): ITreemapData[] {
const $$ = this;
return data.map(d => {
const {id, values} = d;
const {value} = values[0];
return {
name: id,
id, // needed to keep compatibility on whole code logic
value,
ratio: $$.getRatio("treemap", values[0])
} as ITreemapData;
});
}
/**
* Get hierarchy data
* @param {object} data Data object
* @returns {Array} Array of hierarchy data
* @private
*/
function getHierachyData(data) {
const $$ = this;
const hierarchyData = d3Hierarchy(data).sum(d => d.value);
const sortFn = $$.getSortCompareFn(true);
return [
$$.treemap(
sortFn ? hierarchyData.sort(sortFn) : hierarchyData
)
];
}
export default {
initTreemap(): void {
const $$ = this;
const {
$el,
state: {
current: {width, height},
clip,
datetimeId
}
} = $$;
clip.id = `${datetimeId}-clip`;
$$.treemap = d3Treemap()
.tile($$.getTreemapTile());
$el.defs
.append("clipPath")
.attr("id", clip.id)
.append("rect")
.attr("width", width)
.attr("height", height);
$el.treemap = $el.main.select(`.${$COMMON.chart}`)
.attr("clip-path", `url(#${clip.id})`)
.append("g")
.classed($TREEMAP.chartTreemaps, true);
$$.bindTreemapEvent();
},
/**
* Bind events
* @private
*/
bindTreemapEvent(): void {
const $$ = this;
const {$el, config, state} = $$;
const getTarget = event => {
const target = event.isTrusted ? event.target : state.eventReceiver.rect?.node();
let data;
if (/^rect$/i.test(target.tagName)) {
state.event = event;
data = d3Select(target).datum();
}
return data?.data;
};
if (config.interaction_enabled) {
const isTouch = state.inputType === "touch";
$el.treemap
.on(isTouch ? "touchstart" : "mouseover mousemove", event => {
const data = getTarget(event);
if (data) {
$$.showTooltip([data], event.currentTarget);
/^(touchstart|mouseover)$/.test(event.type) && $$.setOverOut(true, data);
}
})
.on(isTouch ? "touchend" : "mouseout", event => {
const data = getTarget(event);
if (config.interaction_onout) {
$$.hideTooltip();
$$.setOverOut(false, data);
}
});
}
},
/**
* Get tiling function
* @returns {Function}
* @private
*/
getTreemapTile() {
const $$ = this;
const {config, state: {current: {width, height}}} = $$;
const tile = {
binary: d3TreemapBinary,
dice: d3TreemapDice,
slice: d3TreemapSlice,
sliceDice: d3TreemapSliceDice,
squarify: d3TreemapSquarify,
resquarify: d3TreemapResquarify
}[config.treemap_tile ?? "binary"] ?? d3TreemapBinary;
return (node, x0, y0, x1, y1) => {
tile(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 + child.x0 / width * (x1 - x0);
child.x1 = x0 + child.x1 / width * (x1 - x0);
child.y0 = y0 + child.y0 / height * (y1 - y0);
child.y1 = y0 + child.y1 / height * (y1 - y0);
}
};
},
/**
* Get treemap hierarchy data
* @param {Array} targets Data targets
* @returns {object}
* @private
*/
getTreemapData(targets: IData[]): ITreemapData {
const $$ = this;
return {
name: "root",
children: convertDataToTreemapData.bind($$)(
$$.filterTargetsToShow(targets.filter($$.isTreemapType, $$))
)
};
},
/**
* Update treemap data
* @param {Array} targets Data targets
* @private
*/
updateTargetsForTreemap(targets: IData): void {
const $$ = this;
const {$el: {treemap}} = $$;
const treemapData = getHierachyData.call($$, $$.getTreemapData(targets ?? $$.data.targets));
// using $el.treemap reference can alter data, so select treemap <g> again
treemap.data($$.filterNullish(treemapData));
},
/**
* Render treemap
* @param {number} durationForExit Duration for exit transition
* @private
*/
updateTreemap(durationForExit: number): void {
const $$ = this;
const {$el, $T} = $$;
const data = $el.treemap.datum();
const classChartTreemap = $$.getChartClass("Treemap");
const classTreemap = $$.getClass("treemap", true);
const treemap = $el.treemap
.selectAll("g")
.data(data.children);
$T(treemap.exit(), durationForExit)
.style("opacity", "0")
.remove();
treemap.enter()
.append("g")
.append("rect");
$el.treemap.selectAll("g")
.attr("class", classChartTreemap)
.select("rect")
.attr("class", classTreemap)
.attr("fill", d => $$.color(d.data.name));
},
/**
* Generate treemap coordinate points data
* @returns {Array} Array of coordinate points
* @private
*/
generateGetTreemapPoints(): (d: IDataRow) => [number, number][] {
const $$ = this;
const {$el, scale: {x, y}} = $$;
const points = {};
$el.treemap.selectAll("g").each(d => {
points[d.data.name] = [
[x(d.x0), y(d.y0)],
[x(d.x1), y(d.y1)]
];
});
return d => points[d.id];
},
/**
* Redraw treemap
* @param {boolean} withTransition With or without transition
* @returns {Array} Selections
* @private
*/
redrawTreemap(withTransition?: boolean): d3Selection[] {
const $$ = this;
const {$el, state: {current: {width, height}}} = $$;
// update defs
$el.defs.select("rect")
.attr("width", width)
.attr("height", height);
return [
$$.$T($el.treemap, withTransition, getRandom())
.call(position.bind($$), $el.treemap.datum())
];
},
/**
* Get treemap data label format function
* @param {object} d Data object
* @returns {Function}
* @private
*/
treemapDataLabelFormat(d: IDataRow): Function {
const $$ = this;
const {config} = $$;
const {id, value} = d;
const format = config.treemap_label_format;
const ratio = $$.getRatio("treemap", d);
const percentValue = (ratio * 100).toFixed(2);
const meetLabelThreshold = config.treemap_label_show && $$.meetsLabelThreshold(
ratio,
"treemap"
) ?
null :
"0";
return function(node) {
node.style("opacity", meetLabelThreshold);
return isFunction(format) ?
format.bind($$.api)(value, ratio, id) :
`${id}\n${percentValue}%`;
};
}
};