billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
204 lines (201 loc) • 6.91 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.0.1
*/
import { select } from 'd3-selection';
import { $COMMON, $TREEMAP } from '../../config/classes.js';
import { meetsLabelThreshold } from '../internals/text.util.js';
import { getTreemapNodeRect } from './core/geometry.js';
import shapeTreemapCommon from './core/treemap.js';
import { isFunction } from '../../module/util/type-checks.js';
import { getRandom } from '../../module/util/object.js';
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
/**
* Get treemap elements' position
* @param {d3Selection} group Root selection
* @param {object} root Root data
* @private
*/
function position(group, root) {
const $$ = this;
group.selectAll("g")
.attr("transform", d => {
const rect = getTreemapNodeRect($$, d, root);
return `translate(${rect.x},${rect.y})`;
})
.select("rect")
.attr("width", d => getTreemapNodeRect($$, d, root).w)
.attr("height", d => getTreemapNodeRect($$, d, root).h);
}
var shapeTreemap = {
...shapeTreemapCommon,
initTreemap() {
const $$ = this;
const { $el, state: { current: { width, height }, clip, datetimeId } } = $$;
clip.id = `${datetimeId}-clip`;
$$.initTreemapLayout();
$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() {
const $$ = this;
const { $el, config, state } = $$;
const getTarget = event => {
const target = event.isTrusted ? event.target : state.eventReceiver.rect?.node();
let data;
if (target && /^rect$/i.test(target.tagName)) {
state.event = event;
data = select(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);
}
}, isTouch ? { passive: true } : undefined)
.on(isTouch ? "touchend" : "mouseout", event => {
const data = getTarget(event);
if (config.interaction_onout) {
$$.hideTooltip();
$$.setOverOut(false, data);
}
});
}
},
/**
* Update treemap data
* @param {Array} targets Data targets
* @private
*/
updateTargetsForTreemap(targets) {
const $$ = this;
const { $el: { treemap } } = $$;
const treemapData = [$$.getTreemapRoot(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) {
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() {
const $$ = this;
const { $el } = $$;
const points = {};
$el.treemap.selectAll("g").each(d => {
const rect = getTreemapNodeRect($$, d);
points[d.data.name] = [
[rect.x, rect.y],
[rect.x + rect.w, rect.y + rect.h]
];
});
return d => points[d.id];
},
/**
* Redraw treemap
* @param {boolean} withTransition With or without transition
* @returns {Array} Selections
* @private
*/
redrawTreemap(withTransition) {
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} Label formatter function
* @private
*/
treemapDataLabelFormat(d) {
const $$ = this;
const { $el: { treemap }, config, scale: { x, y } } = $$;
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.call($$, ratio, "treemap") ?
null :
"0";
// Get treemap dimensions for the specific data
const treemapNode = treemap.selectAll("g")
.filter(node => node.data.id === id)
.datum();
let width = 0;
let height = 0;
if (treemapNode) {
const { x0, x1, y0, y1 } = treemapNode;
width = x(x1) - x(x0);
height = y(y1) - y(y0);
}
return function (node) {
node.style("opacity", meetLabelThreshold);
return isFunction(format) ?
format.bind($$.api)(value, ratio, id, { width, height }) :
`${id}\n${percentValue}%`;
};
}
};
export { shapeTreemap as default };