UNPKG

billboard.js

Version:

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

2,268 lines • 82.8 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 { window as win } from "../../module/browser.js";
import { isBoolean, isFunction } from "../../module/util/type-checks.js";
import { sanitize } from "../../module/sanitize.js";
import { callFn, extend, parseDate, tplProcess } from "../../module/util/object.js";
import { getBoundingRect } from "../../module/util/dom.js";
import { $FOCUS, $LEGEND } from "../classes.js";
import { KEY } from "../../module/Cache.js";
import ChartInternal from "../../ChartInternal/ChartInternal.js";
import { getRenderDataPoint } from "../../ChartInternal/shape/core/geometry.js";
import { getMainCoordFromSubchartCoord } from "../../ChartInternal/internals/subchart.util.js";
import { hasCanvasDrawableValue, isCanvasBarType, isCanvasBubbleType, isCanvasCandlestickType, isCanvasPointType, isCanvasScatterType, isCanvasTargetSupported, isCanvasTreemapType } from "../../canvas/util.js";
import CanvasAxisRenderer from "../../canvas/CanvasAxisRenderer.js";
import { $CANVAS } from "../../canvas/classes.js";
import CanvasEngine from "../../canvas/CanvasEngine.js";
import CanvasRenderer from "../../canvas/CanvasRenderer.js";
import CanvasTheme from "../../canvas/CanvasTheme.js";
import HitDetector from "../../canvas/HitDetector.js";
import { select } from "d3-selection";
//#region src/config/resolver/canvas.ts
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
*/
const CANVAS_SELECTABLE_TYPE_FILTERS = [
	isCanvasPointType,
	isCanvasBarType,
	isCanvasCandlestickType,
	isCanvasTreemapType
];
const CANVAS_LEGEND_TOUCH_TAP_THRESHOLD = 10;
const CANVAS_LEGEND_TOUCH_CLICK_TIMEOUT = 750;
const CANVAS_SUBCHART_HANDLE_SIZE = 6;
const CANVAS_SUBCHART_CLICK_TOLERANCE = 2;
const CANVAS_FLOW_ANIMATION_MAX_VALUES = 1e5;
const CANVAS_HTML_ATTR_ESCAPE = {
	"&": "&",
	"<": "&lt;",
	">": "&gt;",
	"\"": "&quot;",
	"'": "&#39;"
};
const canvasSelectionDragRows = /* @__PURE__ */ new WeakMap();
/**
* Emit canvas v1 unsupported option warning.
* @param {string} message Warning message
* @private
*/
function warn(message) {
	win.console?.warn?.(`[billboard.js] ${message}`);
}
/**
* Get formatted legend text.
* @param {object} $$ ChartInternal instance
* @param {string} id Data id
* @returns {string} Legend text
* @private
*/
function getLegendText($$, id) {
	const { config } = $$;
	const text = config.data_names[id] ?? id;
	return isFunction(config.legend_format) ? config.legend_format(text, id !== text ? id : void 0) : text;
}
/**
* Escape a value for safe HTML attribute interpolation.
* @param {string} value Attribute value
* @returns {string} Escaped value
* @private
*/
function escapeHtmlAttr(value) {
	return `${value}`.replace(/[&<>"']/g, (char) => CANVAS_HTML_ATTR_ESCAPE[char]);
}
/**
* Get the point pattern assigned to a legend item.
* @param {object} $$ ChartInternal instance
* @param {string} id Data id
* @returns {string} Point pattern
* @private
*/
function getCanvasLegendPointPattern($$, id) {
	const { config } = $$;
	const targetIds = $$.mapToIds?.($$.data.targets) || [];
	const pattern = $$.getValidPointPattern?.() || (Array.isArray(config.point_pattern) && config.point_pattern.length ? config.point_pattern : [config.point_type]);
	return pattern[Math.max(0, targetIds.indexOf(id)) % pattern.length] || "circle";
}
/**
* Build inline SVG markup for a canvas HTML legend point tile.
* @param {object} $$ ChartInternal instance
* @param {string} id Data id
* @returns {string} SVG markup
* @private
*/
function getCanvasLegendPointIcon($$, id) {
	const color = escapeHtmlAttr($$.color(id));
	const pattern = getCanvasLegendPointPattern($$, id);
	const svgAttrs = "viewBox=\"0 0 8 8\" aria-hidden=\"true\" focusable=\"false\" style=\"display:block;width:100%;height:100%;overflow:visible;pointer-events:none\"";
	const paintAttrs = `fill="${color}" stroke="${color}"`;
	if (/^rect(angle)?$/i.test(pattern)) return `<svg ${svgAttrs}><rect x="1" y="1" width="6" height="6" ${paintAttrs}></rect></svg>`;
	if (!pattern || /^circle$/i.test(pattern)) return `<svg ${svgAttrs}><circle cx="4" cy="4" r="3" ${paintAttrs}></circle></svg>`;
	return `<svg ${svgAttrs}><g ${paintAttrs}>${sanitize(pattern)}</g></svg>`;
}
/**
* Update legend item lookup cache for canvas HTML legends.
* @param {object} $$ ChartInternal instance
* @param {object} item Legend item selection
* @private
*/
function updateCanvasLegendItemMap($$, item) {
	const itemMap = /* @__PURE__ */ new Map();
	item.each(function(id) {
		itemMap.set(id, this);
	});
	$$.cache.add(KEY.legendItemMap, itemMap);
}
/**
* Apply shared canvas HTML legend classes and state.
* @param {object} $$ ChartInternal instance
* @param {object} item Legend item selection
* @private
*/
function setCanvasHtmlLegendItem($$, item) {
	const { config } = $$;
	item.attr("class", function(id) {
		const next = `${select(this).attr("class") || ""} ${$$.generateClass($LEGEND.legendItem, id)}`;
		return Array.from(new Set(next.trim().split(/\s+/).filter(Boolean))).join(" ");
	}).style("visibility", (id) => $$.isLegendToShow(id) ? null : "hidden").classed($LEGEND.legendItemHidden, (id) => !$$.isTargetToShow(id)).classed($CANVAS.legendItemInteractive, !!config.interaction_enabled);
	updateCanvasLegendItemMap($$, item);
}
/**
* Apply SVG-compatible legend focus styling to canvas HTML legend items.
* @param {object} $$ ChartInternal instance
* @param {string} id Focused data id
* @private
*/
function setCanvasHtmlLegendFocus($$, id) {
	const { legend } = $$.$el;
	const targetIds = $$.mapToTargetIds?.([id]) || [id];
	legend?.selectAll(`.${$LEGEND.legendItem}`).classed($FOCUS.legendItemFocused, (d) => targetIds.indexOf(d) >= 0).style("opacity", function(d) {
		return targetIds.indexOf(d) >= 0 ? null : $$.opacityForUnfocusedLegend.call($$, select(this));
	});
}
/**
* Revert canvas HTML legend focus styling.
* @param {object} $$ ChartInternal instance
* @private
*/
function revertCanvasHtmlLegendFocus($$) {
	const { legend } = $$.$el;
	legend?.selectAll(`.${$LEGEND.legendItem}`).classed($FOCUS.legendItemFocused, false).style("opacity", null);
}
/**
* Apply SVG-compatible target focus state for canvas redraw.
* @param {object} $$ ChartInternal instance
* @param {string} id Focused data id
* @private
*/
function setCanvasLegendTargetFocus($$, id) {
	const { state } = $$;
	const focusedIds = ($$.mapToTargetIds?.([id]) || [id]).filter($$.isTargetToShow, $$);
	const focusedSet = new Set(focusedIds);
	const defocusedIds = ($$.mapToTargetIds?.() || []).filter((targetId) => !focusedSet.has(targetId) && $$.isTargetToShow(targetId));
	state.focusedTargetIds = focusedSet;
	state.defocusedTargetIds = new Set(defocusedIds);
	$$.renderCanvasFrame?.(void 0, null, false);
}
/**
* Revert canvas target focus state.
* @param {object} $$ ChartInternal instance
* @private
*/
function revertCanvasLegendTargetFocus($$) {
	const { state } = $$;
	const changed = !!state.focusedTargetIds?.size || !!state.defocusedTargetIds?.size;
	state.focusedTargetIds = /* @__PURE__ */ new Set();
	state.defocusedTargetIds = /* @__PURE__ */ new Set();
	changed && $$.renderCanvasFrame?.(void 0, null, false);
}
/**
* Get touch point from a canvas legend touch event.
* @param {TouchEvent} event Touch event
* @returns {Touch | undefined} Touch point
* @private
*/
function getCanvasLegendTouchPoint(event) {
	return event.changedTouches?.[0] || event.touches?.[0];
}
/**
* Store the touch start position for canvas legend tap detection.
* @param {object} $$ ChartInternal instance
* @param {string} id Legend data id
* @param {TouchEvent} event Touch event
* @private
*/
function setCanvasLegendTouchStart($$, id, event) {
	const touch = getCanvasLegendTouchPoint(event);
	$$.state.canvasLegendTouch = touch ? {
		id,
		x: touch.clientX,
		y: touch.clientY,
		moved: false
	} : null;
}
/**
* Update whether the current canvas legend touch moved beyond tap tolerance.
* @param {object} $$ ChartInternal instance
* @param {TouchEvent} event Touch event
* @private
*/
function updateCanvasLegendTouchMove($$, event) {
	const start = $$.state.canvasLegendTouch;
	const touch = start && getCanvasLegendTouchPoint(event);
	if (touch) start.moved = start.moved || Math.abs(touch.clientX - start.x) > CANVAS_LEGEND_TOUCH_TAP_THRESHOLD || Math.abs(touch.clientY - start.y) > CANVAS_LEGEND_TOUCH_TAP_THRESHOLD;
}
/**
* Determine whether a touch sequence is a canvas legend tap.
* @param {object} $$ ChartInternal instance
* @param {string} id Legend data id
* @param {TouchEvent} event Touch event
* @returns {boolean} Whether the touch sequence is a tap
* @private
*/
function isCanvasLegendTouchTap($$, id, event) {
	updateCanvasLegendTouchMove($$, event);
	const start = $$.state.canvasLegendTouch;
	$$.state.canvasLegendTouch = null;
	return !!start && start.id === id && !start.moved;
}
/**
* Mark a canvas legend touch tap so the following compatibility click can be skipped.
* @param {object} $$ ChartInternal instance
* @param {string} id Legend data id
* @private
*/
function markCanvasLegendTouchClick($$, id) {
	$$.state.canvasLegendLastTouchClickId = id;
	$$.state.canvasLegendLastTouchClickTime = Date.now();
}
/**
* Check if a canvas legend click duplicates a recent touch tap.
* @param {object} $$ ChartInternal instance
* @param {string} id Legend data id
* @returns {boolean} Whether the click is duplicate
* @private
*/
function isDuplicateCanvasLegendTouchClick($$, id) {
	const { state } = $$;
	const duplicate = state.canvasLegendLastTouchClickId === id && Date.now() - (state.canvasLegendLastTouchClickTime || 0) < CANVAS_LEGEND_TOUCH_CLICK_TIMEOUT;
	if (duplicate) {
		state.canvasLegendLastTouchClickId = null;
		state.canvasLegendLastTouchClickTime = 0;
	}
	return duplicate;
}
/**
* Bind canvas HTML legend interactions without invoking SVG focus paths.
* @param {object} $$ ChartInternal instance
* @param {object} item Legend item selection
* @private
*/
function bindCanvasHtmlLegendInteractions($$, item) {
	const { api, config, state } = $$;
	item.on("click dblclick mouseover mouseout touchstart touchmove touchend", null);
	if (!config.interaction_enabled) return;
	const interaction = config.legend_item_interaction;
	const eventType = typeof interaction === "object" && interaction?.dblclick ? "dblclick" : "click";
	const isTouch = state.inputType === "touch";
	const hasClickInteraction = interaction || isFunction(config.legend_item_onclick);
	const touchOption = isTouch ? getCanvasTouchListenerOption(config) : void 0;
	const handleCanvasLegendToggle = function(event, id) {
		if (!callFn(config.legend_item_onclick, api, id, !state.hiddenTargetIds.has(id))) {
			const selected = select(this);
			if (event.type === "dblclick" || event.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);
			}
			revertCanvasLegendTargetFocus($$);
		}
		isTouch && $$.hideTooltip?.();
	};
	item.on(eventType, hasClickInteraction ? function(event, id) {
		if (isTouch && event.type === "click" && isDuplicateCanvasLegendTouchClick($$, id)) return;
		handleCanvasLegendToggle.call(this, event, id);
	} : null);
	isTouch && eventType === "click" && hasClickInteraction && item.on("touchstart", function(event, id) {
		setCanvasLegendTouchStart($$, id, event);
	}, touchOption).on("touchmove", (event) => {
		updateCanvasLegendTouchMove($$, event);
	}, touchOption).on("touchend", function(event, id) {
		if (isCanvasLegendTouchTap($$, id, event)) {
			markCanvasLegendTouchClick($$, id);
			handleCanvasLegendToggle.call(this, event, id);
		}
	}, touchOption);
	!isTouch && item.on("mouseover", interaction || isFunction(config.legend_item_onover) ? function(event, id) {
		if (!callFn(config.legend_item_onover, api, id, !state.hiddenTargetIds.has(id))) {
			if (!state.transiting && $$.isTargetToShow(id)) {
				setCanvasHtmlLegendFocus($$, id);
				setCanvasLegendTargetFocus($$, id);
			}
		}
	} : null).on("mouseout", interaction || isFunction(config.legend_item_onout) ? function(event, id) {
		if (!callFn(config.legend_item_onout, api, id, !state.hiddenTargetIds.has(id))) {
			revertCanvasHtmlLegendFocus($$);
			revertCanvasLegendTargetFocus($$);
		}
	} : null);
}
/**
* Measure a legend text with the same SVG structure used by the default renderer.
* @param {object} $$ ChartInternal instance
* @param {string} text Legend text
* @returns {object|null} Text bounding rect
* @private
*/
function measureSvgLegendText($$, text) {
	const chart = $$.$el.chart?.node?.();
	const doc = chart?.ownerDocument;
	if (!chart || !doc) return null;
	const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
	const group = doc.createElementNS("http://www.w3.org/2000/svg", "g");
	const textElement = doc.createElementNS("http://www.w3.org/2000/svg", "text");
	svg.style.cssText = "position:absolute;visibility:hidden;left:-10000px;top:-10000px;";
	group.setAttribute("class", $LEGEND.legendItem);
	textElement.textContent = text;
	group.appendChild(textElement);
	svg.appendChild(group);
	chart.appendChild(svg);
	const rect = getBoundingRect(textElement);
	svg.remove();
	return rect;
}
/**
* Check if canvas can render the configured x axis type.
* @param {string} type X axis type
* @returns {boolean} Whether type is supported
* @private
*/
function isSupportedCanvasXType(type) {
	return /^(indexed|category|log|timeseries)$/.test(type);
}
/**
* Check if canvas can render the configured y/y2 axis type.
* @param {string} type Y axis type
* @returns {boolean} Whether type is supported
* @private
*/
function isSupportedCanvasYType(type) {
	return /^(indexed|log|timeseries)$/.test(type);
}
/**
* Get client position from mouse, pointer or touch event.
* @param {Event} event Input event
* @returns {object|null} Client position
* @private
*/
function getCanvasEventClientPoint(event) {
	const touchEvent = event;
	return touchEvent.changedTouches?.[0] || touchEvent.touches?.[0] || ("clientX" in event ? event : null);
}
/**
* Get data row under a canvas pointer event.
* @param {object} $$ ChartInternal instance
* @param {Event} event Input event
* @param {boolean} shapeOnly Whether to skip grouped x-index fallback
* @returns {object|null} Hit data row
* @private
*/
function getCanvasEventDatum($$, event, shapeOnly = false) {
	const point = getCanvasEventPoint($$, event);
	return point ? shapeOnly ? $$.hitDetector.findNearestShape?.(point[0], point[1]) : $$.hitDetector.findNearest(point[0], point[1]) : null;
}
/**
* Check whether a canvas pointer coordinate can select the grouped x index.
* @param {object} $$ ChartInternal instance
* @param {Array} point Canvas-local coordinates
* @returns {boolean} Whether x-index hover is allowed
* @private
*/
function isCanvasXIndexHoverArea($$, point) {
	const { config, state } = $$;
	const { height, margin, width, xAxisHeight } = state;
	const [x, y] = point;
	const left = margin.left;
	const right = margin.left + width;
	const top = margin.top;
	const bottom = margin.top + height;
	const axisBand = config.axis_x_show ? Math.max(xAxisHeight || 0, 24) : 0;
	if ($$.isMultipleX?.() || state.hasTreemap) return false;
	return config.axis_rotated ? y >= top && y <= bottom && x >= left - axisBand && x <= right : x >= left && x <= right && y >= top && y <= bottom + axisBand;
}
/**
* Check whether a canvas pointer coordinate is inside the main plot area.
* @param {object} $$ ChartInternal instance
* @param {Array} point Canvas-local coordinates
* @returns {boolean} Whether axis tooltip can be shown
* @private
*/
function isCanvasAxisTooltipArea($$, point) {
	const { state: { height, margin, width } } = $$;
	const [x, y] = point;
	return x >= margin.left && x <= margin.left + width && y >= margin.top && y <= margin.top + height;
}
/**
* Get data row for hover from a canvas-local coordinate.
* @param {object} $$ ChartInternal instance
* @param {Array} point Canvas-local coordinates
* @returns {object|null} Hover data row
* @private
*/
function getCanvasHoverDatumFromPoint($$, point) {
	return $$.hitDetector.findNearest(point[0], point[1]) || (isCanvasXIndexHoverArea($$, point) ? $$.hitDetector.findNearestIndexByCoord?.(point[0], point[1]) : null);
}
/**
* Get visible canvas data rows for a tooltip target.
* @param {object} $$ ChartInternal instance
* @param {object} d Base data row
* @returns {Array} Tooltip data rows
* @private
*/
function getCanvasTooltipData($$, d) {
	const { config, state } = $$;
	const targetsToShow = $$.filterTargetsToShow($$.data.targets);
	const pointBased = isCanvasPointBasedInteraction($$, d);
	const sameXData = config.tooltip_grouped && !state.hasTreemap && !pointBased ? $$.filterByX?.(targetsToShow, d.x) : null;
	return (state.hasTreemap ? [d] : pointBased ? [d] : config.tooltip_grouped ? sameXData?.length ? sameXData : targetsToShow.map((target) => target.values[d.index]).filter(Boolean) : [d]).map((v) => $$.addName?.(v) || v);
}
/**
* Get data rows for canvas focus rendering.
* @param {object} $$ ChartInternal instance
* @param {object} d Hover data row
* @param {Array} selectedData Tooltip data rows
* @returns {Array} Focus data rows
* @private
*/
function getCanvasFocusData($$, d, selectedData) {
	return $$.isMultipleX?.() && !isCanvasPointBasedInteraction($$, d) ? [$$.addName?.(d) || d] : selectedData;
}
/**
* Get canvas-local coordinates from mouse, pointer or touch event.
* @param {object} $$ ChartInternal instance
* @param {Event} event Input event
* @returns {Array|null} Canvas-local coordinates
* @private
*/
function getCanvasEventPoint($$, event) {
	const point = getCanvasEventClientPoint(event);
	if (!point) return null;
	const canvas = $$.$el.canvas.node();
	const rect = getBoundingRect(canvas, true);
	return [point.clientX - rect.left, point.clientY - rect.top];
}
/**
* Normalize a subchart domain range for the current x axis type.
* @param {object} $$ ChartInternal instance
* @param {Array} domain Domain range
* @returns {Array} Normalized domain range
* @private
*/
function normalizeCanvasSubchartDomain($$, domain) {
	if (!Array.isArray(domain) || domain.length < 2) return null;
	const values = domain.slice(0, 2);
	return $$.axis?.isTimeSeries?.() ? values.map((value) => parseDate.call($$, value)) : values;
}
/**
* Get the canvas-space rectangle occupied by the subchart plot.
* @param {object} $$ ChartInternal instance
* @returns {object|null} Subchart rectangle
* @private
*/
function getCanvasSubchartRect($$) {
	const { config, state } = $$;
	if (!config.subchart_show || !state.hasAxis || state.width2 <= 0 || state.height2 <= 0) return null;
	return {
		x: state.margin2.left,
		y: state.margin2.top,
		w: state.width2,
		h: state.height2
	};
}
/**
* Check whether a canvas point is inside the subchart plot area.
* @param {object} $$ ChartInternal instance
* @param {Array} point Canvas-local point
* @returns {boolean} Whether point is in the subchart
* @private
*/
function isCanvasSubchartPoint($$, point) {
	const rect = getCanvasSubchartRect($$);
	return !!rect && point[0] >= rect.x && point[0] <= rect.x + rect.w && point[1] >= rect.y && point[1] <= rect.y + rect.h;
}
/**
* Convert a canvas subchart point to the equivalent main plot point.
* @param {object} $$ ChartInternal instance
* @param {Array} point Canvas-local point
* @returns {Array|null} Main plot point or null when not applicable
* @private
*/
function getCanvasMainPointFromSubchart($$, point) {
	const { config, state } = $$;
	const rect = getCanvasSubchartRect($$);
	if (config.subchart_brush_enabled !== false || !rect || !isCanvasSubchartPoint($$, point)) return null;
	const mainCoord = getMainCoordFromSubchartCoord($$, config.axis_rotated ? point[1] - rect.y : point[0] - rect.x);
	if (mainCoord === null) return null;
	return config.axis_rotated ? [state.margin.left + state.width / 2, state.margin.top + mainCoord] : [state.margin.left + mainCoord, state.margin.top + state.height / 2];
}
/**
* Get the allowed local brush coordinate extent for canvas subchart interactions.
* @param {object} $$ ChartInternal instance
* @returns {Array} Brush extent in subchart-local pixels
* @private
*/
function getCanvasSubchartBrushExtent($$) {
	const rect = getCanvasSubchartRect($$);
	const axisLength = rect ? $$.config.axis_rotated ? rect.h : rect.w : 0;
	const extent = $$.axis?.getExtent?.();
	const values = Array.isArray(extent) ? extent.slice(0, 2) : [];
	if (values.length === 2 && values.every(Number.isFinite)) {
		const start = Math.max(0, Math.min(axisLength, values[0]));
		const end = Math.max(0, Math.min(axisLength, values[1]));
		return [Math.min(start, end), Math.max(start, end)];
	}
	return [0, axisLength];
}
/**
* Get the local brush coordinate from a canvas subchart pointer event.
* @param {object} $$ ChartInternal instance
* @param {Event} event Input event
* @param {boolean} clampToExtent Whether to clamp pointer outside brush extent
* @returns {number|null} Brush coordinate
* @private
*/
function getCanvasSubchartBrushCoord($$, event, clampToExtent = false) {
	const point = getCanvasEventPoint($$, event);
	const rect = point && getCanvasSubchartRect($$);
	if (!point || !rect || !isCanvasSubchartPoint($$, point)) return null;
	const raw = $$.config.axis_rotated ? point[1] - rect.y : point[0] - rect.x;
	const [min, max] = getCanvasSubchartBrushExtent($$);
	if (!clampToExtent && (raw < min || raw > max)) return null;
	return Math.max(min, Math.min(max, raw));
}
/**
* Build a scale domain from two subchart brush coordinates.
* @param {object} $$ ChartInternal instance
* @param {number} start Brush start coordinate
* @param {number} end Brush end coordinate
* @returns {Array} Domain range
* @private
*/
function getCanvasSubchartDomainFromCoords($$, start, end) {
	const coordStart = Math.min(start, end);
	const coordEnd = Math.max(start, end);
	return [$$.scale.subX.invert(coordStart), $$.scale.subX.invert(coordEnd)];
}
/**
* Get current canvas subchart brush selection in local subchart pixels.
* @param {object} $$ ChartInternal instance
* @returns {Array|null} Brush selection pixels
* @private
*/
function getCanvasSubchartBrushSelection($$) {
	const { scale, state } = $$;
	const domain = state.domain;
	if (!domain?.length || !scale.subX) return null;
	const rect = getCanvasSubchartRect($$);
	const p0 = scale.subX(domain[0]);
	const p1 = scale.subX(domain[1]);
	if (!rect || !Number.isFinite(p0) || !Number.isFinite(p1)) return null;
	const [min, max] = getCanvasSubchartBrushExtent($$);
	return [Math.max(min, Math.min(max, Math.min(p0, p1))), Math.max(min, Math.min(max, Math.max(p0, p1)))];
}
/**
* Get the subchart brush mode at a local brush coordinate.
* @param {object} $$ ChartInternal instance
* @param {number} coord Brush coordinate
* @returns {string} Brush mode
* @private
*/
function getCanvasSubchartBrushMode($$, coord) {
	const selection = getCanvasSubchartBrushSelection($$);
	if (!selection) return "select";
	const [start, end] = selection;
	if (Math.abs(coord - start) <= CANVAS_SUBCHART_HANDLE_SIZE) return "resize-start";
	if (Math.abs(coord - end) <= CANVAS_SUBCHART_HANDLE_SIZE) return "resize-end";
	return coord > start && coord < end ? "move" : "select";
}
/**
* Clamp a subchart brush selection to the subchart extent.
* @param {object} $$ ChartInternal instance
* @param {number} start Selection start pixel
* @param {number} end Selection end pixel
* @returns {Array} Clamped selection pixels
* @private
*/
function clampCanvasSubchartSelection($$, start, end) {
	const [min, max] = getCanvasSubchartBrushExtent($$);
	return [Math.max(min, Math.min(max, start)), Math.max(min, Math.min(max, end))];
}
/**
* Get cursor for a canvas subchart brush mode.
* @param {object} $$ ChartInternal instance
* @param {string} mode Brush mode
* @returns {string} CSS cursor
* @private
*/
function getCanvasSubchartCursor($$, mode) {
	if (/^resize/.test(mode)) return $$.config.axis_rotated ? "ns-resize" : "ew-resize";
	return mode === "move" ? "move" : "crosshair";
}
/**
* Get stable key for canvas data callbacks.
* @param {object} d Data row
* @returns {string|null} Data key
* @private
*/
function getCanvasDataKey(d) {
	return d ? `${d.id}:${d.index}` : null;
}
/**
* Get canvas drag selection delta rows from current and previous hit rows.
* @param {object} $$ ChartInternal instance
* @param {Array} dataRows Current drag hit rows
* @returns {object} Current included keys and rows to toggle
* @private
*/
function getCanvasSelectionDragDelta($$, dataRows) {
	const previousKeys = $$.state.canvasSelectionDragIncluded;
	const previousRows = canvasSelectionDragRows.get($$) || /* @__PURE__ */ new Map();
	const currentRows = /* @__PURE__ */ new Map();
	const included = /* @__PURE__ */ new Set();
	const rows = [];
	dataRows.filter((d) => isCanvasSelectableData($$, d)).forEach((d) => {
		const key = getCanvasDataKey(d);
		if (key) {
			included.add(key);
			!currentRows.has(key) && currentRows.set(key, d);
		}
	});
	currentRows.forEach((d, key) => {
		!previousKeys.has(key) && rows.push(d);
	});
	previousRows.forEach((d, key) => {
		!included.has(key) && rows.push(d);
	});
	canvasSelectionDragRows.set($$, currentRows);
	return {
		included,
		rows
	};
}
/**
* Normalize a data id or id list.
* @param {string|Array} ids Data ids
* @returns {Array|null} Normalized ids
* @private
*/
function getCanvasSelectionIds(ids) {
	return ids ? Array.isArray(ids) ? ids : [ids] : null;
}
/**
* Check whether a canvas data row participates in selection.
* @param {object} $$ ChartInternal instance
* @param {object} d Data row
* @returns {boolean} Whether the row can be selected
* @private
*/
function isCanvasSelectableData($$, d) {
	return d && hasCanvasDrawableValue($$, d) && isCanvasTargetSupported($$, d, CANVAS_SELECTABLE_TYPE_FILTERS) && CANVAS_SELECTABLE_TYPE_FILTERS.some((filter) => filter($$, d)) && $$.config.data_selection_isselectable.bind($$.api)(d);
}
/**
* Iterate selectable canvas rows.
* @param {object} $$ ChartInternal instance
* @param {function} callback Row callback
* @private
*/
function eachCanvasSelectableData($$, callback) {
	$$.filterTargetsToShow($$.data.targets).filter((target) => isCanvasTargetSupported($$, target, CANVAS_SELECTABLE_TYPE_FILTERS)).forEach((target) => {
		target.values.forEach((d) => {
			isCanvasSelectableData($$, d) && callback(d);
		});
	});
}
/**
* Check whether a canvas data row should interact as a single point.
* @param {object} $$ ChartInternal instance
* @param {object} d Data row
* @returns {boolean} Whether the row should stay ungrouped
* @private
*/
function isCanvasPointBasedInteraction($$, d) {
	return !($$.config.axis_x_forceAsSingle && $$.config.tooltip_grouped) && (isCanvasScatterType($$, d) || isCanvasBubbleType($$, d));
}
/**
* Check if touch input is enabled by option.
* @param {object} config Config object
* @returns {boolean} Whether touch is enabled
* @private
*/
function isCanvasTouchEnabled(config) {
	return config.interaction_inputType_touch !== false;
}
/**
* Get touch listener passive option following interaction.inputType.touch.preventDefault.
* @param {object} config Config object
* @returns {object} Event listener options
* @private
*/
function getCanvasTouchListenerOption(config) {
	const preventDefault = config.interaction_inputType_touch?.preventDefault;
	return { passive: !(isBoolean(preventDefault) && preventDefault || false) && (!isNaN(preventDefault) && preventDefault || null) === null };
}
/**
* Create touch preventDefault handler following interaction.inputType.touch.preventDefault.
* @param {object} config Config object
* @returns {function} Touch preventer
* @private
*/
function getCanvasTouchPreventer(config) {
	const preventDefault = config.interaction_inputType_touch?.preventDefault;
	const isPrevented = isBoolean(preventDefault) && preventDefault || false;
	const preventThreshold = !isNaN(preventDefault) && preventDefault || null;
	let startPx;
	return (event) => {
		const touch = event.changedTouches?.[0] || event.touches?.[0];
		if (!touch) return;
		const currentXY = touch[`client${config.axis_rotated ? "Y" : "X"}`];
		if (event.type === "touchstart") {
			if (isPrevented) event.preventDefault();
			else if (preventThreshold !== null) startPx = currentXY;
		} else if (event.type === "touchmove" && (isPrevented || startPx === true || preventThreshold !== null && Math.abs(startPx - currentXY) >= preventThreshold)) {
			startPx = true;
			event.preventDefault();
		}
	};
}
/**
* Check whether a pointer event should be handled by canvas pointer path.
* @param {object} $$ ChartInternal instance
* @param {PointerEvent} event Pointer event
* @returns {boolean} Whether event should be handled
* @private
*/
function shouldHandleCanvasPointerEvent($$, event) {
	return event.pointerType !== "mouse" && $$.state.inputType !== "touch";
}
/**
* Check if click event duplicates a recent touch/pointer data click.
* @param {object} $$ ChartInternal instance
* @param {object} d Data row
* @returns {boolean} Whether the click should skip data callback
* @private
*/
function isDuplicateCanvasInputClick($$, d) {
	const { state } = $$;
	const key = getCanvasDataKey(d);
	const duplicate = key && state.canvasLastInputClickKey === key && Date.now() - (state.canvasLastInputClickTime || 0) < 750;
	if (duplicate) {
		state.canvasLastInputClickKey = null;
		state.canvasLastInputClickTime = 0;
	}
	return Boolean(duplicate);
}
/**
* Mark a touch/pointer data click so the following compatibility click can be ignored.
* @param {object} $$ ChartInternal instance
* @param {object} d Data row
* @private
*/
function markCanvasInputClick($$, d) {
	$$.state.canvasLastInputClickKey = getCanvasDataKey(d);
	$$.state.canvasLastInputClickTime = Date.now();
}
/**
* Get current animation timestamp.
* @returns {number} Timestamp
* @private
*/
function getCanvasAnimationTime() {
	return win.performance?.now?.() ?? Date.now();
}
/**
* Check whether a domain can be interpolated for canvas flow.
* @param {Array} domain Domain values
* @param {boolean} isLog Whether the domain is in log scale
* @returns {boolean} Whether domain is numeric/date-like
* @private
*/
function isCanvasFlowDomain(domain, isLog = false) {
	return Array.isArray(domain) && domain.length >= 2 && domain.every((v) => Number.isFinite(+v) && (!isLog || +v > 0));
}
/**
* Interpolate numeric/date domain values.
* @param {Array} start Start domain
* @param {Array} end End domain
* @param {number} ratio Interpolation ratio
* @param {boolean} isLog Whether to interpolate in log space
* @returns {Array} Interpolated domain
* @private
*/
function interpolateCanvasFlowDomain(start, end, ratio, isLog = false) {
	return start.slice(0, 2).map((value, index) => {
		const next = isLog ? Math.exp(Math.log(+value) + (Math.log(+end[index]) - Math.log(+value)) * ratio) : +value + (+end[index] - +value) * ratio;
		return value instanceof Date || end[index] instanceof Date ? new Date(next) : next;
	});
}
/**
* Count current canvas flow values.
* @param {object} $$ ChartInternal instance
* @returns {number} Total values
* @private
*/
function getCanvasFlowValueCount($$) {
	return $$.data.targets.reduce((sum, target) => sum + target.values.length, 0);
}
/**
* Sync y/y2 domains for canvas flow frames after data has been appended.
* @param {object} $$ ChartInternal instance
* @private
*/
function syncCanvasFlowYDomains($$) {
	const { scale } = $$;
	const targetsToShow = $$.filterTargetsToShow($$.data.targets);
	["y", "y2"].forEach((key) => {
		scale[key]?.domain($$.getYDomain(targetsToShow, key));
	});
	$$.withSubchartTypeContext(() => {
		scale.subY?.domain($$.getYDomain(targetsToShow, "y"));
		scale.subY2?.domain($$.getYDomain(targetsToShow, "y2"));
	});
}
const canvasInternal = {
	/**
	* Normalize unsupported options before initializing canvas mode.
	* @private
	*/
	prepareCanvasConfig() {
		const { config, state } = this;
		this.warnUnsupportedCanvasOptions();
		if (state.orgConfig?.transition && Object.prototype.hasOwnProperty.call(state.orgConfig.transition, "duration") && state.orgConfig.transition.duration) warn("canvas mode: transition.duration is ignored; canvas redraws synchronously.");
		config.transition_duration = 0;
		if (!isSupportedCanvasXType(config.axis_x_type)) config.axis_x_type = "indexed";
		if (!isSupportedCanvasYType(config.axis_y_type)) config.axis_y_type = "indexed";
		if (!isSupportedCanvasYType(config.axis_y2_type)) config.axis_y2_type = "indexed";
		if (this.hasType?.("bubble")) {
			config.point_show = true;
			config.point_type = "circle";
		}
	},
	/**
	* Apply the current canvas subchart selection to the main x domain.
	* @private
	*/
	applyCanvasSubchartDomain() {
		const $$ = this;
		const { axis, config, scale, state } = $$;
		if (!config.subchart_show || !state.hasAxis || !scale.subX) return;
		if (!state.rendered && !state.domain && config.subchart_init_range) state.domain = normalizeCanvasSubchartDomain($$, config.subchart_init_range);
		const domain = normalizeCanvasSubchartDomain($$, state.domain);
		if (!domain || !$$.withinRange(domain, $$.getZoomDomain("subX", true), $$.getZoomDomain("subX"))) return;
		state.domain = domain;
		scale.x.domain(domain);
		axis.x.scale(scale.x);
	},
	/**
	* Set the canvas subchart selection domain and redraw the main chart.
	* @param {Array} domain Domain range
	* @param {boolean} redraw Whether to redraw immediately
	* @param {boolean} callCallback Whether to call subchart.onbrush
	* @returns {Array|undefined} Applied domain range
	* @private
	*/
	setCanvasSubchartDomain(domain, redraw = true, callCallback = false) {
		const $$ = this;
		const { config, scale, state } = $$;
		const nextDomain = normalizeCanvasSubchartDomain($$, domain);
		if (!config.subchart_show || !nextDomain || !$$.withinRange(nextDomain, $$.getZoomDomain("subX", true), $$.getZoomDomain("subX"))) return;
		state.domain = nextDomain;
		scale.zoom = null;
		if (redraw) $$.redraw({
			withTransition: false,
			withY: config.zoom_rescale,
			withSubchart: false,
			withEventRect: false,
			withDimension: false
		});
		callCallback && config.subchart_onbrush.bind($$.api)(state.domain);
		return state.domain;
	},
	/**
	* Clear the canvas subchart brush selection and restore the full x domain.
	* @param {boolean} redraw Whether to redraw immediately
	* @param {boolean} callCallback Whether to call subchart.onbrush
	* @private
	*/
	clearCanvasSubchartDomain(redraw = true, callCallback = false) {
		const $$ = this;
		const { axis, config, org, scale, state } = $$;
		if (!config.subchart_show) return;
		state.domain = void 0;
		scale.zoom = null;
		if (org.xDomain?.length) {
			scale.x.domain(org.xDomain);
			axis.x.scale(scale.x);
		}
		if (redraw) $$.redraw({
			withTransition: false,
			withUpdateXDomain: true,
			withUpdateOrgXDomain: false,
			withY: config.zoom_rescale,
			withSubchart: false,
			withEventRect: false,
			withDimension: false
		});
		callCallback && config.subchart_onbrush.bind($$.api)(scale.x.orgDomain());
	},
	/**
	* Finish a pending canvas flow animation before accepting another flow mutation.
	* @private
	*/
	flushCanvasFlow() {
		const { state } = this;
		if (!state.canvasFlowFinish) return;
		state.canvasFlowFrame !== null && win.cancelAnimationFrame?.(state.canvasFlowFrame);
		state.canvasFlowFrame = null;
		state.canvasFlowFinish();
	},
	/**
	* Animate canvas flow by interpolating the x domain, then commit the final data window.
	* @param {object} flow Flow metadata
	* @returns {boolean} Whether animation was started
	* @private
	*/
	animateCanvasFlow(flow) {
		const $$ = this;
		const { axis, data, org, scale, state } = $$;
		const { done = () => {}, duration, length, orgDataCount } = flow;
		const requestFrame = win.requestAnimationFrame?.bind(win);
		const isLog = !!axis.isLog?.("x");
		if (!requestFrame || !duration || !length || !orgDataCount || getCanvasFlowValueCount($$) > CANVAS_FLOW_ANIMATION_MAX_VALUES) return false;
		const startDomain = scale.x.domain().slice(0, 2);
		const startOrgDomain = org.xDomain?.slice?.();
		const startSubXDomain = scale.subX?.domain?.().slice?.();
		if (!isCanvasFlowDomain(startDomain, isLog)) return false;
		const removed = data.targets.map((target) => target.values.splice(0, length));
		$$.updateXDomain($$.filterTargetsToShow(data.targets), true, true);
		const endDomain = scale.x.domain().slice(0, 2);
		const endOrgDomain = org.xDomain?.slice?.();
		data.targets.forEach((target, index) => {
			target.values.unshift(...removed[index]);
		});
		if (!isCanvasFlowDomain(endDomain, isLog)) {
			startOrgDomain && (org.xDomain = startOrgDomain);
			scale.x.domain(startDomain);
			startSubXDomain && scale.subX.domain(startSubXDomain);
			axis.x.scale(scale.x);
			return false;
		}
		syncCanvasFlowYDomains($$);
		let finished = false;
		const started = getCanvasAnimationTime();
		const finish = () => {
			if (finished) return;
			finished = true;
			data.targets.forEach((target) => {
				target.values.splice(0, length);
			});
			endOrgDomain && (org.xDomain = endOrgDomain);
			scale.x.domain(endDomain);
			scale.subX?.domain?.(endOrgDomain ?? endDomain);
			axis.x.scale(scale.x);
			state.dirty.data = true;
			state._eventRectFingerprint = null;
			state.canvasShape = null;
			state._cachedDrawShape = null;
			$$.redraw({
				withLegend: true,
				withTransition: false,
				withTrimXDomain: false,
				withUpdateOrgXDomain: true,
				withUpdateXAxis: true,
				withUpdateXDomain: true
			});
			$$.updateTypesElements();
			state.canvasFlowFrame = null;
			state.canvasFlowFinish = null;
			state.flowing = false;
			done.call($$.api);
		};
		const render = (timestamp) => {
			const ratio = Math.min(1, Math.max(0, (timestamp - started) / duration));
			const domain = interpolateCanvasFlowDomain(startDomain, endDomain, ratio, isLog);
			scale.x.domain(domain);
			axis.x.scale(scale.x);
			state.canvasShape = null;
			state._cachedDrawShape = null;
			state._canvasVisibleRangeCache = null;
			state._canvasXTickValuesCache = null;
			$$.renderCanvasFrame(void 0, null, false);
			if (ratio < 1) state.canvasFlowFrame = requestFrame(render);
			else finish();
		};
		startOrgDomain && (org.xDomain = startOrgDomain);
		scale.x.domain(startDomain);
		startSubXDomain && scale.subX.domain(startSubXDomain);
		axis.x.scale(scale.x);
		state.flowing = true;
		state.cancelClick = true;
		$$.hideTooltip?.();
		state.canvasFlowFinish = finish;
		state.canvasFlowFrame = requestFrame(render);
		return true;
	},
	/**
	* Initialize canvas renderers, theme, hit detector and event bindings.
	* @private
	*/
	initCanvas() {
		const $$ = this;
		const { config, state, $el } = $$;
		const container = $el.chart.node();
		state.isCanvasMode = true;
		state.canvasInlineStyle.minHeight = container.style.minHeight;
		if (win.getComputedStyle(container).position === "static") $el.chart.style("position", "relative");
		$el.chart.style("min-height", `${state.current.height}px`);
		$$.canvasEngine = new CanvasEngine();
		$$.canvasEngine.init(container, state.current.width, $$.getCanvasSurfaceHeight());
		$el.canvas = select($$.canvasEngine.canvas);
		$$.canvasTheme = new CanvasTheme();
		$$.canvasTheme.load(container, config.canvas_theme);
		$$.canvasAxisRenderer = new CanvasAxisRenderer($$.canvasEngine, $$.canvasTheme);
		$$.canvasRenderer = new CanvasRenderer($$.canvasEngine, $$.canvasTheme);
		$$.hitDetector = new HitDetector();
		$$.bindCanvasEvents();
		config.zoom_enabled && $$.bindZoomEvent?.();
	},
	/**
	* Warn about options that canvas mode does not support yet.
	* @private
	*/
	warnUnsupportedCanvasOptions() {
		const { config } = this;
		[
			[!isSupportedCanvasXType(config.axis_x_type), "axis.x.type other than indexed/category/log/timeseries"],
			[!isSupportedCanvasYType(config.axis_y_type), "axis.y.type other than indexed/log/timeseries"],
			[!isSupportedCanvasYType(config.axis_y2_type), "axis.y2.type other than indexed/log/timeseries"],
			[config.boost_useCssRule, "boost.useCssRule"],
			[this.hasArcType?.(), "arc charts"],
			[this.hasType?.("radar"), "radar chart"],
			[this.hasType?.("polar"), "polar chart"],
			[this.hasType?.("funnel"), "funnel chart"]
		].forEach(([condition, name]) => {
			condition && warn(`canvas mode: ${name} is not yet supported.`);
		});
	},
	/**
	* Bind pointer and click handlers to the canvas surface.
	* @private
	*/
	bindCanvasEvents() {
		const $$ = this;
		const { config, state, $el } = $$;
		const canvas = $el.canvas.node();
		const preventTouchEvent = getCanvasTouchPreventer(config);
		const touchOption = getCanvasTouchListenerOption(config);
		canvas.addEventListener("mousemove", $$.onCanvasMouseMove.bind($$));
		canvas.addEventListener("mouseenter", (event) => {
			state.event = event;
			config.onover?.bind($$.api)(event);
		});
		canvas.addEventListener("mouseout", $$.onCanvasMouseOut.bind($$));
		canvas.addEventListener("mouseleave", (event) => {
			state.event = event;
			config.onout?.bind($$.api)(event);
		});
		canvas.addEventListener("mousedown", (event) => {
			if ($$.onCanvasSubchartBrushStart?.(event)) return;
			$$.onCanvasSelectionDragStart(event);
		});
		canvas.addEventListener("click", $$.onCanvasClick.bind($$));
		canvas.addEventListener("pointerdown", $$.onCanvasPointerDown.bind($$));
		canvas.addEventListener("pointerenter", $$.onCanvasPointerEnter.bind($$));
		canvas.addEventListener("pointermove", $$.onCanvasPointerMove.bind($$));
		canvas.addEventListener("pointerup", $$.onCanvasPointerUp.bind($$));
		canvas.addEventListener("pointerleave", $$.onCanvasPointerOut.bind($$));
		canvas.addEventListener("pointercancel", $$.onCanvasPointerCancel.bind($$));
		if (isCanvasTouchEnabled(config)) {
			canvas.addEventListener("touchstart", (event) => {
				preventTouchEvent(event);
				$$.onCanvasTouchStart(event);
			}, touchOption);
			canvas.addEventListener("touchmove", (event) => {
				preventTouchEvent(event);
				$$.onCanvasTouchMove(event);
			}, touchOption);
			canvas.addEventListener("touchend", $$.onCanvasTouchEnd.bind($$), touchOption);
			canvas.addEventListener("touchcancel", $$.onCanvasTouchCancel.bind($$), touchOption);
		}
	},
	/**
	* Render and update the HTML legend used by canvas mode.
	* @private
	*/
	updateHtmlLegend() {
		const $$ = this;
		const { config, state, $el } = $$;
		const chart = $el.chart.node();
		if (!config.legend_show || state.hasTreemap) {
			if (state.hasTreemap) {
				$el.legend?.remove();
				$el.legend = null;
			} else $el.legend?.style("visibility", "hidden");
			state.legendItemWidth = 0;
			state.legendItemHeight = 0;
			state.legendStep = 0;
			return;
		}
		const targetIds = $$.mapToIds($$.data.targets).filter((id) => config.data_names[id] !== null);
		if (config.legend_contents_bindto && config.legend_contents_template) {
			if ($$.updateHtmlLegendTemplate(targetIds)) {
				state.legendItemWidth = 0;
				state.legendItemHeight = 0;
				state.legendStep = 0;
				state.legendHasRendered = true;
			} else {
				state.legendItemWidth = 0;
				state.legendItemHeight = 0;
				state.legendStep = 0;
			}
			return;
		}
		if (!$el.legend) $el.legend = select(chart).append("div").classed($LEGEND.legend, true).classed($CANVAS.legend, true);
		$el.legend.style("visibility", null);
		const legendItems = $el.legend.selectAll(`button.${$LEGEND.legendItem}`).data(targetIds, (id) => id);
		legendItems.exit().remove();
		const enter = legendItems.enter().append("button").attr("type", "button").attr("data-id", (id) => id);
		enter.append("span").classed($LEGEND.legendItemTile, true);
		enter.append("span").classed($CANVAS.legendItemTitle, true);
		const item = enter.merge(legendItems).attr("class", (id) => $$.generateClass($LEGEND.legendItem, id).trim());
		setCanvasHtmlLegendItem($$, item);
		item.select(`.${$LEGEND.legendItemTile}`).classed($CANVAS.legendItemTileCircle, !config.legend_usePoint && config.legend_item_tile_type === "circle").style("background-color", (id) => config.legend_usePoint ? null : $$.color(id)).html((id) => config.legend_usePoint ? getCanvasLegendPointIcon($$, id) : "");
		item.select(`.${$CANVAS.legendItemTitle}`).text((id) => getLegendText($$, id));
		if (config.legend_tooltip) item.attr("title", (id) => getLegendText($$, id));
		bindCanvasHtmlLegendInteractions($$, item);
		$$.updateHtmlLegendSize(targetIds);
		$$.positionHtmlLegend();
		state.legendHasRendered = true;
	},
	/**
	* Render canvas mode legend into custom HTML template container.
	* @param {Array} targetIds Legend target ids
	* @returns {boolean} Whether template legend was rendered
	* @private
	*/
	updateHtmlLegendTemplate(targetIds) {
		const $$ = this;
		const { api, config, $el } = $$;
		const wrapper = select(config.legend_contents_bindto);
		const template = config.legend_contents_template;
		const ids = [];
		let html = "";
		if (wrapper.empty()) return false;
		targetIds.forEach((id) => {
			const content = isFunction(template) ? sanitize(template.call(api, id, $$.color(id), api.data(id)[0].values)) : tplProcess(template, {
				COLOR: $$.color(id),
				TITLE: id
			});
			if (content) {
				ids.push(id);
				html += content;
			}
		});
		const legendItem = wrapper.html(html).classed($LEGEND.legend, true).classed($CANVAS.legend, false).style("visibility", null).selectAll(function() {
			return this.children;
		}).data(ids);
		setCanvasHtmlLegendItem($$, legendItem);
		bindCanvasHtmlLegendInteractions($$, legendItem);
		$el.legend = wrapper;
		return true;
	},
	/**
	* Measure HTML legend items and update legend layout state.
	* @param {Array} targetIds Visible legend target ids
	* @private
	*/
	updateHtmlLegendSize(targetIds) {
		const $$ = this;
		const { config, state, $el } = $$;
		const itemSizes = [];
		const itemLayouts = {};
		let maxWidth = 0;
		let maxHeight = 0;
		let legendStep = 0;
		const useTemplateLegend = config.legend_contents_bindto && config.legend_contents_template;
		if (useTemplateLegend) $el.legend.selectAll(`.${$LEGEND.legendItem}`).each(function(id) {
			const rect = getBoundingRect(this, true);
			const text = `${getLegendText($$, id)}`;
			const fallbackWidth = Math.max(32, text.length * 7 + 24);
			const fallbackHeight = 20;
			const width = Math.ceil(rect.width || fallbackWidth);
			const height = Math.ceil(rect.height || fallbackHeight);
			itemSizes.push(width);
			maxWidth = Math.max(maxWidth, width);
			maxHeight = Math.max(maxHeight, height);
		});
		else if (targetIds.length) {
			const isRightOrInset = state.isLegendRight || state.isLegendInset;
			const isRectangle = config.legend_item_tile_type !== "circle";
			const itemTileSize = {
				width: isRectangle ? config.legend_item_tile_width : config.legend_item_tile_r * 2,
				height: isRectangle ? config.legend_item_tile_height : config.legend_item_tile_r * 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: {}
			};
			const measured = targetIds.map((id, index) => {
				const text = `${getLegendText($$, id)}`;
				const rect = measureSvgLegendText($$, text);
				const fallbackWidth = Math.max(32, text.length * 7);
				const fallbackHeight = 12;
				const isLast = index === targetIds.length - 1;
				const hidden = config.legend_show && !$$.isLegendToShow(id);
				const width = hidden ? 0 : (rect?.width || fallbackWidth) + dimension.tileWidth + (isLast && !isRightOrInset ? 0 : dimension.padding.right) + config.legend_padding;
				const height = hidden ? 0 : (rect?.height || fallbackHeight) + dimension.padding.top;
				dimension.max.width = Math.max(dimension.max.width, width);
				dimension.max.height = Math.max(dimension.max.height, height);
				return {
					id,
					hidden,
					width,
					height
				};
			});
			const areaLength = isRightOrInset ? $$.getLegendHeight() : $$.getLegendWidth();
			const updateValues = (id, itemLength, withoutStep = false) => {
				let margin;
				if (!withoutStep) {
					margin = (areaLength - dimension.totalLength - itemLength) / 2;
					if (margin < dimension.posMin) {
						margin = (areaLength - itemLength) / 2;
						dimension.totalLength = 0;
						dimension.step++;
					}
				}
				sizes.steps[id] = dimension.step;
				sizes.margins[dimension.step] = state.isLegendInset ? 10 : margin;
				sizes.offsets[id] = dimension.totalLength;
				dimension.totalLength += itemLength;
			};
			if (state.isLegendInset) {
				dimension.step = config.legend_inset_step ? config.legend_inset_step : targetIds.length;
				state.legendStep = dimension.step;
			}
			measured.forEach(({ id, hidden, width, height }) => {
				const itemWidth = config.legend_equally && !hidden ? dimension.max.width : width;
				const itemHeight = config.legend_equally && !hidden ? dimension.max.height : height;
				const itemLength = isRightOrInset ? itemHeight : itemWidth;
				sizes.widths[id] = itemWidth;
				sizes.heights[id] = itemHeight;
				itemSizes.push(itemWidth);
				maxWidth = Math.max(maxWidth, itemWidth);
				maxHeight = Math.max(maxHeight, itemHeight);
				if (hidden) {
					sizes.steps[id] = 0;
					sizes.offsets[id] = 0;
					return;
				}
				updateValues(id, itemLength);
			});
			const xForLegend = state.isLegendRight ? (id) => dimension.max.width * sizes.steps[id] : state.isLegendInset ? (id) => dimension.max.width * sizes.steps[id] + 10 : (id) => sizes.margins[sizes.steps[id]] + sizes.offsets[id];
			const yForLegend = state.isLegendRight || state.isLegendInset ? (id) => sizes.margins[sizes.steps[id]] + sizes.offsets[id] : (id) => dimension.max.height * sizes.steps[id];
			const titleX = 4 + itemTileSize.width;
			const tileCenterY = 9;
			measured.forEach(({ id, hidden }) => {
				const x = hidden ? 0 : xForLegend(id);
				const y = hidden ? 0 : yForLegend(id);
				itemLayouts[id] = {
					item: {
						x,
						y: y - 5,
						width: sizes.widths[id],
						height: sizes.heights[id]
					},
					title: {
						x: titleX,
						y: 0,
						height: sizes.heights[id]
					},
					tile: {
						x: -2,
						y: tileCenterY - itemTileSize.height / 2,
						width: itemTileSize.width,
						height: itemTileSize.height
					}
				};
			});
			legendStep = dimension.step;
		}
		if (!targetIds.length) {
			state.legendItemWidth = 0;
			state.legendItemHeight = 0;
			state.legendStep = 0;
			return;
		}
		if (useTemplateLegend) {
			const availableWidth = Math.max(1, state.current.width);
			const rowGap = 10;
			let rows = 1;
			let rowWidth = 0;
			itemSizes.forEach((width) => {
				if (rowWidth && rowWidth + rowGap + width > availableWidth) {
					rows++;
					rowWidth = width;
				} else rowWidth += (rowWidth ? rowGap : 0) + width;
			});
			state.legendStep = Math.max(0, rows - 1);
		} else state.legendStep = legendStep;
		state.legendItemWidth = maxWidth;
		state.legendItemHeight = Math.max(12, maxHeight);
		if (!useTemplateLegend) $el.legend.selectAll(`button.${$LEGEND.legendItem}`).each(function(id) {
			const layout = itemLayouts[id];
			const item = select(this);
			if (!layout) return;
			item.style("left", `${layout.item.x}px`).style("top", `${layout.item.y}px`).style("width", `${layout.item.width}px`).style("height", `${layout.item.height}px`);
			item.select(`.${$CANVAS.legendItemTitle}`).style("left", `${layout.title.x}px`).style("top", `${layout.title.y}px`).style("height", `${layout.title.height}px`).style("line-height", `${layout.title.height}px`);
			item.select(`.${$LEGEND.legendItemTile}`).style("left", `${layout.tile.x}px`).style("top", `${layout.tile.y}px`).style("width", `${layout.tile.width}px`).style("height", `${layout.tile.height}px`);
		});
	},
	/**
	* Get bottom legend block height separated from the canvas drawing surface.
	* @returns {number} Legend block height
	* @private
	*/
	getCanvasBottomLegendHeight() {
		const $$ = this;
		const { config, state, $el } = $$;
		if (!config.legend_show || state.isLegendRight || state.isLegendInset || state.hasTreemap || !$el.legend || !state.legendItemHeight) return 0;
		const rectHeight = getBoundingRect($el.legend.node(), true).height;
		const rowHeight = state.legendItemHeight * (state.legendStep + 1);
		return Math.ceil(Math.max($$.getLegendHeight?.() || 0, rectHeight || rowHeight));
	},
	/**
	* Get canvas drawing surface height excluding separately rendered bottom legend DOM.
	* @returns {number} Canvas drawing surface height
	* @private
	*/
	getCanvasSurfaceHeight() {
		const $$ = this;
		return Math.max(0, $$.state.current.height - $$.getCanvasBottomLegendHeight());
	},
	/**
	* Position the HTML legend around or inside the canvas chart.
	* @private
	*/
	positionHtmlLegend() {
		const $$ = this;
		const { config, state, $el } = $$;
		const legend = $el.legend;
		legend.classed($CANVAS.legendBottom, !state.isLegendRight && !state.isLegendInset).classed($CANVAS.legendRight, state.isLegendRight).classed($CANVAS.legendInset, state.isLegendInset).style("top", null).style("right", null).style("bottom", null).style("left", null).style("width", null).style("height", null).style("min-height", null);
		if (state.isLegendRight) legend.style("top", `${state.margin3.top}px`).style("right", "0").style("bottom", `${state.margin3.bottom}px`).style("width", `${Math.max(0, $$.getLegendWidth())}px`);
		else if (!state.isLegendInset) {
			const legendHeight = $$.getLegendHeight?.() || 0;
			legend.style("top", `${Math.max(0, state.current.height - legendHeight)}px`).style("right", "0").style("bottom", "auto").style("left", "0");
		} else if (state.isLegendInset) {
			const isTop = state.isLegendTop;
			const isLeft = state.isLegendLeft;
			legend.style(isTop ? "top" : "bottom", `${config.legend_inset_y}px`).style(isLeft ? "left" : "right", `${config.legend_inset_x}px`).style("width", `${Math.max(0, state.legendItemWidth * (state.legendStep + 1) + 20)}px`);
		}
	},
	/**
	* Get selected canvas data rows.
	* @param {string} targetId Target id
	* @returns {Array} Selected rows
	* @private
	*/
	getCanvasSelectedData(targetId) {
		const $$ = this;
		const selected = $$.state.canvasSelection;
		const data = [];
		if (!$$.config.data_selection_enabled || !selected?.size) return data;
		eachCanvasSelectableData($$, (d) => {
			(!targetId || d.id === targetId) && selected.has(getCanvasDataKey(d)) && data.push(d);
		});
		return data;
	},
	/**
	* Select or unselect canvas data rows.
	* @param {boolean} isSelection Whether to select
	* @param {string|Array} ids Target ids
	* @param {Array} indices Target indices
	* @param {boolean} resetOther Whether to reset unrelated selections
	* @private
	*/
	setCanvasSelection(isSelection = false, ids, indices, resetOther) {
		const $$ = this;
		const { config, state } = $$;
		const selected = state.canvasSelection;
		const selectionGrouped = config.data_selection_grouped;
		let targetIds = getCanvasSelectionIds(ids);
		let changed = false;
		const singleSelection = isSelection && !config.data_selection_multiple;
		let resetDone = !singleSelection;
		if (!config.data_selection_enabled) return;
		if (singleSelection) {
			indices = indices?.length ? [indices[0]] : [0];
			if (!selectionGrouped) {
				if (targetIds) targetIds = targetIds.slice(0, 1);
				else {
					const [firstTarget] = $$.filterTargetsToShow($$.data.targets);
					targetIds = firstTarget ? [firstTarget.id] : [];
				}
			}
		}
		eachCanvasSelectableData($$, (d) => {
			const key = getCanvasDataKey(d);
			const isTargetId = selectionGrouped || !targetIds || targetIds.indexOf(d.id) >= 0;
			const isTargetIndex = !indices || indices.indexOf(d.index) >= 0;
			const isSelected = selected.has(key);
			if (isSelection) {
				if (isTargetId && isTargetIndex && (!isSelected || singleSelection)) {
					if (!resetDone) {
						$$.setCanvasSelection(false);
						resetDone = true;
					}
					if (selected.has(key)) return;
					selected.add(key);
					callFn(config.data_onselected, $$.api, d, $$.canvasEngine.canvas);
					changed = true;
				} else if ((!singleSelection || resetDone) && resetOther && isSelected) {
					selected.delete(key);
					callFn(config.data_onunselected, $$.api, d, $$.canvasEngine.canvas);
					changed = true;
				}
			} else if (isTargetId && isTargetIndex && isSelected) {
				selected.delete(key);
				callFn(config.data_onunselected, $$.api, d, $$.canvasEngine.canvas);
				changed = true;
			}
		});
		changed && $$.renderCanvasFrame?.(void 0, null, false);
	},
	/**
	* Toggle canvas data selection from a click.
	* @param {Array} dataRows Data rows to toggle
	* @private
	*/
	toggleCanvasSelection(dataRows) {
		const $$ = this;
		const { config, state } = $$;
		const selected = state.canvasSelection;
		let changed = false;
		if (!config.data_selection_enabled) return;
		dataRows.filter((d) => isCanvasSelectableData($$, d)).forEach((d) => {
			const key = getCanvasDataKey(d);
			const isSelected = selected.has(key);
			if (!config.data_selection_multiple) eachCanvasSelectableData($$, (selectedData) => {
				const selectedKey = getCanvasDataKey(selectedData);
				if ((config.data_selection_grouped ? selectedData.id === d.id : selectedKey !== key) && selected.has(selectedKey)) {
					selected.delete(selectedKey);
					callFn(config.data_onunselected, $$.api, selectedData, $$.canvasEngine.canvas);
					changed = true;
				}
			});
			if (isSelected && selected.has(key)) {
				selected.delete(key);
				callFn(config.data_onunselected, $$.api, d, $$.canvasEngine.canvas);
			} else {
				selected.add(key);
				callFn(config.data_onselected, $$.api, d, $$.canvasEngine.canvas);
			}
			changed = true;
		});
		changed && $$.renderCanvasFrame?.(void 0, null, false);
	},
	/**
	* Check whether canvas draggable selection can start.
	* @param {MouseEvent|TouchEvent} event Input event
	* @returns {boolean} Whether drag selection can start
	* @private
	*/
	canStartCanvasSelectionDrag(event) {
		const { config } = this;
		const mouse = event;
		return Boolean(config.interaction_enabled && config.data_selection_enabled && config.data_selection_draggable && config.data_selection_multiple && !config.zoom_enabled && (mouse.button === void 0 || mouse.button === 0));
	},
	/**
	* Get the selected data rows inside the current canvas drag rectangle.
	* @param {object} rect Canvas rectangle
	* @returns {Array} Included rows
	* @private
	*/
	getCanvasSelectionDragData(rect) {
		const $$ = this;
		const { config } = $$;
		const data = $$.hitDetector.findInRect(rect, config.data_selection_grouped);
		if (!config.data_selection_grouped) return data;
		const indices = new Set(data.map((d) => d.index));
		return $$.filterTargetsToShow($$.data.targets).flatMap((target) => target.values.filter((d) => indices.has(d.index)));
	},
	/**
	* Start canvas draggable selection.
	* @param {MouseEvent|TouchEvent} event Input event
	* @returns {boolean} Whether drag started
	* @private
	*/
	startCanvasSelectionDrag(event) {
		const $$ = this;
		const { state } = $$;
		const point = getCanvasEventPoint($$, event);
		if (!point || !$$.canStartCanvasSelectionDrag(event)) return false;
		state.dragStart = point;
		state.canvasSelectionDragStart = point;
		state.canvasSelectionDragIncluded = /* @__PURE__ */ new Set();
		state.canvasSelectionDragging = true;
		state.canvasSelectionDragMoved = false;
		canvasSelectionDragRows.set($$, /* @__PURE__ */ new Map());
		$$.setDragStatus(true);
		$$.hideTooltip?.();
		event.preventDefault?.();
		return true;
	},
	/**
	* Update canvas draggable selection.
	* @param {MouseEvent|TouchEvent} event Input event
	* @returns {boolean} Whether drag was handled
	* @private
	*/
	updateCanvasSelectionDrag(event) {
		const $$ = this;
		const { config, state } = $$;
		const start = state.canvasSelectionDragStart;
		const point = getCanvasEventPoint($$, event);
		if (!state.canvasSelectionDragging || !start || !point) return false;
		const [sx, sy] = start;
		const [mx, my] = point;
		const minX = Math.min(sx, mx);
		const maxX = Math.max(sx, mx);
		const minY = config.data_selection_grouped ? state.margin.top : Math.min(sy, my);
		const maxY = config.data_selection_grouped ? state.margin.top + state.height : Math.max(sy, my);
		const rect = {
			x: minX,
			y: minY,
			w: maxX - minX,
			h: maxY - minY
		};
		const moved = Math.abs(mx - sx) > 4 || Math.abs(my - sy) > 4;
		state.canvasSelectionDragMoved = state.canvasSelectionDragMoved || moved;
		if (moved) {
			const { included, rows } = getCanvasSelectionDragDelta($$, $$.getCanvasSelectionDragData(rect));
			rows.length && $$.toggleCanvasSelection(rows);
			state.canvasSelectionDragIncluded = included;
			state.cancelClick = true;
		}
		$$.renderCanvasSelectionDrag(rect);
		event.preventDefault?.();
		return true;
	},
	/**
	* End canvas draggable selection.
	* @param {MouseEvent|TouchEvent} event Input event
	* @returns {boolean} Whether drag ended
	* @private
	*/
	endCanvasSelectionDrag(event) {
		const $$ = this;
		const { state } = $$;
		if (!state.canvasSelectionDragging) return false;
		$$.updateCanvasSelectionDrag(event);
		state.cancelClick = state.cancelClick || state.canvasSelectionDragMoved;
		state.dragStart = null;
		state.canvasSelectionDragStart = null;
		state.canvasSelectionDragIncluded = /* @__PURE__ */ new Set();
		state.canvasSelectionDragging = false;
		state.canvasSelectionDragMoved = false;
		canvasSelectionDragRows.delete($$);
		$$.setDragStatus(false);
		$$.clearCanvasFocus();
		event.preventDefault?.();
		return true;
	},
	/**
	* Dispatch canvas data.onout callback for the current hovered row.
	* @param {HTMLCanvasElement} canvas Canvas element
	* @private
	*/
	dispatchCanvasDataOut(canvas) {
		const $$ = this;
		const { config, state } = $$;
		const d = state.canvasDataHover;
		if (!d) return;
		state.canvasDataHover = null;
		state.canvasDataHoverKey = null;
		config.interaction_enabled && callFn(config.data_onout, $$.api, d, canvas);
	},
	/**
	* Dispatch canvas data.onover callback when the hovered row changes.
	* @param {object} d Data row
	* @param {HTMLCanvasElement} canvas Canvas element
	* @private
	*/
	dispatchCanvasDataOver(d, canvas) {
		const $$ = this;
		const { config, state } = $$;
		const key = getCanvasDataKey(d);
		if (state.canvasDataHoverKey === key) return;
		$$.dispatchCanvasDataOut(canvas);
		state.canvasDataHover = d;
		state.canvasDataHoverKey = key;
		config.interaction_enabled && d && callFn(config.data_onover, $$.api, d, canvas);
	},
	/**
	* Dispatch canvas data.onclick callback for a pointer position.
	* @param {Event} event Input event
	* @param {boolean} markInputClick Whether to suppress following compatibility click
	* @returns {object|null} Clicked data row
	* @private
	*/
	dispatchCanvasDataClick(event, markInputClick = false) {
		const $$ = this;
		const { config, $el } = $$;
		const canvas = $el.canvas.node();
		const d = getCanvasEventDatum($$, event, true);
		if (!d || isDuplicateCanvasInputClick($$, d)) return d;
		if (config.interaction_enabled) {
			const dataRows = config.data_selection_grouped ? $$.filterTargetsToShow($$.data.targets).map((target) => target.values[d.index]).filter(Boolean) : [d];
			$$.toggleCanvasSelection?.(dataRows);
			dataRows.forEach((row) => callFn(config.data_onclick, $$.api, row, canvas));
			markInputClick && markCanvasInputClick($$, d);
		}
		return d;
	},
	/**
	* Update canvas subchart brush domain from an input event.
	* @param {Event} event Input event
	* @returns {boolean} Whether the brush was updated
	* @private
	*/
	updateCanvasSubchartBrush(event) {
		const $$ = this;
		const { config, state } = $$;
		const start = state.canvasSubchartBrushStart;
		const origin = state.canvasSubchartBrushOrigin;
		const mode = state.canvasSubchartBrushMode;
		const coord = getCanvasSubchartBrushCoord($$, event, true);
		if (config.subchart_brush_enabled === false || !state.canvasSubchartBrushDragging || start === null || coord === null || !mode) return false;
		const delta = coord - start;
		if (Math.abs(delta) > CANVAS_SUBCHART_CLICK_TOLERANCE) state.canvasSubchartBrushMoved = true;
		if (!state.canvasSubchartBrushMoved) {
			event.preventDefault?.();
			return true;
		}
		let selection;
		if (mode === "move" && origin) {
			const [extentStart, extentEnd] = getCanvasSubchartBrushExtent($$);
			const size = origin[1] - origin[0];
			let nextStart = origin[0] + delta;
			let nextEnd = origin[1] + delta;
			if (nextStart < extentStart) {
				nextEnd += extentStart - nextStart;
				nextStart = extentStart;
			}
			if (nextEnd > extentEnd) {
				nextStart -= nextEnd - extentEnd;
				nextEnd = extentEnd;
			}
			selection = [Math.max(extentStart, Math.min(extentEnd - size, nextStart)), Math.min(extentEnd, Math.max(extentStart + size, nextEnd))];
		} else if (mode === "resize-start" && origin) selection = clampCanvasSubchartSelection($$, coord, origin[1]);
		else if (mode === "resize-end" && origin) selection = clampCanvasSubchartSelection($$, origin[0], coord);
		else selection = clampCanvasSubchartSelection($$, start, coord);
		const domain = getCanvasSubchartDomainFromCoords($$, selection[0], selection[1]);
		$$.setCanvasSubchartDomain(domain, true);
		state.cancelClick = true;
		event.preventDefault?.();
		return true;
	},
	/**
	* Update cursor while hovering over the canvas subchart brush area.
	* @param {Event} event Input event
	* @returns {boolean} Whether the pointer is over the subchart
	* @private
	*/
	updateCanvasSubchartCursor(event) {
		const $$ = this;
		const canvas = $$.$el.canvas.node();
		if ($$.config.subchart_brush_enabled === false) {
			canvas.style.cursor = "";
			return false;
		}
		const coord = getCanvasSubchartBrushCoord($$, event);
		if (coord === null) {
			canvas.style.cursor = "";
			return false;
		}
		const mode = getCanvasSubchartBrushMode($$, coord);
		canvas.style.cursor = getCanvasSubchartCursor($$, mode);
		return true;
	},
	/**
	* Start a canvas subchart brush gesture.
	* @param {Event} event Mouse, pointer or touch event
	* @returns {boolean} Whether brushing started
	* @private
	*/
	startCanvasSubchartBrush(event) {
		const $$ = this;
		const { state } = $$;
		if ($$.config.subchart_brush_enabled === false) return false;
		const coord = getCanvasSubchartBrushCoord($$, event);
		if (coord === null) return false;
		const mode = getCanvasSubchartBrushMode($$, coord);
		state.event = event;
		state.canvasSubchartBrushDragging = true;
		state.canvasSubchartBrushMode = mode;
		state.canvasSubchartBrushStart = coord;
		state.canvasSubchartBrushOrigin = getCanvasSubchartBrushSelection($$);
		state.canvasSubchartBrushMoved = false;
		state.cancelClick = true;
		$$.setDragStatus(true);
		event.preventDefault?.();
		return true;
	},
	/**
	* End a canvas subchart brush gesture.
	* @param {Event} event Mouse, pointer or touch event
	* @returns {boolean} Whether brushing ended
	* @private
	*/
	endCanvasSubchartBrush(event) {
		const $$ = this;
		const { state } = $$;
		if (!state.canvasSubchartBrushDragging) return false;
		$$.updateCanvasSubchartBrush(event);
		const shouldClear = state.canvasSubchartBrushMode === "select" && !state.canvasSubchartBrushMoved;
		const shouldCallback = state.canvasSubchartBrushMoved || shouldClear;
		if (shouldClear) $$.clearCanvasSubchartDomain?.(true, true);
		else if (shouldCallback) $$.config.subchart_onbrush.bind($$.api)(state.domain ?? $$.scale.x.orgDomain());
		state.canvasSubchartBrushDragging = false;
		state.canvasSubchartBrushMode = null;
		state.canvasSubchartBrushStart = null;
		state.canvasSubchartBrushOrigin = null;
		state.canvasSubchartBrushMoved = false;
		$$.setDragStatus(false);
		event.preventDefault?.();
		return true;
	},
	/**
	* Cancel a canvas subchart brush gesture without applying callbacks.
	* @param {Event} event Mouse, pointer or touch event
	* @returns {boolean} Whether brushing was cancelled
	* @private
	*/
	cancelCanvasSubchartBrush(event) {
		const $$ = this;
		const { state } = $$;
		if (!state.canvasSubchartBrushDragging) return false;
		state.canvasSubchartBrushDragging = false;
		state.canvasSubchartBrushMode = null;
		state.canvasSubchartBrushStart = null;
		state.canvasSubchartBrushOrigin = null;
		state.canvasSubchartBrushMoved = false;
		$$.setDragStatus(false);
		event.preventDefault?.();
		return true;
	},
	/**
	* Start mouse-driven canvas subchart brushing.
	* @param {MouseEvent} event Mouse event
	* @returns {boolean} Whether brushing started
	* @private
	*/
	onCanvasSubchartBrushStart(event) {
		const $$ = this;
		if (!$$.startCanvasSubchartBrush(event)) return false;
		const move = (event) => {
			$$.updateCanvasSubchartBrush(event);
		};
		const end = (event) => {
			$$.endCanvasSubchartBrush(event);
			win.removeEventListener("mousemove", move);
			win.removeEventListener("mouseup", end);
		};
		win.addEventListener("mousemove", move);
		win.addEventListener("mouseup", end);
		return true;
	},
	/**
	* Handle canvas click and dispatch shape-level data onclick.
	* @param {MouseEvent} event Mouse click event
	* @private
	*/
	onCanvasClick(event) {
		const $$ = this;
		const { config, state } = $$;
		state.event = event;
		if (state.flowing || state.cancelClick) {
			state.cancelClick = false;
			return;
		}
		$$.dispatchCanvasDataClick(event);
		config.onclick?.bind($$.api)(event);
	},
	/**
	* Start mouse-driven canvas draggable selection.
	* @param {MouseEvent} event Mouse event
	* @private
	*/
	onCanvasSelectionDragStart(event) {
		const $$ = this;
		const { state } = $$;
		if (!$$.startCanvasSelectionDrag(event)) return;
		const move = (event) => {
			$$.updateCanvasSelectionDrag(event);
		};
		const end = (event) => {
			$$.endCanvasSelectionDrag(event);
			win.removeEventListener("mousemove", move);
			win.removeEventListener("mouseup", end);
			state.canvasSelectionDragMoveHandler = null;
			state.canvasSelectionDragEndHandler = null;
		};
		state.canvasSelectionDragMoveHandler = move;
		state.canvasSelectionDragEndHandler = end;
		win.addEventListener("mousemove", move);
		win.addEventListener("mouseup", end);
	},
	/**
	* Start non-mouse pointer-driven canvas subchart brushing.
	* @param {PointerEvent} event Pointer event
	* @private
	*/
	onCanvasPointerDown(event) {
		if (!shouldHandleCanvasPointerEvent(this, event)) return;
		const $$ = this;
		const canvas = $$.$el.canvas.node();
		if (!$$.startCanvasSubchartBrush(event)) return;
		const pointerId = event.pointerId;
		const matchesPointer = (event) => event.pointerId === pointerId;
		const move = (event) => {
			matchesPointer(event) && $$.updateCanvasSubchartBrush(event);
		};
		const end = (event) => {
			if (matchesPointer(event)) {
				$$.endCanvasSubchartBrush(event);
				cleanup();
			}
		};
		const cancel = (event) => {
			if (matchesPointer(event)) {
				$$.cancelCanvasSubchartBrush(event);
				cleanup();
			}
		};
		/**
		* Remove temporary pointer brush listeners.
		* @private
		*/
		function cleanup() {
			win.removeEventListener("pointermove", move);
			win.removeEventListener("pointerup", end);
			win.removeEventListener("pointercancel", cancel);
			try {
				canvas.releasePointerCapture?.(pointerId);
			} catch {}
		}
		try {
			canvas.setPointerCapture?.(pointerId);
		} catch {}
		win.addEventListener("pointermove", move);
		win.addEventListener("pointerup", end);
		win.addEventListener("pointercancel", cancel);
	},
	/**
	* Handle non-mouse pointer entering the canvas.
	* @param {PointerEvent} event Pointer event
	* @private
	*/
	onCanvasPointerEnter(event) {
		if (!shouldHandleCanvasPointerEvent(this, event)) return;
		this.state.event = event;
		this.config.onover?.bind(this.api)(event);
	},
	/**
	* Handle non-mouse pointer movement over the canvas.
	* @param {PointerEvent} event Pointer event
	* @private
	*/
	onCanvasPointerMove(event) {
		if (!shouldHandleCanvasPointerEvent(this, event)) return;
		if (this.updateCanvasSubchartBrush(event)) return;
		this.onCanvasMouseMove(event);
	},
	/**
	* Handle non-mouse pointer click.
	* @param {PointerEvent} event Pointer event
	* @private
	*/
	onCanvasPointerUp(event) {
		if (!shouldHandleCanvasPointerEvent(this, event)) return;
		this.state.event = event;
		if (this.endCanvasSubchartBrush(event)) {
			this.config.onout?.bind(this.api)(event);
			return;
		}
		this.dispatchCanvasDataClick(event, true);
		this.dispatchCanvasDataOut(this.$el.canvas.node());
	},
	/**
	* Handle non-mouse pointer leaving the canvas.
	* @param {PointerEvent} event Pointer event
	* @private
	*/
	onCanvasPointerOut(event) {
		if (!shouldHandleCanvasPointerEvent(this, event)) return;
		if (this.state.canvasSubchartBrushDragging) return;
		this.state.event = event;
		this.onCanvasMouseOut();
		this.config.onout?.bind(this.api)(event);
	},
	/**
	* Handle non-mouse pointer cancellation.
	* @param {PointerEvent} event Pointer event
	* @private
	*/
	onCanvasPointerCancel(event) {
		if (!shouldHandleCanvasPointerEvent(this, event)) return;
		this.state.event = event;
		if (this.cancelCanvasSubchartBrush(event)) {
			this.config.onout?.bind(this.api)(event);
			return;
		}
		this.onCanvasPointerOut(event);
	},
	/**
	* Handle touch start over the canvas.
	* @param {TouchEvent} event Touch event
	* @private
	*/
	onCanvasTouchStart(event) {
		if (event.touches.length > 1) return;
		this.state.event = event;
		if (this.startCanvasSubchartBrush(event)) return;
		if (this.startCanvasSelectionDrag(event)) return;
		this.config.onover?.bind(this.api)(event);
		this.onCanvasMouseMove(event);
	},
	/**
	* Handle touch move over the canvas.
	* @param {TouchEvent} event Touch event
	* @private
	*/
	onCanvasTouchMove(event) {
		if (event.touches.length > 1) return;
		if (this.updateCanvasSubchartBrush(event)) return;
		if (this.updateCanvasSelectionDrag(event)) return;
		this.onCanvasMouseMove(event);
	},
	/**
	* Handle touch end over the canvas.
	* @param {TouchEvent} event Touch event
	* @private
	*/
	onCanvasTouchEnd(event) {
		this.state.event = event;
		if (this.endCanvasSubchartBrush(event)) {
			this.config.onout?.bind(this.api)(event);
			return;
		}
		if (this.endCanvasSelectionDrag(event)) {
			this.config.onout?.bind(this.api)(event);
			return;
		}
		this.dispatchCanvasDataClick(event, true);
		this.dispatchCanvasDataOut(this.$el.canvas.node());
		this.config.onout?.bind(this.api)(event);
	},
	/**
	* Handle touch cancellation.
	* @param {TouchEvent} event Touch event
	* @private
	*/
	onCanvasTouchCancel(event) {
		this.state.event = event;
		this.cancelCanvasSubchartBrush(event);
		this.endCanvasSelectionDrag(event);
		this.onCanvasMouseOut();
		this.config.onout?.bind(this.api)(event);
	},
	/**
	* Update canvas focus state and tooltip on pointer movement.
	* @param {Event} event Mouse, pointer or touch move event
	* @private
	*/
	onCanvasMouseMove(event) {
		const $$ = this;
		const { config, state, $el } = $$;
		const canvas = $el.canvas.node();
		state.event = event;
		if (state.flowing) return;
		if (state.canvasSelectionDragging) return;
		if ($$.updateCanvasSubchartCursor?.(event)) {
			$$.dispatchCanvasDataOut(canvas);
			if (state.canvasFocusKey) {
				state.canvasFocusKey = null;
				$$.clearCanvasFocus();
			}
			$$.hideTooltip?.();
			return;
		}
		const rawPoint = getCanvasEventPoint($$, event);
		const point = rawPoint && (getCanvasMainPointFromSubchart($$, rawPoint) || rawPoint);
		const d = point ? getCanvasHoverDatumFromPoint($$, point) : null;
		if (!d) {
			$$.dispatchCanvasDataOut(canvas);
			if (state.canvasFocusKey) {
				state.canvasFocusKey = null;
				$$.clearCanvasFocus();
			}
			if (rawPoint && config.axis_tooltip && isCanvasAxisTooltipArea($$, rawPoint)) {
				$$.renderCanvasAxisTooltip(rawPoint);
				$$.hideTooltip?.();
				return;
			}
			$$.hideTooltip?.();
			return;
		}
		$$.dispatchCanvasDataOver(d, canvas);
		const selectedData = getCanvasTooltipData($$, d);
		const focusData = getCanvasFocusData($$, d, selectedData);
		const focusKey = focusData.map((v) => `${v.id}:${v.index}`).join("|");
		if (state.canvasFocusKey !== focusKey || config.axis_tooltip) {
			state.canvasFocusKey = focusKey;
			$$.renderCanvasFocus(focusData, point);
		}
		$$.showTooltip?.(selectedData, canvas);
	},
	/**
	* Clear focus and tooltip state when the pointer leaves the canvas.
	* @private
	*/
	onCanvasMouseOut() {
		const canvas = this.$el.canvas.node();
		canvas.style.cursor = "";
		this.dispatchCanvasDataOut(canvas);
		if (this.state.canvasFocusKey) {
			this.state.canvasFocusKey = null;
			this.clearCanvasFocus();
		} else if (this.config.axis_tooltip) this.clearCanvasFocus();
		this.hideTooltip?.();
	},
	/**
	* Show canvas tooltip from linked tooltip propagation.
	* @param {number} index Data index
	* @returns {boolean} Whether linked tooltip was shown
	* @private
	*/
	showCanvasLinkedTooltip(index) {
		const $$ = this;
		const { $el, state } = $$;
		const canvas = $el.canvas?.node();
		const d = $$.filterTargetsToShow($$.data.targets).map((target) => target.values[index]).find(Boolean);
		if (!canvas || !d) {
			$$.hideTooltip?.();
			return false;
		}
		const selectedData = getCanvasTooltipData($$, d);
		const focus = selectedData.find((v) => v && hasCanvasDrawableValue($$, v)) || selectedData[0];
		if (!selectedData.length || !focus) {
			$$.hideTooltip?.();
			return false;
		}
		const { left, top } = getBoundingRect(canvas, true);
		const point = getRenderDataPoint($$, focus);
		const eventPoint = [state.margin.left + point.x, state.margin.top + point.y];
		state.event = {
			clientX: left + eventPoint[0],
			clientY: top + eventPoint[1],
			currentTarget: canvas,
			target: canvas
		};
		state.canvasFocusKey = selectedData.map((v) => `${v.id}:${v.index}`).join("|");
		$$.renderCanvasFocus(selectedData, eventPoint);
		$$.showTooltip?.(selectedData, canvas);
		return true;
	},
	/**
	* Draw drag-zoom brush overlay on the cached canvas frame.
	* @param {number} start Start coordinate
	* @param {number} end End coordinate
	* @private
	*/
	renderCanvasZoomBrush(start, end) {
		const $$ = this;
		$$.canvasEngine.withOverlay((ctx) => {
			$$.canvasRenderer.withContext(ctx, () => {
				$$.canvasRenderer.drawZoomBrush($$, start, end);
			});
		});
	},
	/**
	* Clear drag-zoom brush overlay.
	* @private
	*/
	clearCanvasZoomBrush() {
		this.canvasEngine.clearOverlay();
	},
	/**
	* Draw selection drag overlay on the cached canvas frame.
	* @param {object} rect Selection rectangle
	* @private
	*/
	renderCanvasSelectionDrag(rect) {
		const $$ = this;
		$$.canvasEngine.withOverlay((ctx) => {
			$$.canvasRenderer.withContext(ctx, () => {
				$$.canvasRenderer.drawSelectionDragArea($$, rect);
			});
		});
	},
	/**
	* Draw a complete canvas frame and optionally rebuild hit indexes.
	* @param {object} shape Cached draw shape object
	* @param {Array} focusData Focused data rows
	* @param {boolean} rebuildHit Whether to rebuild hit indexes
	* @private
	*/
	renderCanvasFrame(shape, focusData, rebuildHit = false) {
		const $$ = this;
		const { state } = $$;
		const drawShape = shape || state.canvasShape || $$.getDrawShape();
		state.canvasShape = drawShape;
		state._canvasVisibleRangeCache = null;
		state._canvasXTickValuesCache = null;
		$$.canvasEngine.beginFrame(state.current.width, $$.getCanvasSurfaceHeight());
		try {
			$$.canvasRenderer.drawBackground($$);
			$$.canvasAxisRenderer.drawTitle($$);
			state.hasAxis && $$.canvasAxisRenderer.draw($$);
			$$.canvasRenderer.draw($$, drawShape);
			state.hasAxis && $$.config.grid_lines_front && $$.canvasAxisRenderer.drawGridLines($$);
			$$.canvasRenderer.drawSubchart($$, drawShape);
			state.hasAxis && $$.canvasAxisRenderer.drawSubXAxis($$);
			state.hasAxis && $$.canvasAxisRenderer.drawSubYAxes($$);
			$$.canvasRenderer.drawEmptyLabel($$);
			rebuildHit && $$.hitDetector.rebuild($$, drawShape);
		} finally {
			$$.canvasEngine.endFrame();
		}
		$$.canvasEngine.clearOverlay();
		state.canvasFocusMainRedraw = false;
		focusData && $$.renderCanvasFocus(focusData);
	},
	/**
	* Restore the cached frame and draw focus overlays.
	* @param {Array} focusData Focused data rows
	* @param {Array} point Canvas-local pointer coordinate
	* @private
	*/
	renderCanvasFocus(focusData, point) {
		const $$ = this;
		const { state } = $$;
		const withMainRedraw = $$.canvasRenderer.hasExpandedShapeFocus?.($$, focusData);
		if (withMainRedraw) {
			const drawShape = state.canvasShape || $$.getDrawShape();
			$$.canvasEngine.beginFrame(state.current.width, $$.getCanvasSurfaceHeight());
			try {
				$$.canvasRenderer.drawBackground($$);
				$$.canvasAxisRenderer.drawTitle($$);
				state.hasAxis && $$.canvasAxisRenderer.draw($$);
				$$.canvasRenderer.draw($$, drawShape, focusData);
				state.hasAxis && $$.config.grid_lines_front && $$.canvasAxisRenderer.drawGridLines($$);
				$$.canvasRenderer.drawSubchart($$, drawShape);
				state.hasAxis && $$.canvasAxisRenderer.drawSubXAxis($$);
				state.hasAxis && $$.canvasAxisRenderer.drawSubYAxes($$);
				$$.canvasRenderer.drawEmptyLabel($$);
			} finally {
				$$.canvasEngine.endFrame();
			}
		}
		state.canvasFocusMainRedraw = !!withMainRedraw;
		$$.canvasEngine.withOverlay((ctx) => {
			$$.canvasRenderer.withContext(ctx, () => {
				$$.canvasRenderer.drawFocus($$, focusData);
			});
			$$.canvasAxisRenderer.withContext(ctx, () => {
				$$.canvasAxisRenderer.drawFocusedXAxisTick($$, focusData);
				point && $$.canvasAxisRenderer.drawAxisTooltip($$, point);
			});
		});
	},
	/**
	* Draw only the canvas axis tooltip overlay.
	* @param {Array} point Canvas-local pointer coordinate
	* @private
	*/
	renderCanvasAxisTooltip(point) {
		const $$ = this;
		$$.canvasEngine.withOverlay((ctx) => {
			$$.canvasAxisRenderer.withContext(ctx, () => {
				$$.canvasAxisRenderer.drawAxisTooltip($$, point);
			});
		});
	},
	/**
	* Restore the cached canvas frame without focus overlays.
	* @private
	*/
	clearCanvasFocus() {
		const $$ = this;
		if ($$.state.canvasFocusMainRedraw) {
			$$.state.canvasFocusMainRedraw = false;
			$$.renderCanvasFrame(void 0, null, false);
		} else $$.canvasEngine?.clearOverlay();
	},
	/**
	* Resize canvas surfaces and reload canvas theme values.
	* @private
	*/
	resizeCanvas() {
		const $$ = this;
		const { config, state, $el } = $$;
		const container = $el.chart.node();
		$$.clearResizePreview();
		$el.chart.style("min-height", `${state.current.height}px`);
		$$.canvasEngine?.resize(state.current.width, $$.getCanvasSurfaceHeight());
		$$.canvasTheme?.reload(container, config.canvas_theme);
	}
};
/**
* Register canvas rendering modules and return canvas render mode.
* @returns {string} Canvas render mode
*/
let canvas = () => {
	extend(ChartInternal.prototype, canvasInternal);
	return (canvas = () => "canvas")();
};
//#endregion
export { canvas };