UNPKG

billboard.js

Version:

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

719 lines (718 loc) • 28.6 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 { document as doc } from "../../module/browser.js"; import { isBoolean, isDefined, isEmpty, isFunction } from "../../module/util/type-checks.js"; import { sanitize } from "../../module/sanitize.js"; import { callFn, getOption, toMap, tplProcess } from "../../module/util/object.js"; import { $FOCUS, $GAUGE, $LEGEND } from "../../config/classes.js"; import { KEY } from "../../module/Cache.js"; import { namespaces, select } from "d3-selection"; //#region src/ChartInternal/internals/legend.ts /** * Copyright (c) 2017 ~ present NAVER Corp. * billboard.js project is licensed under the MIT license */ const LEGEND_TOUCH_TAP_THRESHOLD = 10; const LEGEND_TOUCH_CLICK_TIMEOUT = 750; /** * Get color string for given data id * Internal helper used only within this file * @param {string} id Data id * @returns {string} Color string * @private */ function _getLegendColor(id) { const $$ = this; const data = $$.getDataById(id); return $$.levelColor ? $$.levelColor(data.values[0].value) : $$.color(data); } /** * Get formatted text value * Internal helper used only within this file * @param {string} id Legend text id * @param {boolean} formatted Whether or not to format the text * @returns {string} Formatted legend text * @private */ function _getFormattedText(id, formatted = true) { const { config } = this; let text = config.data_names[id] ?? id; if (formatted && isFunction(config.legend_format)) text = config.legend_format(text, id !== text ? id : void 0); return text; } /** * Build a Map of legend items for fast O(1) lookup by ID * Internal helper used only within this file * @param {object} $$ ChartInternal context * @param {d3.selection} legendItems D3 selection of legend items * @private */ function _buildLegendItemMap($$, legendItems) { if (!legendItems || legendItems.empty()) return; const allItems = $$.$el.legend?.selectAll(`.${$LEGEND.legendItem}`); if (allItems && !allItems.empty()) legendItems = allItems; const items = []; legendItems.each(function(id) { items.push({ id, node: this }); }); const itemMap = toMap(items, (item) => item.id, (item) => item.node); $$.cache.add(KEY.legendItemMap, itemMap); } /** * Get touch point from a touch event. * @param {TouchEvent} event Touch event * @returns {Touch | undefined} Touch point * @private */ function _getLegendTouchPoint(event) { return event.changedTouches?.[0] || event.touches?.[0]; } /** * Store the touch start position for legend tap detection. * @param {object} $$ ChartInternal context * @param {string} id Legend data id * @param {TouchEvent} event Touch event * @private */ function _setLegendTouchStart($$, id, event) { const touch = _getLegendTouchPoint(event); $$.state.legendTouch = touch ? { id, x: touch.clientX, y: touch.clientY, moved: false } : null; } /** * Update whether the current legend touch moved beyond tap tolerance. * @param {object} $$ ChartInternal context * @param {TouchEvent} event Touch event * @private */ function _updateLegendTouchMove($$, event) { const start = $$.state.legendTouch; const touch = start && _getLegendTouchPoint(event); if (touch) start.moved = start.moved || Math.abs(touch.clientX - start.x) > LEGEND_TOUCH_TAP_THRESHOLD || Math.abs(touch.clientY - start.y) > LEGEND_TOUCH_TAP_THRESHOLD; } /** * Determine whether a touch sequence is a legend tap. * @param {object} $$ ChartInternal context * @param {string} id Legend data id * @param {TouchEvent} event Touch event * @returns {boolean} Whether the touch sequence is a tap * @private */ function _isLegendTouchTap($$, id, event) { _updateLegendTouchMove($$, event); const start = $$.state.legendTouch; $$.state.legendTouch = null; return !!start && start.id === id && !start.moved; } /** * Mark a touch legend tap so the following compatibility click can be skipped. * @param {object} $$ ChartInternal context * @param {string} id Legend data id * @private */ function _markLegendTouchClick($$, id) { $$.state.legendLastTouchClickId = id; $$.state.legendLastTouchClickTime = Date.now(); } /** * Check if a click duplicates a recent touch legend tap. * @param {object} $$ ChartInternal context * @param {string} id Legend data id * @returns {boolean} Whether the click is duplicate * @private */ function _isDuplicateLegendTouchClick($$, id) { const { state } = $$; const duplicate = state.legendLastTouchClickId === id && Date.now() - (state.legendLastTouchClickTime || 0) < LEGEND_TOUCH_CLICK_TIMEOUT; if (duplicate) { state.legendLastTouchClickId = null; state.legendLastTouchClickTime = 0; } return duplicate; } /** * Get touch listener passive option following interaction.inputType.touch.preventDefault. * @param {object} $$ ChartInternal context * @returns {object} Touch listener option * @private */ function _getLegendTouchOption($$) { const preventDefault = $$.config.interaction_inputType_touch?.preventDefault; return { passive: !(isBoolean(preventDefault) && preventDefault || false) && (!isNaN(preventDefault) && preventDefault || null) === null }; } var legend_default = { /** * Initialize the legend. * @private */ initLegend() { const $$ = this; const { config, $el } = $$; $$.legendItemTextBox = {}; $$.state.legendHasRendered = false; if ($$.state.isCanvasMode) { if (config.legend_show) $$.updateHtmlLegend?.(); else $$.state.hiddenLegendIds = new Set($$.mapToIds($$.data.targets)); return; } if (config.legend_show) { if (!config.legend_contents_bindto) $el.legend = $$.$el.svg.append("g").classed($LEGEND.legend, true).attr("transform", $$.getTranslate("legend")); $$.updateLegend(); } else $$.state.hiddenLegendIds = new Set($$.mapToIds($$.data.targets)); }, /** * Update legend element * @param {Array} targetIds ID's of target * @param {object} options withTransform : Whether to use the transform property / withTransitionForTransform: Whether transition is used when using the transform property / withTransition : whether or not to transition. * @param {object} transitions Return value of the generateTransitions * @private */ updateLegend(targetIds, options, transitions) { const $$ = this; const { config, state, scale, $el } = $$; const optionz = options || { withTransform: false, withTransitionForTransform: false, withTransition: false }; optionz.withTransition = getOption(optionz, "withTransition", true); optionz.withTransitionForTransform = getOption(optionz, "withTransitionForTransform", true); if (config.legend_contents_bindto && config.legend_contents_template) $$.updateLegendTemplate(); else if (!state.hasTreemap) $$.updateLegendElement(targetIds || $$.mapToIds($$.data.targets), optionz); $el.legend?.selectAll(`.${$LEGEND.legendItem}`).classed($LEGEND.legendItemHidden, function(id) { const hide = !$$.isTargetToShow(id); if (hide) this.style.opacity = null; return hide; }); $$.updateScales(false, !scale.zoom); $$.updateSvgSize(); $$.transformAll(optionz.withTransitionForTransform, transitions); state.legendHasRendered = true; }, /** * Update legend using template option * @private */ updateLegendTemplate() { const $$ = this; const { config, $el } = $$; const wrapper = select(config.legend_contents_bindto); const template = config.legend_contents_template; if (!wrapper.empty()) { const targets = $$.mapToIds($$.data.targets); const ids = []; let html = ""; targets.forEach((v) => { const content = isFunction(template) ? sanitize(template.call($$.api, v, $$.color(v), $$.api.data(v)[0].values)) : tplProcess(template, { COLOR: $$.color(v), TITLE: v }); if (content) { ids.push(v); html += content; } }); const legendItem = wrapper.html(html).selectAll(function() { return this.childNodes; }).data(ids); $$.setLegendItem(legendItem); $el.legend = wrapper; } }, /** * Update the size of the legend. * @param {Obejct} size Size object * @private */ updateSizeForLegend(size) { const $$ = this; const { config, state: { isLegendTop, isLegendLeft, isLegendRight, isLegendInset, current } } = $$; const { width, height } = size; const insetLegendPosition = { top: isLegendTop ? $$.getCurrentPaddingByDirection("top") + config.legend_inset_y + 5.5 : current.height - height - $$.getCurrentPaddingByDirection("bottom") - config.legend_inset_y, left: isLegendLeft ? $$.getCurrentPaddingByDirection("left") + config.legend_inset_x + .5 : current.width - width - $$.getCurrentPaddingByDirection("right") - config.legend_inset_x + .5 }; $$.state.margin3 = { top: isLegendRight ? 0 : isLegendInset ? insetLegendPosition.top : current.height - height, right: NaN, bottom: 0, left: isLegendRight ? current.width - width : isLegendInset ? insetLegendPosition.left : 0 }; }, /** * Transform Legend * @param {boolean} withTransition whether or not to transition. * @private */ transformLegend(withTransition) { const $$ = this; const { $el: { legend }, $T } = $$; $T(legend, withTransition).attr("transform", $$.getTranslate("legend")); }, /** * Update legend item color * @param {string} id Corresponding data ID value * @param {string} color Color value * @private */ updateLegendItemColor(id, color) { const $$ = this; const { legend } = $$.$el; if (legend) { const legendItem = $$.getLegendItemById(id); if (legendItem) select(legendItem).select("line").style("stroke", color); } }, /** * Get the width of the legend * @returns {number} width * @private */ getLegendWidth() { const $$ = this; const { current: { width }, isLegendRight, isLegendInset, legendItemWidth, legendStep } = $$.state; return $$.config.legend_show ? isLegendRight || isLegendInset ? legendItemWidth * (legendStep + 1) : width : 0; }, /** * Get the height of the legend * @returns {number} height * @private */ getLegendHeight() { const $$ = this; const { current, isLegendRight, legendItemHeight, legendStep } = $$.state; return $$.config.legend_show ? isLegendRight ? current.height : Math.max($$.config.padding?.mode === "fit" ? 10 : 20, legendItemHeight) * (legendStep + 1) : 0; }, /** * Get the opacity of the legend that is unfocused * @param {d3.selection} legendItem Legend item node * @returns {string|null} opacity * @private */ opacityForUnfocusedLegend(legendItem) { return legendItem.classed($LEGEND.legendItemHidden) ? null : "0.3"; }, /** * Toggles the focus of the legend * @param {Array} targetIds ID's of target * @param {boolean} focus whether or not to focus. * @private */ toggleFocusLegend(targetIds, focus) { const $$ = this; const { $el: { legend }, $T } = $$; const targetIdz = $$.mapToTargetIds(targetIds); legend && $T(legend.selectAll(`.${$LEGEND.legendItem}`).filter((id) => targetIdz.indexOf(id) >= 0).classed($FOCUS.legendItemFocused, focus)).style("opacity", function() { return focus ? null : $$.opacityForUnfocusedLegend.call($$, select(this)); }); }, /** * Revert the legend to its default state * @private */ revertLegend() { const { $el: { legend }, $T } = this; legend && $T(legend.selectAll(`.${$LEGEND.legendItem}`).classed($FOCUS.legendItemFocused, false)).style("opacity", null); }, /** * Shows the legend * @param {Array} targetIds ID's of target * @private */ showLegend(targetIds) { const $$ = this; const { config, $el, $T } = $$; if (!config.legend_show) { config.legend_show = true; $el.legend ? $el.legend.style("visibility", null) : $$.initLegend(); !$$.state.legendHasRendered && $$.updateLegend(); } $$.removeHiddenLegendIds(targetIds); $T($el.legend.selectAll($$.selectorLegends(targetIds)).style("visibility", null)).style("opacity", null); }, /** * Hide the legend * @param {Array} targetIds ID's of target * @private */ hideLegend(targetIds) { const $$ = this; const { config, $el: { legend } } = $$; if (config.legend_show && isEmpty(targetIds)) { config.legend_show = false; legend?.style("visibility", "hidden"); } $$.addHiddenLegendIds(targetIds); legend?.selectAll($$.selectorLegends(targetIds)).style("opacity", "0").style("visibility", "hidden"); }, /** * Get legend item textbox dimension * @param {string} id Data ID * @param {HTMLElement|d3.selection} textElement Text node element * @returns {object} Bounding rect * @private */ getLegendItemTextBox(id, textElement) { const $$ = this; const { cache, state } = $$; let data; const cacheKey = KEY.legendItemTextBox; if (id) { data = !state.redrawing && cache.get(cacheKey) || {}; if (!data[id]) { data[id] = $$.getTextRect(textElement, $LEGEND.legendItem); cache.add(cacheKey, data); } data = data[id]; } return data; }, /** * Set legend item style & bind events * @param {d3.selection} item Item node * @private */ setLegendItem(item) { const $$ = this; const { $el, api, config, state } = $$; const isTouch = state.inputType === "touch"; const hasGauge = $$.hasType("gauge"); const useCssRule = config.boost_useCssRule; const interaction = config.legend_item_interaction; const eventType = interaction.dblclick ? "dblclick" : "click"; const hasClickInteraction = interaction || isFunction(config.legend_item_onclick); const touchOption = isTouch ? _getLegendTouchOption($$) : void 0; const handleLegendToggle = function(event, id) { if (!callFn(config.legend_item_onclick, api, id, !state.hiddenTargetIds.has(id))) { const { altKey, type } = event; const selected = select(this); if (type === "dblclick" || altKey) { if (state.hiddenTargetIds.size && !selected.classed($LEGEND.legendItemHidden)) api.show(); else { api.hide(); api.show(id); } } else { api.toggle(id); selected.classed($FOCUS.legendItemFocused, false); } } if (isTouch) { $$.hideTooltip(); $$.hideGridFocus?.(true); } }; item.attr("class", function(id) { const node = select(this); return (!node.empty() && node.attr("class") || "") + $$.generateClass($LEGEND.legendItem, id); }).style("visibility", (id) => $$.isLegendToShow(id) ? null : "hidden"); if (config.interaction_enabled) { if (useCssRule) [ [`.${$LEGEND.legendItem}`, "cursor:pointer"], [`.${$LEGEND.legendItem} text`, "pointer-events:none"], [`.${$LEGEND.legendItemPoint} text`, "pointer-events:none"], [`.${$LEGEND.legendItemTile}`, "pointer-events:none"], [`.${$LEGEND.legendItemEvent}`, "fill-opacity:0"] ].forEach((v) => { const [selector, props] = v; $$.setCssRule(false, selector, [props])($el.legend); }); item.on(eventType, hasClickInteraction ? function(event, id) { if (isTouch && event.type === "click" && _isDuplicateLegendTouchClick($$, id)) return; handleLegendToggle.call(this, event, id); } : null); isTouch && eventType === "click" && hasClickInteraction && item.on("touchstart", function(event, id) { _setLegendTouchStart($$, id, event); }, touchOption).on("touchmove", (event) => { _updateLegendTouchMove($$, event); }, touchOption).on("touchend", function(event, id) { if (_isLegendTouchTap($$, id, event)) { _markLegendTouchClick($$, id); handleLegendToggle.call(this, event, id); } }, touchOption); !isTouch && item.on("mouseout", interaction || isFunction(config.legend_item_onout) ? function(event, id) { if (!callFn(config.legend_item_onout, api, id, !state.hiddenTargetIds.has(id))) { select(this).classed($FOCUS.legendItemFocused, false); if (hasGauge) $$.undoMarkOverlapped($$, `.${$GAUGE.gaugeValue}`); $$.api.revert(); } } : null).on("mouseover", interaction || isFunction(config.legend_item_onover) ? function(event, id) { if (!callFn(config.legend_item_onover, api, id, !state.hiddenTargetIds.has(id))) { select(this).classed($FOCUS.legendItemFocused, true); if (hasGauge) $$.markOverlapped(id, $$, `.${$GAUGE.gaugeValue}`); if (!state.transiting && $$.isTargetToShow(id)) api.focus(id); } } : null); !item.empty() && item.on("click mouseout mouseover") && item.style("cursor", $$.getStylePropValue("pointer")); } _buildLegendItemMap($$, item); }, /** * Get legend item node by ID using cached Map for O(1) lookup * Falls back to D3 selection if map is not available * @param {string} id Data ID * @returns {HTMLElement | null} Legend item node * @private */ getLegendItemById(id) { const $$ = this; const itemMap = $$.cache.get(KEY.legendItemMap); if (itemMap && itemMap instanceof Map) return itemMap.get(id) || null; return ($$.$el.legend?.selectAll(`.${$LEGEND.legendItem}`).filter((d) => d === id))?.node() || null; }, /** * Update the legend * @param {Array} targetIds ID's of target * @param {object} options withTransform : Whether to use the transform property / withTransitionForTransform: Whether transition is used when using the transform property / withTransition : whether or not to transition. * @private */ updateLegendElement(targetIds, options) { const $$ = this; const { config, state, $el: { legend }, $T } = $$; const isRectangle = config.legend_item_tile_type !== "circle"; const legendItemR = config.legend_item_tile_r; const itemTileSize = { width: isRectangle ? config.legend_item_tile_width : legendItemR * 2, height: isRectangle ? config.legend_item_tile_height : legendItemR * 2 }; const dimension = { padding: { top: 4, right: 10 }, max: { width: 0, height: 0 }, posMin: 10, step: 0, tileWidth: itemTileSize.width + 5, totalLength: 0 }; const sizes = { offsets: {}, widths: {}, heights: {}, margins: [0], steps: {} }; let xForLegend; let yForLegend; let background; const targetIdz = targetIds.filter((id) => !isDefined(config.data_names[id]) || config.data_names[id] !== null); const withTransition = options.withTransition; const isLegendRightOrInset = state.isLegendRight || state.isLegendInset; const getFormattedText = _getFormattedText.bind($$); const updatePositions = $$.getUpdateLegendPositions(targetIdz, dimension, sizes, isLegendRightOrInset); if (state.isLegendInset) { dimension.step = config.legend_inset_step ? config.legend_inset_step : targetIdz.length; state.legendStep = dimension.step; } if (state.isLegendRight) { xForLegend = (id) => dimension.max.width * sizes.steps[id]; yForLegend = (id) => sizes.margins[sizes.steps[id]] + sizes.offsets[id]; } else if (state.isLegendInset) { xForLegend = (id) => dimension.max.width * sizes.steps[id] + 10; yForLegend = (id) => sizes.margins[sizes.steps[id]] + sizes.offsets[id]; } else { xForLegend = (id) => sizes.margins[sizes.steps[id]] + sizes.offsets[id]; yForLegend = (id) => dimension.max.height * sizes.steps[id]; } const posFn = { xText: (id, i) => xForLegend(id, i) + 4 + itemTileSize.width, xRect: (id, i) => xForLegend(id, i), x1Tile: (id, i) => xForLegend(id, i) - 2, x2Tile: (id, i) => xForLegend(id, i) - 2 + itemTileSize.width, yText: (id, i) => yForLegend(id, i) + 9, yRect: (id, i) => yForLegend(id, i) - 5, yTile: (id, i) => yForLegend(id, i) + 4 }; $$.generateLegendItem(targetIdz, itemTileSize, updatePositions, posFn, isLegendRightOrInset, getFormattedText); background = legend.select(`.${$LEGEND.legendBackground} rect`); if (state.isLegendInset && dimension.max.width > 0 && background.size() === 0) background = legend.insert("g", `.${$LEGEND.legendItem}`).attr("class", $LEGEND.legendBackground).append("rect"); if (config.legend_tooltip) legend.selectAll("title").data(targetIdz).text((id) => getFormattedText(id, false)); $T(legend.selectAll("text").data(targetIdz).text((id) => getFormattedText(id)).each(function(id, i) { updatePositions(this, id, i); }), withTransition).attr("x", posFn.xText).attr("y", posFn.yText); $T(legend.selectAll(`rect.${$LEGEND.legendItemEvent}`).data(targetIdz), withTransition).attr("width", (id) => sizes.widths[id]).attr("height", (id) => sizes.heights[id]).attr("x", posFn.xRect).attr("y", posFn.yRect); $$.updateLegendItemPos(targetIdz, withTransition, posFn); if (background) $T(background, withTransition).attr("height", $$.getLegendHeight() - 12).attr("width", dimension.max.width * (dimension.step + 1) + 10); state.legendItemWidth = dimension.max.width; state.legendItemHeight = dimension.max.height; state.legendStep = dimension.step; }, /** * Get position update function * @param {Array} targetIdz Data ids * @param {object} dimension Dimension object * @param {object} sizes Size object * @param {boolean} isLegendRightOrInset Whether legend is right or inset * @returns {function} Update position function * @private */ getUpdateLegendPositions(targetIdz, dimension, sizes, isLegendRightOrInset) { const $$ = this; const { config, state } = $$; return function(textElement, id, index) { const reset = index === 0; const isLast = index === targetIdz.length - 1; const box = $$.getLegendItemTextBox(id, textElement); const itemWidth = box.width + dimension.tileWidth + (isLast && !isLegendRightOrInset ? 0 : dimension.padding.right) + config.legend_padding; const itemHeight = box.height + dimension.padding.top; const itemLength = isLegendRightOrInset ? itemHeight : itemWidth; const areaLength = isLegendRightOrInset ? $$.getLegendHeight() : $$.getLegendWidth(); let margin; const updateValues = function(id2, withoutStep) { if (!withoutStep) { margin = (areaLength - dimension.totalLength - itemLength) / 2; if (margin < dimension.posMin) { margin = (areaLength - itemLength) / 2; dimension.totalLength = 0; dimension.step++; } } sizes.steps[id2] = dimension.step; sizes.margins[dimension.step] = state.isLegendInset ? 10 : margin; sizes.offsets[id2] = dimension.totalLength; dimension.totalLength += itemLength; }; if (reset) { dimension.totalLength = 0; dimension.step = 0; dimension.max.width = 0; dimension.max.height = 0; } if (config.legend_show && !$$.isLegendToShow(id)) { sizes.widths[id] = 0; sizes.heights[id] = 0; sizes.steps[id] = 0; sizes.offsets[id] = 0; return; } sizes.widths[id] = itemWidth; sizes.heights[id] = itemHeight; if (!dimension.max.width || itemWidth >= dimension.max.width) dimension.max.width = itemWidth; if (!dimension.max.height || itemHeight >= dimension.max.height) dimension.max.height = itemHeight; const maxLength = isLegendRightOrInset ? dimension.max.height : dimension.max.width; if (config.legend_equally) { for (const id2 in sizes.widths) sizes.widths[id2] = dimension.max.width; for (const id2 in sizes.heights) sizes.heights[id2] = dimension.max.height; margin = (areaLength - maxLength * targetIdz.length) / 2; if (margin < dimension.posMin) { dimension.totalLength = 0; dimension.step = 0; targetIdz.forEach((id2) => updateValues(id2)); } else updateValues(id, true); } else updateValues(id); }; }, /** * Generate legend item elements * @param {Array} targetIdz Data ids * @param {object} itemTileSize Item tile size {width, height} * @param {function} updatePositions Update position function * @param {object} posFn Position functions * @param {boolean} isLegendRightOrInset Whether legend is right or inset * @param {function} getFormattedText Bound text formatter function * @private */ generateLegendItem(targetIdz, itemTileSize, updatePositions, posFn, isLegendRightOrInset, getFormattedText) { const $$ = this; const { config, state, $el: { legend } } = $$; const usePoint = config.legend_usePoint; const legendItemR = config.legend_item_tile_r; const legendType = config.legend_item_tile_type; const isRectangle = legendType !== "circle"; const pos = -200; const l = legend.selectAll(`.${$LEGEND.legendItem}`).data(targetIdz).enter().append("g"); $$.setLegendItem(l); if (config.legend_tooltip) l.append("title").text((id) => id); l.append("text").text((id) => getFormattedText(id)).each(function(id, i) { updatePositions(this, id, i); }).style("pointer-events", $$.getStylePropValue("none")).attr("x", isLegendRightOrInset ? posFn.xText : pos).attr("y", isLegendRightOrInset ? pos : posFn.yText); l.append("rect").attr("class", $LEGEND.legendItemEvent).style("fill-opacity", $$.getStylePropValue("0")).attr("x", isLegendRightOrInset ? posFn.xRect : pos).attr("y", isLegendRightOrInset ? pos : posFn.yRect); if (usePoint) { const ids = []; const pattern = $$.getValidPointPattern(); l.append((d) => { ids.indexOf(d) === -1 && ids.push(d); let point = pattern[ids.indexOf(d) % pattern.length]; if (point === "rectangle") point = "rect"; return doc.createElementNS(namespaces.svg, "hasValidPointType" in $$ && $$.hasValidPointType(point) ? point : "use"); }).attr("class", $LEGEND.legendItemPoint).style("fill", _getLegendColor.bind($$)).style("pointer-events", $$.getStylePropValue("none")).attr("href", (data, idx, selection) => { const nodeName = selection[idx].nodeName.toLowerCase(); const id = $$.getTargetSelectorSuffix(data); return nodeName === "use" ? `#${state.datetimeId}-point${id}` : void 0; }); } else l.append(isRectangle ? "line" : legendType).attr("class", $LEGEND.legendItemTile).style("stroke", _getLegendColor.bind($$)).style("pointer-events", $$.getStylePropValue("none")).call((selection) => { if (legendType === "circle") selection.attr("r", legendItemR).style("fill", _getLegendColor.bind($$)).attr("cx", isLegendRightOrInset ? posFn.x2Tile : pos).attr("cy", isLegendRightOrInset ? pos : posFn.yTile); else if (isRectangle) selection.attr("stroke-width", itemTileSize.height).attr("x1", isLegendRightOrInset ? posFn.x1Tile : pos).attr("y1", isLegendRightOrInset ? pos : posFn.yTile).attr("x2", isLegendRightOrInset ? posFn.x2Tile : pos).attr("y2", isLegendRightOrInset ? pos : posFn.yTile); }); }, /** * Update legend item position * @param {Array} targetIdz Data ids * @param {boolean} withTransition Whether or not to apply transition * @param {object} posFn Position functions * @private */ updateLegendItemPos(targetIdz, withTransition, posFn) { const $$ = this; const { config, $el: { legend }, $T } = $$; const usePoint = config.legend_usePoint; const legendType = config.legend_item_tile_type; const isRectangle = legendType !== "circle"; if (usePoint) { const tiles = legend.selectAll(`.${$LEGEND.legendItemPoint}`).data(targetIdz); const isRectangleTile = config.legend_item_tile_type !== "circle"; const tileWidth = isRectangleTile ? config.legend_item_tile_width : config.legend_item_tile_r * 2; const tileHeight = isRectangleTile ? config.legend_item_tile_height : config.legend_item_tile_r * 2; const iconWidth = tileWidth * .75; const iconHeight = tileHeight * .75; const customScaleX = tileWidth / 8; const customScaleY = tileHeight / 8; $T(tiles, withTransition).each(function() { const nodeName = this.nodeName.toLowerCase(); let x = "x"; let y = "y"; let radius = null; let width = null; let height = null; if (nodeName === "circle") { x = "cx"; y = "cy"; radius = Math.min(iconWidth, iconHeight) / 2; } else if (nodeName === "rect") { width = iconWidth; height = iconHeight; } const tile = select(this).attr("transform", null).attr("x", null).attr("y", null).attr("cx", null).attr("cy", null).attr("r", radius).attr("width", width).attr("height", height); if (nodeName === "use") tile.attr("transform", (d) => `translate(${posFn.x1Tile(d)} ${posFn.yTile(d) - tileHeight / 2}) scale(${customScaleX} ${customScaleY})`); else tile.attr(x, (d) => posFn.x1Tile(d) + (tileWidth - (width || 0)) / 2).attr(y, (d) => posFn.yTile(d) - (height || 0) / 2); }); } else $T(legend.selectAll(`.${$LEGEND.legendItemTile}`).data(targetIdz), withTransition).style("stroke", _getLegendColor.bind($$)).call((selection) => { if (legendType === "circle") selection.attr("cx", (d) => { const x2 = posFn.x2Tile(d); return x2 - (x2 - posFn.x1Tile(d)) / 2; }).attr("cy", posFn.yTile); else if (isRectangle) selection.attr("x1", posFn.x1Tile).attr("y1", posFn.yTile).attr("x2", posFn.x2Tile).attr("y2", posFn.yTile); }); } }; //#endregion export { legend_default as default };