UNPKG

billboard.js

Version:

Re-usable easy interface JavaScript chart library, based on D3 v4+

243 lines (242 loc) • 9.87 kB
/*! * 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.1.1 */ import { isDefined, isEmpty, isNumber, isUndefined } from "../../module/util/type-checks.js"; import { getMinMax, getRange, toArray } from "../../module/util/object.js"; import { getBoundingRect, getPathBox, setTextValue } from "../../module/util/dom.js"; import { $AXIS, $COMMON, $LEVEL, $RADAR, $SHAPE, $TEXT } from "../../config/classes.js"; import { KEY } from "../../module/Cache.js"; import { select } from "d3-selection"; //#region src/ChartInternal/shape/radar.ts /** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ /** * Get the position value * @param {boolean} isClockwise If the direction is clockwise * @param {string} type Coordinate type 'x' or 'y' * @param {number} edge Number of edge * @param {number} pos The indexed position * @param {number} range Range value * @param {number} ratio Ratio value * @returns {number} * @private */ function _getPosition(isClockwise, type, edge, pos, range, ratio) { const index = isClockwise && pos > 0 ? edge - pos : pos; return range * (1 - ratio * (type === "x" ? Math.sin : Math.cos)(index * (2 * Math.PI) / edge)); } const cacheKeyPoints = KEY.radarPoints; const cacheKeyTextWidth = KEY.radarTextWidth; var radar_default = { initRadar() { const $$ = this; const { config, state: { current }, $el } = $$; if ($$.hasType("radar")) { $el.radar = $el.main.select(`.${$COMMON.chart}`).append("g").attr("class", $RADAR.chartRadars); $el.radar.levels = $el.radar.append("g").attr("class", $LEVEL.levels); $el.radar.axes = $el.radar.append("g").attr("class", $AXIS.axis); $el.radar.shapes = $el.radar.append("g").attr("class", $SHAPE.shapes); current.dataMax = config.radar_axis_max || $$.getMinMaxData().max[0].value; if (config.radar_axis_text_show) { config.interaction_enabled && $$.bindRadarEvent(); $$.updateRadarLevel(); $$.updateRadarAxes(); } } }, getRadarSize() { const { config, state: { arcWidth, arcHeight } } = this; const size = (Math.min(arcWidth, arcHeight) - (config.axis_x_categories.length < 4 ? -20 : 10)) / 2; return [size, size]; }, updateTargetsForRadar(targets) { const $$ = this; const { config } = $$; if (isEmpty(config.axis_x_categories)) config.axis_x_categories = getRange(0, getMinMax("max", targets.map((v) => v.values.length))); $$.generateRadarPoints(); }, getRadarPosition(type, index, range, ratio) { const $$ = this; const { config } = $$; const [width, height] = $$.getRadarSize(); const edge = config.axis_x_categories.length; const isClockwise = config.radar_direction_clockwise; const pos = toArray(type).map((v) => _getPosition(isClockwise, v, edge, index, isDefined(range) ? range : type === "x" ? width : height, isNumber(ratio) ? ratio : config.radar_size_ratio)); return pos.length === 1 ? pos[0] : pos; }, /** * Generate data points * @private */ generateRadarPoints() { const $$ = this; const targets = $$.data.targets; const [width, height] = $$.getRadarSize(); const points = $$.cache.get(cacheKeyPoints) || {}; const size = points._size; if (!size || size.width !== width && size.height !== height) { targets.forEach((d) => { points[d.id] = d.values.map((v, i) => $$.getRadarPosition(["x", "y"], i, void 0, $$.getRatio("radar", v))); }); points._size = { width, height }; $$.cache.add(cacheKeyPoints, points); } }, redrawRadar() { const $$ = this; const { radar, main } = $$.$el; const translate = $$.getTranslate("radar"); if (translate) { radar.attr("transform", translate); main.select(`.${$TEXT.chartTexts}`).attr("transform", translate); $$.generateRadarPoints(); $$.updateRadarLevel(); $$.updateRadarAxes(); $$.updateRadarShape(); } }, generateGetRadarPoints() { const points = this.cache.get(cacheKeyPoints); return (d, i) => { const point = points[d.id][i]; return [ point, point, point, point ]; }; }, updateRadarLevel() { const $$ = this; const { config, state, $el: { radar } } = $$; const [width, height] = $$.getRadarSize(); const depth = config.radar_level_depth; const edge = config.axis_x_categories.length; const showText = config.radar_level_text_show; const radarLevels = radar.levels; const levelData = getRange(0, depth); const radius = config.radar_size_ratio * Math.min(width, height); const levelRatio = levelData.map((l) => radius * ((l + 1) / depth)); const levelTextFormat = (config.radar_level_text_format || function() {}).bind($$.api); const points = levelData.map((v) => { const range = levelRatio[v]; return getRange(0, edge).map((i) => $$.getRadarPosition(["x", "y"], i, range, 1).join(",")).join(" "); }); const level = radarLevels.selectAll(`.${$LEVEL.level}`).data(levelData); level.exit().remove(); const levelEnter = level.enter().append("g").attr("class", (d, i) => `${$LEVEL.level} ${$LEVEL.level}-${i}`); levelEnter.append("polygon").style("visibility", config.radar_level_show ? null : "hidden"); if (showText) { if (radarLevels.select("text").empty()) radarLevels.append("text").attr("dx", "-.5em").attr("dy", "-.7em").style("text-anchor", "end").text(() => levelTextFormat(0)); levelEnter.append("text").attr("dx", "-.5em").style("text-anchor", "end").text((d) => levelTextFormat(state.current.dataMax / levelData.length * (d + 1))); } levelEnter.merge(level).attr("transform", (d) => `translate(${width - levelRatio[d]}, ${height - levelRatio[d]})`).selectAll("polygon").attr("points", (d) => points[d]); if (showText) radarLevels.selectAll("text").attr("x", (d) => isUndefined(d) ? width : points[d].split(",")[0]).attr("y", (d) => isUndefined(d) ? height : 0); }, updateRadarAxes() { const $$ = this; const { config, $el: { radar } } = $$; const [width, height] = $$.getRadarSize(); const categories = config.axis_x_categories; let axis = radar.axes.selectAll("g").data(categories); axis.exit().remove(); const axisEnter = axis.enter().append("g").attr("class", (d, i) => `${$AXIS.axis}-${i}`); config.radar_axis_line_show && axisEnter.append("line"); config.radar_axis_text_show && axisEnter.append("text"); axis = axisEnter.merge(axis); if (config.radar_axis_line_show) axis.select("line").attr("x1", width).attr("y1", height).attr("x2", (d, i) => $$.getRadarPosition("x", i)).attr("y2", (d, i) => $$.getRadarPosition("y", i)); if (config.radar_axis_text_show) { const { x = 0, y = 0 } = config.radar_axis_text_position; const textWidth = $$.cache.get(cacheKeyTextWidth) || 0; axis.select("text").style("text-anchor", "middle").attr("dy", ".5em").call((selection) => { selection.each(function(d) { setTextValue(select(this), String(d), [-.6, 1.2]); }); }).datum((d, i) => ({ index: i })).attr("transform", function(d) { if (isUndefined(this.width)) this.width = getBoundingRect(this, true).width / 2; let posX = $$.getRadarPosition("x", d.index, void 0, 1); let posY = Math.round($$.getRadarPosition("y", d.index, void 0, 1)); if (posX > width) posX += this.width + x; else if (Math.round(posX) < width) posX -= this.width + x; if (posY > height) { if (posY / 2 === height && this.firstChild.tagName === "tspan") this.firstChild.setAttribute("dy", "0em"); posY += y; } else if (posY < height) posY -= y; return `translate(${posX} ${posY})`; }); if (!textWidth) { const widths = [radar.axes, radar.levels].map((v) => getPathBox(v.node()).width); if (widths.every((v) => v > 0)) $$.cache.add(cacheKeyTextWidth, widths[0] - widths[1]); } } }, bindRadarEvent() { const $$ = this; const { config, state, $el: { radar, svg } } = $$; const focusOnly = $$.isPointFocusOnly(); const { inputType, transiting } = state; const isMouse = inputType === "mouse"; const hide = (event) => { state.event = event; if (!config.interaction_onout) return; const index = $$.getDataIndexFromEvent(event); const noIndex = isUndefined(index); if (isMouse || noIndex) { $$.hideTooltip(); focusOnly ? $$.hideCircleFocus() : $$.unexpandCircles(); if (isMouse) $$.setOverOut(false, index); else if (noIndex) $$.callOverOutForTouch(); } }; radar.axes.on(isMouse ? "mouseover " : "touchstart", (event) => { if (transiting) return; state.event = event; const index = $$.getDataIndexFromEvent(event); $$.selectRectForSingle(svg.node(), index); isMouse ? $$.setOverOut(true, index) : $$.callOverOutForTouch(index); }, isMouse ? void 0 : { passive: true }).on("mouseout", isMouse ? hide : null); if (!isMouse) svg.on("touchstart", hide, { passive: true }); }, updateRadarShape() { const $$ = this; const targets = $$.data.targets.filter((d) => $$.isRadarType(d)); const points = $$.cache.get(cacheKeyPoints); const areas = $$.$el.radar.shapes.selectAll("polygon").data($$.filterNullish(targets)); const areasEnter = areas.enter().append("g").attr("class", $$.getChartClass("Radar")); $$.$T(areas.exit()).remove(); areasEnter.append("polygon").merge(areas).style("fill", $$.color).style("stroke", $$.color).attr("points", (d) => points[d.id].join(" ")); $$.updateTargetForCircle(targets, areasEnter); }, /** * Get data point x coordinate * @param {object} d Data object * @returns {number} * @private */ radarCircleX(d) { return this.cache.get(cacheKeyPoints)[d.id][d.index][0]; }, /** * Get data point y coordinate * @param {object} d Data object * @returns {number} * @private */ radarCircleY(d) { return this.cache.get(cacheKeyPoints)[d.id][d.index][1]; } }; //#endregion export { radar_default as default };