billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
243 lines (197 loc) • 7.07 kB
text/typescript
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
import {mouse as d3Mouse} from "d3-selection";
import CLASS from "../../config/classes";
import {getRandom, getRectSegList, isNumber, isObjectType, isValue} from "../../module/util";
export default {
initBar(): void {
const {$el} = this;
$el.bar = $el.main.select(`.${CLASS.chart}`)
// should positioned at the beginning of the shape node to not overlap others
.insert("g", ":first-child")
.attr("class", CLASS.chartBars);
},
updateTargetsForBar(targets): void {
const $$ = this;
const {config, $el} = $$;
const classChartBar = $$.classChartBar.bind($$);
const classBars = $$.classBars.bind($$);
const classFocus = $$.classFocus.bind($$);
if (!$el.bar) {
$$.initBar();
}
const mainBarUpdate = $$.$el.main.select(`.${CLASS.chartBars}`)
.selectAll(`.${CLASS.chartBar}`)
.data(targets)
.attr("class", d => classChartBar(d) + classFocus(d));
const mainBarEnter = mainBarUpdate.enter().append("g")
.attr("class", classChartBar)
.style("opacity", "0")
.style("pointer-events", "none");
// Bars for each data
mainBarEnter.append("g")
.attr("class", classBars)
.style("cursor", d => (config.data_selection_isselectable.bind($$.api)(d) ? "pointer" : null));
},
updateBar(durationForExit: number): void {
const $$ = this;
const {$el} = $$;
const barData = $$.barData.bind($$);
const classBar = $$.classBar.bind($$);
const initialOpacity = $$.initialOpacity.bind($$);
$el.bar = $el.main.selectAll(`.${CLASS.bars}`).selectAll(`.${CLASS.bar}`)
.data(barData);
$el.bar.exit().transition()
.duration(durationForExit)
.style("opacity", "0")
.remove();
$el.bar = $el.bar.enter().append("path")
.attr("class", classBar)
.style("fill", $$.color)
.merge($el.bar)
.style("opacity", initialOpacity);
},
redrawBar(drawBar, withTransition?: boolean) {
const {bar} = this.$el;
return [
(withTransition ? bar.transition(getRandom()) : bar)
.attr("d", drawBar)
.style("fill", this.color)
.style("opacity", "1")
];
},
getBarW(axis, barTargetsNum: number): number {
const $$ = this;
const {config, scale} = $$;
const maxDataCount = $$.getMaxDataCount();
const isGrouped = config.data_groups.length;
const tickInterval = (scale.zoom || $$) && !$$.axis.isCategorized() ?
$$.xx(scale.subX.domain()[1]) / maxDataCount : axis.tickInterval(maxDataCount);
let result;
const getWidth = (id?: string) => {
const width = id ? config.bar_width[id] : config.bar_width;
const ratio = id ? width.ratio : config.bar_width_ratio;
const max = id ? width.max : config.bar_width_max;
const w = isNumber(width) ?
width : barTargetsNum ? (tickInterval * ratio) / barTargetsNum : 0;
return max && w > max ? max : w;
};
result = getWidth();
if (!isGrouped && isObjectType(config.bar_width)) {
result = {width: result, total: []};
$$.filterTargetsToShow($$.data.targets).forEach(v => {
if (config.bar_width[v.id]) {
result[v.id] = getWidth(v.id);
result.total.push(result[v.id] || result.width);
}
});
}
return result;
},
getBars(i: number, id: string) {
const $$ = this;
const {main} = $$.$el;
const suffix = (isValue(i) ? `-${i}` : ``);
return (id ? main
.selectAll(`.${CLASS.bars}${$$.getTargetSelectorSuffix(id)}`) : main)
.selectAll(`.${CLASS.bar}${suffix}`);
},
expandBars(i: number, id: string, reset: boolean): void {
const $$ = this;
reset && $$.unexpandBars();
$$.getBars(i, id).classed(CLASS.EXPANDED, true);
},
unexpandBars(i: number): void {
this.getBars(i).classed(CLASS.EXPANDED, false);
},
generateDrawBar(barIndices, isSub?: boolean): Function {
const $$ = this;
const {config} = $$;
const getPoints = $$.generateGetBarPoints(barIndices, isSub);
const isRotated = config.axis_rotated;
const isGrouped = config.data_groups.length;
const barRadius = config.bar_radius;
const barRadiusRatio = config.bar_radius_ratio;
// get the bar radius
const getRadius = isNumber(barRadius) && barRadius > 0 ?
() => barRadius : (
isNumber(barRadiusRatio) ? w => w * barRadiusRatio : null
);
return (d, i) => {
// 4 points that make a bar
const points = getPoints(d, i);
// switch points if axis is rotated, not applicable for sub chart
const indexX = +isRotated;
const indexY = +!indexX;
const isNegative = d.value < 0;
const pathRadius = ["", ""];
let radius = 0;
if (getRadius && !isGrouped) {
const index = isRotated ? indexY : indexX;
const barW = points[2][index] - points[0][index];
radius = getRadius(barW);
const arc = `a${radius},${radius} ${isNegative ? `1 0 0` : `0 0 1`} `;
pathRadius[+!isRotated] = `${arc}${radius},${radius}`;
pathRadius[+isRotated] = `${arc}${[-radius, radius][isRotated ? "sort" : "reverse"]()}`;
isNegative && pathRadius.reverse();
}
// path string data shouldn't be containing new line chars
// https://github.com/naver/billboard.js/issues/530
const path = isRotated ?
`H${points[1][indexX] - radius} ${pathRadius[0]}V${points[2][indexY] - radius} ${pathRadius[1]}H${points[3][indexX]}` :
`V${points[1][indexY] + (isNegative ? -radius : radius)} ${pathRadius[0]}H${points[2][indexX] - radius} ${pathRadius[1]}V${points[3][indexY]}`;
return `M${points[0][indexX]},${points[0][indexY]}${path}z`;
};
},
generateGetBarPoints(barIndices, isSub?: boolean): Function {
const $$ = this;
const {config} = $$;
const axis = isSub ? $$.axis.subX : $$.axis.x;
const barTargetsNum = $$.getIndicesMax(barIndices) + 1;
const barW = $$.getBarW(axis, barTargetsNum);
const barX = $$.getShapeX(barW, barIndices, !!isSub);
const barY = $$.getShapeY(!!isSub);
const barOffset = $$.getShapeOffset($$.isBarType, barIndices, !!isSub);
const yScale = $$.getYScaleById.bind($$);
return (d, i) => {
const y0 = yScale.call($$, d.id)($$.getShapeYMin(d.id));
const offset = barOffset(d, i) || y0; // offset is for stacked bar chart
const width = isNumber(barW) ? barW : barW[d.id] || barW.width;
const posX = barX(d);
let posY = barY(d);
// fix posY not to overflow opposite quadrant
if (config.axis_rotated && (
(d.value > 0 && posY < y0) || (d.value < 0 && y0 < posY)
)) {
posY = y0;
}
posY -= (y0 - offset);
// 4 points that make a bar
return [
[posX, offset],
[posX, posY],
[posX + width, posY],
[posX + width, offset]
];
};
},
isWithinBar(that): boolean {
const mouse = d3Mouse(that);
const list = getRectSegList(that);
const [seg0, seg1] = list;
const x = Math.min(seg0.x, seg1.x);
const y = Math.min(seg0.y, seg1.y);
const offset = this.config.bar_sensitivity;
const {width, height} = that.getBBox();
const sx = x - offset;
const ex = x + width + offset;
const sy = y + height + offset;
const ey = y - offset;
return sx < mouse[0] &&
mouse[0] < ex &&
ey < mouse[1] &&
mouse[1] < sy;
}
};