UNPKG

apexcharts

Version:

A JavaScript Chart Library

186 lines (171 loc) 6.31 kB
// @ts-check import { BrowserAPIs } from '../../ssr/BrowserAPIs.js' import { Environment } from '../../utils/Environment.js' import { placeInReservedBand } from '../../charts/common/Breadcrumb.js' const XHTML = 'http://www.w3.org/1999/xhtml' /** * Drilldown breadcrumb: an absolutely-positioned <nav> rendered inside * w.dom.elWrap (the same wrapper the toolbar uses), re-rendered after every * chart (re)render because elWrap's contents are rebuilt each time. * * On an axis chart `Dimensions.gridPadForBreadcrumb` reserves a band above the * plot and the strip sits in it. Being an overlay is not enough on its own: * with nothing reserved it was pushed below the title and came to rest on the * top gridline and the first y-axis label. A pie or donut still floats, since * its corners are empty. * * @module Breadcrumb */ export default class Breadcrumb { /** * @param {import('../../types/internal').ChartStateW} w * @param {import('../../types/internal').ChartContext} ctx * @param {import('./Drilldown').default} drilldown */ constructor(w, ctx, drilldown) { this.w = w this.ctx = ctx this.drilldown = drilldown } /** * @param {Array<string|number>} path - ['root', id, id, ...] */ render(path) { if (!Environment.isBrowser()) return const w = this.w const elWrap = w.dom.elWrap if (!elWrap) return // Remove any stale breadcrumb first (guards against double-render). const existing = elWrap.querySelector('.apexcharts-breadcrumb') if (existing && existing.parentNode) { existing.parentNode.removeChild(existing) } const cfg = w.config.drilldown && w.config.drilldown.breadcrumb if (!cfg || cfg.show === false) return // Only show once the user has drilled in. if (this.drilldown.depth === 0) return const nav = BrowserAPIs.createElementNS(XHTML, 'nav') nav.setAttribute('class', 'apexcharts-breadcrumb') nav.setAttribute('aria-label', 'Drilldown breadcrumb') this._position(nav, cfg) const separator = cfg.separator != null ? cfg.separator : ' / ' path.forEach((id, i) => { if (i > 0) { const sep = BrowserAPIs.createElementNS(XHTML, 'span') sep.setAttribute('class', 'apexcharts-breadcrumb-separator') sep.setAttribute('aria-hidden', 'true') sep.textContent = separator nav.appendChild(sep) } const label = this._label(id, i) const isCurrent = i === path.length - 1 if (isCurrent) { const cur = BrowserAPIs.createElementNS(XHTML, 'span') cur.setAttribute( 'class', 'apexcharts-breadcrumb-item apexcharts-breadcrumb-current', ) cur.setAttribute('aria-current', 'page') cur.textContent = label nav.appendChild(cur) } else { const btn = /** @type {HTMLButtonElement} */ ( BrowserAPIs.createElementNS(XHTML, 'button') ) btn.setAttribute('type', 'button') btn.setAttribute('class', 'apexcharts-breadcrumb-item') // Prefix the leftmost crumb with a back-arrow so it reads as the // "go back" affordance, e.g. "← All Years / 2021 by Channel". if (i === 0) { const arrow = BrowserAPIs.createElementNS(XHTML, 'span') arrow.setAttribute('class', 'apexcharts-breadcrumb-arrow') arrow.setAttribute('aria-hidden', 'true') arrow.textContent = '←' btn.appendChild(arrow) } const text = BrowserAPIs.createElementNS(XHTML, 'span') text.setAttribute('class', 'apexcharts-breadcrumb-label') text.textContent = label btn.appendChild(text) btn.addEventListener('click', () => this.drilldown.drillToLevel(i)) nav.appendChild(btn) } }) elWrap.appendChild(nav) if (this.w.globals.axisCharts) { placeInReservedBand(this.w, this.ctx, nav, cfg) } // Still runs after the band placement: on a pie there is no band, and even // on an axis chart a responsive override can turn drilldown on after layout // and leave the strip with nowhere reserved. this._avoidChromeOverlap(nav) } /** * The breadcrumb is an absolute overlay, so at its default top-left it can * sit on top of a left-aligned title (or subtitle). After mounting, push it * below any chart chrome it intersects. (Sunburst's self-contained * breadcrumb applies the same rule.) * @param {HTMLElement} nav */ _avoidChromeOverlap(nav) { const w = this.w const chrome = /** @type {Element[]} */ ( ['.apexcharts-title-text', '.apexcharts-subtitle-text'] .map((s) => w.dom.baseEl.querySelector(s)) .filter((el) => el !== null) ) if (!chrome.length) return const wrapTop = w.dom.elWrap.getBoundingClientRect().top // Up to two passes: moving below the title can land on the subtitle. for (let pass = 0; pass < chrome.length + 1; pass++) { const nr = nav.getBoundingClientRect() const hit = chrome.find((el) => { const r = el.getBoundingClientRect() return ( nr.left < r.right && nr.right > r.left && nr.top < r.bottom && nr.bottom > r.top ) }) if (!hit) break nav.style.top = `${hit.getBoundingClientRect().bottom - wrapTop + 4}px` } } /** * @param {string|number} id * @param {number} index * @returns {string} */ _label(id, index) { const cfg = this.w.config.drilldown.breadcrumb let label if (index === 0) { label = cfg.rootLabel != null ? cfg.rootLabel : 'All' } else { const list = (this.w.config.drilldown.series || []).find( (s) => s && s.id === id, ) label = (list && list.name) || String(id) } if (typeof cfg.formatter === 'function') { return cfg.formatter(label, { index, depth: this.drilldown.depth }) } return label } /** * @param {HTMLElement} nav * @param {Record<string, any>} cfg */ _position(nav, cfg) { const ox = cfg.offsetX || 0 const oy = cfg.offsetY || 0 nav.style.position = 'absolute' nav.style.top = oy + 'px' if (cfg.position === 'top-right') { nav.style.right = -ox + 3 + 'px' } else { nav.style.left = ox + 'px' } } }