UNPKG

billboard.js

Version:

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

736 lines (735 loc) • 29.1 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 { parseDate } from "../module/util/object.js"; import { getScale } from "../ChartInternal/internals/scale.js"; import { getCanvasComparableValue, getCanvasComparableXDomain } from "./util.js"; //#region src/canvas/axisTicks.ts /** * Get tick option value for main/subchart canvas axes. * @param {object} config Chart config * @param {string} id Base axis id * @param {string} key Tick option key * @param {string} prefix Option prefix * @returns {boolean|number|string|Array|object|function|null|undefined} Tick option value * @private */ function getAxisTickOption(config, id, key, prefix = `axis_${id}`) { const value = config[`${prefix}_tick_${key}`]; return value !== void 0 ? value : config[`axis_${id}_tick_${key}`]; } /** * Get explicitly configured tick values. * @param {Array|function} values Tick values option * @param {object} api Chart API * @returns {Array} Tick values * @private */ function getOptionTickValues(values, api) { const resolved = typeof values === "function" ? values.call(api) : values; return resolved?.length ? resolved : void 0; } /** * Normalize axis values that can be expressed as category names, dates or numbers. * @param {object} $$ ChartInternal context * @param {number|Date|string} value Axis value * @param {string} id Axis id * @returns {number|Date|string} Scale-compatible value * @private */ function normalizeAxisValue($$, value, id) { if ($$.axis?.isTimeSeries?.(id)) return value instanceof Date ? value : parseDate.call($$, value); if (id === "x" && $$.axis?.isCategorized?.() && typeof value === "string") { const index = $$.config.axis_x_categories.indexOf(value); return index >= 0 ? index : value; } if ($$.axis?.isLog?.(id) && value !== null && value !== void 0) return +value; return value; } /** * Normalize x values that can be expressed as category names, dates or numbers. * @param {object} $$ ChartInternal context * @param {number|Date|string} value X value * @returns {number|Date|string} Scale-compatible value * @private */ function normalizeXValue($$, value) { return normalizeAxisValue($$, value, "x"); } /** * Normalize y/y2 tick or grid values. * @param {object} $$ ChartInternal context * @param {number|Date|string} value Axis value * @param {string} id Axis id * @returns {number|Date|string} Scale-compatible value * @private */ function normalizeYValue($$, value, id = "y") { return normalizeAxisValue($$, value, id); } /** * Normalize y/y2 tick values. * @param {object} $$ ChartInternal context * @param {Array} values Tick values * @param {string} id Axis id * @returns {Array} Scale-compatible tick values * @private */ function normalizeYTickValues($$, values, id = "y") { return values.map((value) => normalizeYValue($$, value, id)); } /** * Normalize x tick values. * @param {object} $$ ChartInternal context * @param {Array} values Tick values * @returns {Array} Scale-compatible tick values * @private */ function normalizeXTickValues($$, values) { return values.map((value) => normalizeXValue($$, value)).filter((value) => !(typeof value === "string" && $$.axis?.isCategorized?.())); } /** * Get tick values from scale. * @param {object} scale Scale function * @param {number} count Tick count * @returns {Array} Tick values * @private */ function getScaleTicks(scale, count) { const tickCount = typeof count === "function" ? count() : count; return scale.ticks ? scale.ticks(tickCount) : scale.domain(); } /** * Generate ticks for billboard log axes using the same SVG helper behavior. * @param {object} scale Symlog display scale * @param {number|function} count Optional tick count * @returns {Array} Tick values * @private */ function getLogScaleTicks(scale, count) { const [start, end] = scale.domain(); const numericStart = +start; const numericEnd = +end; if (count || !Number.isFinite(numericStart) || !Number.isFinite(numericEnd) || numericEnd <= 0) return getScaleTicks(scale, count); const logScale = getScale("_log").domain([numericStart > 0 ? numericStart : 1, numericEnd]).range(scale.range()); let ticks = logScale.ticks(); for (let cnt = numericEnd.toFixed().length; ticks.length > 15; cnt--) ticks = logScale.ticks(cnt); if (ticks.length) { ticks.splice(0, 1, start); ticks.splice(ticks.length - 1, 1, end); } return ticks; } /** * Check whether two tick values are equivalent. * @param {number|Date|string} a First value * @param {number|Date|string} b Second value * @returns {boolean} Whether values are equivalent * @private */ function isSameTickValue(a, b) { const av = +a; const bv = +b; return Number.isFinite(av) && Number.isFinite(bv) ? Math.abs(av - bv) < 1e-6 : a === b; } /** * Keep the visible domain edges in generated canvas ticks. * @param {Array} ticks Generated tick values * @param {Array} domain Current x domain * @returns {Array} Tick values including domain edges * @private */ function includeDomainEndpoints(ticks, domain) { const [start, end] = domain; const values = ticks.slice(); if (start !== void 0 && !values.some((value) => isSameTickValue(value, start))) values.unshift(start); if (end !== void 0 && !values.some((value) => isSameTickValue(value, end))) values.push(end); return values; } /** * Convert an x value to a comparable number. * @param {object} $$ ChartInternal context * @param {number|Date|string} value X value * @returns {number|null} Comparable value * @private */ function toComparableValue($$, value) { const normalized = normalizeXValue($$, value); return getCanvasComparableValue(normalized); } /** * Check whether a zoom scale is showing a narrowed x domain. * @param {object} $$ ChartInternal context * @returns {boolean} Whether zoom scale should be used * @private */ function hasActiveXZoom($$) { const { scale } = $$; const zoomDomain = scale.zoom?.domain?.(); const xDomain = scale.subX?.domain?.() || scale.x?.orgDomain?.() || scale.x?.domain?.(); if (!zoomDomain?.length || !xDomain?.length) return false; return !isSameTickValue(zoomDomain[0], xDomain[0]) || !isSameTickValue(zoomDomain[1], xDomain[1]); } /** * Get current x scale, using zoom scale when active. * @param {object} $$ ChartInternal context * @returns {function} X scale * @private */ function getXScale($$) { return hasActiveXZoom($$) ? $$.scale.zoom : $$.scale.x; } /** * Check if an x tick is inside the current x scale domain. * @param {object} $$ ChartInternal context * @param {number|Date|string} value Tick value * @param {number} tolerance Domain tolerance * @returns {boolean} Whether value is visible in current domain * @private */ function isInCurrentXDomain($$, value, tolerance = 0) { const domain = getXScale($$).domain?.(); if (!domain?.length) return true; const target = toComparableValue($$, value); const min = +domain[0]; const max = +domain[1]; if (target === null || !Number.isFinite(min) || !Number.isFinite(max)) return true; return target >= Math.min(min, max) - tolerance && target <= Math.max(min, max) + tolerance; } /** * Get half-distance to the nearest neighboring x data point. * @param {Array} comparableValues Comparable x values * @param {number} index Current index * @returns {number} Half-distance tolerance * @private */ function getHalfNeighborDistance(comparableValues, index) { const distances = [index > 0 ? Math.abs(comparableValues[index] - comparableValues[index - 1]) : Infinity, index < comparableValues.length - 1 ? Math.abs(comparableValues[index + 1] - comparableValues[index]) : Infinity].filter(Number.isFinite); const distance = Math.min(...distances); return Number.isFinite(distance) ? distance / 2 : 0; } /** * Find the first numeric value greater than or equal to the target. * @param {Array} values Sorted numbers * @param {number} target Target value * @returns {number} Value index * @private */ function lowerBoundNumbers(values, target) { let low = 0; let high = values.length; while (low < high) { const mid = low + high >> 1; if (values[mid] < target) low = mid + 1; else high = mid; } return low; } /** * Find the first numeric value greater than the target. * @param {Array} values Sorted numbers * @param {number} target Target value * @returns {number} Value index * @private */ function upperBoundNumbers(values, target) { let low = 0; let high = values.length; while (low < high) { const mid = low + high >> 1; if (values[mid] <= target) low = mid + 1; else high = mid; } return low; } /** * Get cached sorted x data ticks and numeric values. * @param {object} $$ ChartInternal context * @param {Array} targets Visible targets * @returns {object} Cached data ticks * @private */ function getCachedXDataTicks($$, targets) { const { axis, state } = $$; const key = [ state.dataGeneration, axis?.isTimeSeries?.() ? 1 : 0, targets.map((target) => `${target.id}:${target.values.length}`).join(",") ].join("|"); const cached = state._canvasXDataTickCache; if (cached?.key === key) return cached; const values = $$.mapTargetsToUniqueXs(targets); const entry = { key, values, comparable: values.map((value) => toComparableValue($$, value) ?? NaN) }; state._canvasXDataTickCache = entry; return entry; } /** * Include the nearest data ticks at zoom boundaries. * @param {object} $$ ChartInternal context * @param {object} dataTicks Cached data x ticks * @returns {Array} Data ticks visible in current zoom domain * @private */ function filterXDataTicksForZoom($$, dataTicks) { const { values: ticks, comparable } = dataTicks; if (!hasActiveXZoom($$)) return ticks; const domain = getCanvasComparableXDomain($$); if (!domain || comparable.length !== ticks.length || !Number.isFinite(comparable[0])) return ticks.filter((value) => isInCurrentXDomain($$, value)); const [start, end] = domain; let startIndex = lowerBoundNumbers(comparable, start); let endIndex = upperBoundNumbers(comparable, end); const includeNearest = (edge) => { const index = lowerBoundNumbers(comparable, edge); let nearestIndex = -1; let nearestDistance = Infinity; for (const candidate of [index - 1, index]) { const distance = Math.abs(comparable[candidate] - edge); if (distance < nearestDistance) { nearestDistance = distance; nearestIndex = candidate; } } if (nearestIndex > -1 && nearestIndex < comparable.length && nearestDistance <= getHalfNeighborDistance(comparable, nearestIndex) + 1e-6) { startIndex = Math.min(startIndex, nearestIndex); endIndex = Math.max(endIndex, nearestIndex + 1); } }; includeNearest(start); includeNearest(end); return ticks.slice(Math.max(0, startIndex), Math.min(ticks.length, endIndex)); } /** * Get stable key part for configured explicit tick values. * @param {Array|function} values Tick values option * @param {number} generation Current redraw generation * @returns {string} Cache key * @private */ function getTickOptionCacheKey(values, generation) { if (typeof values === "function") return `fn:${generation}`; return Array.isArray(values) ? values.map((value) => value instanceof Date ? +value : String(value)).join(",") : ""; } /** * Get x tick cache key for the current draw inputs. * @param {object} $$ ChartInternal context * @param {boolean} cull Whether ticks are text-culled * @returns {string} Cache key * @private */ function getXTickCacheKey($$, cull) { const { config, state } = $$; const domainKey = (getXScale($$).domain?.() || []).map((value) => value instanceof Date ? +value : String(value)).join(","); return [ cull ? 1 : 0, state.dataGeneration, domainKey, config.axis_rotated ? state.height : state.width, config.axis_x_tick_fit ? 1 : 0, config.axis_x_tick_count || "", config.axis_x_tick_culling, config.axis_x_tick_culling_max || "", config.axis_x_tick_culling_reverse ? 1 : 0, config.axis_x_categories?.length || 0, getTickOptionCacheKey(config.axis_x_tick_values, state.redrawGeneration) ].join("|"); } /** * Generate tick values reusing the axis helper when available. * @param {object} $$ ChartInternal context * @param {Array} values Base values * @param {number} count Tick count * @param {boolean} forTimeSeries Whether values are timeseries * @returns {Array} Tick values * @private */ function generateTickValues($$, values, count, forTimeSeries = false) { return $$.axis?.generateTickValues ? $$.axis.generateTickValues(values, count, forTimeSeries) : values; } /** * Get step ticks for y axis. * @param {Array} domain Scale domain * @param {number} stepSize Step size * @returns {Array} Tick values * @private */ function getStepTicks(domain, stepSize) { const [start, end] = domain; const ticks = []; if (!stepSize || !Number.isFinite(stepSize)) return ticks; for (let value = Math.round(start); value <= end; value += stepSize) ticks.push(value); return ticks; } /** * Get x ticks for indexed/linear canvas mode. * @param {object} $$ ChartInternal context * @param {boolean} cull Whether to cull tick values * @returns {Array} Tick values * @private */ function getXTickValues($$, cull = true) { const { axis, config } = $$; const targetScale = getXScale($$); const targetsToShow = $$.getTargetsToShow?.() || $$.filterTargetsToShow(); const cache = $$.state._canvasXTickValuesCache || ($$.state._canvasXTickValuesCache = /* @__PURE__ */ new Map()); const cacheKey = getXTickCacheKey($$, cull); const cached = cache.get(cacheKey); if (cached) return cached; const setCache = (ticks) => { cache.set(cacheKey, ticks); return ticks; }; if (!targetsToShow?.length) return setCache([]); const explicit = getOptionTickValues(config.axis_x_tick_values, $$.api); if (explicit) { const values = normalizeXTickValues($$, explicit); return setCache(cull ? cullDataTicks($$, values) : values); } if (config.axis_x_tick_fit && targetsToShow?.length && $$.mapTargetsToUniqueXs) { const generated = generateTickValues($$, filterXDataTicksForZoom($$, getCachedXDataTicks($$, targetsToShow)), config.axis_x_tick_count, axis?.isTimeSeries?.()); return setCache(cull ? cullDataTicks($$, generated, true) : generated); } if (hasActiveXZoom($$) && !axis?.isCategorized?.()) { const domain = $$.zoom?.getDomain?.() || targetScale.domain(); const generated = includeDomainEndpoints(getScaleTicks(targetScale, config.axis_x_tick_count || 10), domain); return setCache(cull ? cullTicks(generated, getXTickCullMax($$)) : generated); } if (axis?.isCategorized?.() && config.axis_x_categories?.length) { const generated = config.axis_x_categories.map((_, i) => i); return setCache(cull ? cullTicks(generated, getXTickCullMax($$)) : generated); } const generated = getScaleTicks(targetScale, config.axis_x_tick_count || 10); return setCache(cull ? cullTicks(generated, getXTickCullMax($$)) : generated); } /** * Get max number of subchart x ticks to render. * @param {object} $$ ChartInternal context * @returns {number|undefined} Max tick count * @private */ function getSubXTickCullMax($$) { const { config, state: { height2, width2 } } = $$; const size = config.axis_rotated ? height2 : width2; const count = getAxisTickOption(config, "x", "count", "subchart_axis_x"); const culling = getAxisTickOption(config, "x", "culling", "subchart_axis_x"); const cullingMax = getAxisTickOption(config, "x", "culling_max", "subchart_axis_x"); if (count) return count; if (culling !== false) return Math.min(cullingMax || 10, Math.max(2, Math.floor(size / 70))); } /** * Get x ticks for the canvas subchart axis. * @param {object} $$ ChartInternal context * @param {boolean} culling Whether to cull tick values * @returns {Array} Tick values * @private */ function getSubXTickValues($$, culling = true) { const { axis, config, scale } = $$; const targetScale = scale.subX; const targetsToShow = $$.getTargetsToShow?.() || $$.filterTargetsToShow(); const tickCount = getAxisTickOption(config, "x", "count", "subchart_axis_x"); const cullMax = getSubXTickCullMax($$); const cullData = (ticks, sorted = false) => culling ? cullDataTicks($$, ticks, sorted, "subchart_axis_x", cullMax) : ticks; if (!targetScale || !targetsToShow?.length) return []; const explicit = getOptionTickValues(getAxisTickOption(config, "x", "values", "subchart_axis_x"), $$.api); if (explicit) return cullData(normalizeXTickValues($$, explicit)); if (config.axis_x_tick_fit && $$.mapTargetsToUniqueXs) return cullData(generateTickValues($$, $$.mapTargetsToUniqueXs(targetsToShow), tickCount, axis?.isTimeSeries?.()), true); if (axis?.isCategorized?.() && config.axis_x_categories?.length) return cullData(config.axis_x_categories.map((_, i) => i), true); return cullData(getScaleTicks(targetScale, tickCount || 10), true); } /** * Get raw category boundary tick values for x axis tick lines. * @param {object} $$ ChartInternal context * @param {function} targetScale X scale * @param {boolean} outerTick Whether outer ticks are shown * @returns {Array} Category boundary values * @private */ function getCategoryXTickLineValues($$, targetScale = getXScale($$), outerTick = $$.config.axis_x_tick_outer) { const scale = targetScale; const domain = scale.orgDomain?.() || scale.domain?.(); if (!domain?.length) return []; const start = +domain[0]; const end = +domain[domain.length - 1]; if (!Number.isFinite(start) || !Number.isFinite(end)) return []; const min = Math.ceil(Math.min(start, end)); const values = Array.from({ length: Math.max(0, Math.floor(Math.max(start, end)) - min + 1) }, (_, i) => min + i); return outerTick ? values.slice(1, -1) : values; } /** * Resolve x tick line coordinate. Category tick lines are drawn on raw scale boundaries. * @param {object} $$ ChartInternal context * @param {number|Date|string} value X tick value * @param {function} targetScale X scale * @returns {number} Pixel coordinate relative to plot area * @private */ function getXTickLinePosition($$, value, targetScale = getXScale($$)) { const scale = targetScale; const normalized = normalizeXValue($$, value); if ($$.axis?.isCategorized?.()) { const rawScale = scale.orgScale?.(); if (rawScale) return rawScale(normalized); const offset = $$.axis.x?.tickOffset?.() || (scale(1) - scale(0)) / 2; return scale(normalized) - offset; } return scale(normalized); } /** * Check whether adjacent x tick line intervals overlap on canvas. * @param {object} $$ ChartInternal context * @param {Array} ticks Sorted tick values * @param {number} tickLineWidth Tick line stroke width * @param {function} targetScale X scale * @returns {boolean} Whether tick lines should follow culled text ticks * @private */ function hasOverlappedXTickLineIntervals($$, ticks, tickLineWidth, targetScale = getXScale($$)) { if (ticks.length < 2) return false; const halfWidth = Math.max(1, tickLineWidth) / 2; const positions = ticks.map((tick) => getXTickLinePosition($$, tick, targetScale)).filter(Number.isFinite).sort((a, b) => a - b); if (positions.length < 2) return false; let previousEnd = positions[0] + halfWidth; for (let i = 1; i < positions.length; i++) { const start = positions[i] - halfWidth; const end = positions[i] + halfWidth; if (start <= previousEnd + 1) return true; previousEnd = Math.max(previousEnd, end); } return false; } /** * Remove line ticks that map to the same canvas pixel. * @param {object} $$ ChartInternal context * @param {Array} ticks Tick values * @param {function} targetScale X scale * @returns {Array} Pixel-deduped tick values * @private */ function dedupeXTickLineValues($$, ticks, targetScale = getXScale($$)) { const seen = /* @__PURE__ */ new Set(); return ticks.filter((tick) => { const pos = getXTickLinePosition($$, tick, targetScale); const key = Math.round(pos); if (!Number.isFinite(pos) || seen.has(key)) return false; seen.add(key); return true; }); } /** * Get x tick values for tick lines, following SVG culling options. * @param {object} $$ ChartInternal context * @param {Array} textTicks Text tick values * @param {number} tickLineWidth Tick line stroke width * @returns {Array} Tick line values * @private */ function getXTickLineValues($$, textTicks, tickLineWidth = 1) { const { axis, config } = $$; if (axis?.isCategorized?.()) return dedupeXTickLineValues($$, getCategoryXTickLineValues($$)); if (config.axis_x_tick_culling === false || config.axis_x_tick_culling_lines === false) return textTicks; const lineTicks = getXTickValues($$, false); return hasOverlappedXTickLineIntervals($$, lineTicks, tickLineWidth) ? textTicks : dedupeXTickLineValues($$, lineTicks); } /** * Get subchart x tick values for tick lines, following SVG culling options. * @param {object} $$ ChartInternal context * @param {Array} textTicks Text tick values * @param {number} tickLineWidth Tick line stroke width * @returns {Array} Tick line values * @private */ function getSubXTickLineValues($$, textTicks, tickLineWidth = 1) { const { axis, config, scale } = $$; const targetScale = scale.subX; const culling = getAxisTickOption(config, "x", "culling", "subchart_axis_x"); const cullingLines = getAxisTickOption(config, "x", "culling_lines", "subchart_axis_x"); const outerTick = getAxisTickOption(config, "x", "outer", "subchart_axis_x"); if (!targetScale) return []; if (axis?.isCategorized?.()) return dedupeXTickLineValues($$, getCategoryXTickLineValues($$, targetScale, outerTick), targetScale); if (culling === false || cullingLines === false) return textTicks; const lineTicks = getSubXTickValues($$, false); return hasOverlappedXTickLineIntervals($$, lineTicks, tickLineWidth, targetScale) ? textTicks : dedupeXTickLineValues($$, lineTicks, targetScale); } /** * Get y ticks for indexed/linear canvas mode. * @param {object} $$ ChartInternal context * @param {string} id Axis id * @param {number} count Optional tick count override * @param {boolean} culling Whether to apply tick culling * @param {d3Scale} targetScale Scale to generate ticks from * @param {string} optionPrefix Tick option prefix * @returns {Array} Tick values * @private */ function getYTickValues($$, id = "y", count, culling = true, targetScale = $$.scale[id], optionPrefix = `axis_${id}`) { const { axis, config } = $$; const prefix = `axis_${id}`; const explicit = getOptionTickValues(getAxisTickOption(config, id, "values", optionPrefix), $$.api); const maybeCull = (ticks) => culling ? cullAxisTicks($$, id, ticks, optionPrefix) : ticks; if (explicit) return maybeCull(normalizeYTickValues($$, explicit, id)); const stepTicks = getStepTicks(targetScale.domain(), config[`${prefix}_tick_stepSize`]); if (stepTicks.length) return maybeCull(stepTicks); const tickCount = count ?? getAxisTickOption(config, id, "count", optionPrefix); if (axis?.isTimeSeries?.(id) && config[`${prefix}_tick_time_value`]) return maybeCull(getScaleTicks(targetScale, config[`${prefix}_tick_time_value`])); if (axis?.isLog?.(id)) return maybeCull(getLogScaleTicks(targetScale, tickCount)); if (tickCount) { const domain = targetScale.domain(); return maybeCull(generateTickValues($$, domain, domain.every((v) => v === 0) ? 1 : tickCount, axis?.isTimeSeries?.(id))); } return maybeCull(getScaleTicks(targetScale, 10)); } /** * Get scale for an additional axis. * @param {object} $$ ChartInternal context * @param {string} id Axis id * @param {object} axisConfig Additional axis config * @returns {function} Scale * @private */ function getAdditionalAxisScale($$, id, axisConfig) { const baseScale = id === "x" ? getXScale($$) : $$.scale[id]; const scale = baseScale?.copy ? baseScale.copy() : baseScale; if (axisConfig.domain && scale?.domain) scale.domain(axisConfig.domain); return scale; } /** * Get tick values for an additional axis. * @param {object} $$ ChartInternal context * @param {string} id Axis id * @param {function} scale Axis scale * @param {object} axisConfig Additional axis config * @returns {Array} Tick values * @private */ function getAdditionalAxisTickValues($$, id, scale, axisConfig) { const tick = axisConfig.tick || {}; const explicit = getOptionTickValues(tick.values, $$.api); if (explicit) return id === "x" ? normalizeXTickValues($$, explicit) : normalizeYTickValues($$, explicit, id); if (id !== "x" && $$.axis?.isLog?.(id)) return getLogScaleTicks(scale, tick.count); return getScaleTicks(scale, tick.count ?? 10); } /** * Get tick formatter for an additional axis. * @param {object} $$ ChartInternal context * @param {object} axisConfig Additional axis config * @returns {function} Formatter * @private */ function getAdditionalAxisTickFormat($$, axisConfig) { const format = axisConfig.tick?.format; return typeof format === "function" ? format.bind($$.api) : ((value) => value); } /** * Cull generated ticks like AxisRenderer.getGeneratedTicks(). * @param {Array} ticks Tick values * @param {number} count Target count * @returns {Array} Culled tick values * @private */ function cullTicks(ticks, count) { const len = ticks.length - 1; if (count && len > count) { const last = ticks.length - 1; if (count <= 1) return [ticks[0]]; return Array.from({ length: count }, (_, i) => ticks[Math.round(i * last / (count - 1))]); } return ticks; } /** * Cull axis ticks following SVG manual culling. * @param {object} $$ ChartInternal context * @param {string} id Axis id * @param {Array} ticks Tick values * @param {string} optionPrefix Tick option prefix * @returns {Array} Culled tick values * @private */ function cullAxisTicks($$, id, ticks, optionPrefix = `axis_${id}`) { const { config } = $$; if (!getAxisTickOption(config, id, "culling", optionPrefix)) return ticks; const cullingMax = getAxisTickOption(config, id, "culling_max", optionPrefix) || 10; const reverse = getAxisTickOption(config, id, "culling_reverse", optionPrefix); const sortedTicks = ticks.slice().sort((a, b) => { const av = +a; const bv = +b; const order = Number.isFinite(av) && Number.isFinite(bv) ? av - bv : String(a).localeCompare(String(b)); return reverse ? -order : order; }); const tickSize = sortedTicks.length; let intervalForCulling = 0; for (let i = 1; i < tickSize; i++) if (tickSize / i < cullingMax) { intervalForCulling = i; break; } if (!intervalForCulling) return ticks; const visible = new Set(sortedTicks.filter((_, i) => i % intervalForCulling === 0)); return ticks.filter((tick) => visible.has(tick)); } /** * Cull data-based x ticks like the SVG axis manual culling path. * @param {object} $$ ChartInternal context * @param {Array} ticks Tick values * @param {boolean} sorted Whether ticks are already sorted in x order * @param {string} optionPrefix Tick option prefix * @param {number} max Optional culling max override * @returns {Array} Culled tick values * @private */ function cullDataTicks($$, ticks, sorted = false, optionPrefix = "axis_x", max) { const { config } = $$; if (getAxisTickOption(config, "x", "culling", optionPrefix) === false) return ticks; const cullingMax = max || getAxisTickOption(config, "x", "culling_max", optionPrefix) || 10; const reverse = getAxisTickOption(config, "x", "culling_reverse", optionPrefix); const sortedTicks = sorted ? ticks : ticks.slice().sort((a, b) => { const av = +a; const bv = +b; if (Number.isFinite(av) && Number.isFinite(bv)) return reverse ? bv - av : av - bv; return reverse ? String(b).localeCompare(String(a)) : String(a).localeCompare(String(b)); }); const tickSize = sortedTicks.length; let intervalForCulling = 0; for (let i = 1; i < tickSize; i++) if (tickSize / i < cullingMax) { intervalForCulling = i; break; } if (!intervalForCulling) return ticks; if (sorted) return ticks.filter((_, i) => reverse ? (tickSize - 1 - i) % intervalForCulling === 0 : i % intervalForCulling === 0); const visible = new Set(sortedTicks.filter((_, i) => i % intervalForCulling === 0)); return ticks.filter((tick) => visible.has(tick)); } /** * Get max number of x ticks to render. * @param {object} $$ ChartInternal context * @returns {number|undefined} Max tick count * @private */ function getXTickCullMax($$) { const { config, state: { height, width } } = $$; const size = config.axis_rotated ? height : width; if (config.axis_x_tick_count) return config.axis_x_tick_count; if (config.axis_x_tick_culling !== false) return Math.min(config.axis_x_tick_culling_max || 10, Math.max(2, Math.floor(size / 70))); } /** * Get y grid ticks. * @param {object} $$ ChartInternal context * @returns {Array} Tick values * @private */ function getYGridTickValues($$) { const { axis, config } = $$; const generated = axis?.y?.getGeneratedTicks?.(config.grid_y_ticks); if (generated?.length) return generated; return config.grid_y_ticks ? cullTicks(getYTickValues($$, "y", void 0, false), config.grid_y_ticks) : getYTickValues($$, "y", void 0, false); } //#endregion export { getAdditionalAxisScale, getAdditionalAxisTickFormat, getAdditionalAxisTickValues, getSubXTickLineValues, getSubXTickValues, getXScale, getXTickLinePosition, getXTickLineValues, getXTickValues, getYGridTickValues, getYTickValues, isSameTickValue, normalizeXValue, normalizeYValue };