async function createPlot(config = {}) {
const d3 = await import("https://esm.sh/d3@7");
let cfg;
let initialConfig;
let innerWidth, innerHeight;
let xScale,
yScale,
initialXScale,
initialYScale,
referenceXScale,
referenceYScale;
let svg, g, plotArea, clipPathId;
let xAxisGroup, yAxisGroup, xLabel, yLabel, viewport;
let redrawTimeout;
let lastZoomLevel = 1;
let currentZoomTransform = d3.zoomIdentity;
let isProgrammaticZoom = false;
let plot = {};
const defaults = {
width: 600,
height: 400,
margin: { top: 20, right: 30, bottom: 40, left: 50 },
xDomain: [-1, 1],
yDomain: [-1, 1],
xTicks: 10,
yTicks: 10,
xLabel: "x",
yLabel: "y",
gridLines: true,
axisColor: "#333",
backgroundColor: "#fff",
zoomEnabled: true,
zoomExtent: [0.1, 10],
independentAxisZoomFactor: 1.1,
zoomDebounce: 0,
debug: false
};
// --- Helper: Configuration Merging ---
function mergeConfiguration(newConfig, isInitialCall = false) {
const oldCfg = isInitialCall ? null : { ...cfg };
if (isInitialCall) {
// Start with defaults, then merge user config
cfg = { ...defaults };
if (newConfig && newConfig.margin) {
cfg.margin = { ...cfg.margin, ...newConfig.margin };
delete newConfig.margin;
}
Object.assign(cfg, newConfig);
// Store the effective initial config
initialConfig = { ...cfg };
if (cfg.debug) console.log("Initial Plot Config:", cfg);
} else if (newConfig) {
// Subsequent updates
if (newConfig.margin) {
cfg.margin = { ...cfg.margin, ...newConfig.margin };
delete newConfig.margin;
}
Object.assign(cfg, newConfig);
if (cfg.debug) console.log("Plot config updated:", newConfig);
}
return oldCfg;
}
// --- Helper: Calculate Inner Dimensions ---
function calculateDimensions() {
innerWidth = cfg.width - cfg.margin.left - cfg.margin.right;
innerHeight = cfg.height - cfg.margin.top - cfg.margin.bottom;
if (innerWidth < 0) innerWidth = 0;
if (innerHeight < 0) innerHeight = 0;
}
// --- Helper: Setup Scales (Updates ALL scale types based on current cfg) ---
function setupScales() {
// Use domains from the *current* config object
xScale = d3.scaleLinear().domain(cfg.xDomain).range([0, innerWidth]);
yScale = d3.scaleLinear().domain(cfg.yDomain).range([innerHeight, 0]);
// Initial scales now reflect the *current* config domain (the base for standard zoom)
initialXScale = xScale.copy();
initialYScale = yScale.copy();
// Reference scales also match the current config domain initially or after reset/setDomain
referenceXScale = xScale.copy();
referenceYScale = yScale.copy();
if (cfg.debug) {
console.log("Scales Setup/Updated:");
console.log(` Config Domain: X=${cfg.xDomain}, Y=${cfg.yDomain}`);
console.log(
` Drawing Scales Initialized: X=[${xScale.domain()}], Y=[${yScale.domain()}]`
);
console.log(
` Initial Scales (for std zoom): X=[${initialXScale.domain()}], Y=[${initialYScale.domain()}]`
);
console.log(
` Reference Scales (for alt zoom): X=[${referenceXScale.domain()}], Y=[${referenceYScale.domain()}]`
);
}
}
// --- Helper: Setup SVG Structure ---
function setupSvgStructure() {
if (!svg) svg = d3.create("svg");
if (!g) g = svg.append("g").attr("class", "main-group");
if (!plotArea) plotArea = g.append("g").attr("class", "plot-area");
if (!xAxisGroup) xAxisGroup = g.append("g").attr("class", "x-axis");
if (!yAxisGroup) yAxisGroup = g.append("g").attr("class", "y-axis");
if (!viewport)
viewport = plotArea.append("rect").attr("class", "viewport-indicator");
svg
.attr("width", cfg.width)
.attr("height", cfg.height)
.attr("viewBox", [0, 0, cfg.width, cfg.height])
.attr("style", "max-width: 100%; height: auto; display: block;")
.style("background", cfg.backgroundColor)
.style("pointer-events", "all");
g.attr("transform", `translate(${cfg.margin.left},${cfg.margin.top})`);
clipPathId =
clipPathId || `plot-area-clip-${Math.random().toString(36).substr(2, 9)}`;
g.select("clipPath").remove();
g.insert("clipPath", ":first-child")
.attr("id", clipPathId)
.append("rect")
.attr("width", innerWidth)
.attr("height", innerHeight)
.attr("x", 0)
.attr("y", 0);
plotArea.attr("clip-path", `url(#${clipPathId})`);
viewport
.attr("fill", "none")
.attr("stroke", "#aaa")
.attr("stroke-width", 1)
.attr("stroke-dasharray", "5,5");
}
// --- Redraw Logic ---
function requestRedraw() {
if (!cfg.zoomDebounce) {
performRedraw();
return;
}
if (redrawTimeout) {
clearTimeout(redrawTimeout);
}
redrawTimeout = setTimeout(performRedraw, cfg.zoomDebounce);
}
function performRedraw() {
if (cfg.debug) console.log("Performing Redraw...");
drawGridLines();
drawAxes();
createOrUpdateLabels();
drawViewportIndicator();
}
function drawGridLines() {
plotArea.selectAll(".grid-lines").remove();
if (cfg.gridLines) {
plotArea
.append("g")
.attr("class", "grid-lines x-grid")
.lower()
.selectAll("line")
.data(xScale.ticks(cfg.xTicks))
.join("line")
.attr("x1", (d) => xScale(d))
.attr("x2", (d) => xScale(d))
.attr("y1", 0)
.attr("y2", innerHeight)
.attr("stroke", "#e0e0e0")
.attr("stroke-opacity", 0.7)
.attr("stroke-dasharray", "3,3");
plotArea
.append("g")
.attr("class", "grid-lines y-grid")
.lower()
.selectAll("line")
.data(yScale.ticks(cfg.yTicks))
.join("line")
.attr("x1", 0)
.attr("x2", innerWidth)
.attr("y1", (d) => yScale(d))
.attr("y2", (d) => yScale(d))
.attr("stroke", "#e0e0e0")
.attr("stroke-opacity", 0.7)
.attr("stroke-dasharray", "3,3");
}
}
function drawAxes() {
xAxisGroup
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale).ticks(cfg.xTicks).tickSizeOuter(0))
.attr("color", cfg.axisColor)
.selectAll(".tick line")
.attr("stroke-opacity", 0.3);
yAxisGroup
.call(d3.axisLeft(yScale).ticks(cfg.yTicks).tickSizeOuter(0))
.attr("color", cfg.axisColor)
.selectAll(".tick line")
.attr("stroke-opacity", 0.3);
}
function createOrUpdateLabels() {
if (cfg.xLabel) {
if (!xLabel) {
xLabel = g
.append("text")
.attr("class", "x-label")
.attr("text-anchor", "middle")
.style("font-size", "12px")
.style("font-family", "sans-serif");
}
xLabel
.attr("x", innerWidth / 2)
.attr("y", innerHeight + cfg.margin.bottom - 5)
.attr("fill", cfg.axisColor)
.text(cfg.xLabel);
} else if (xLabel) {
xLabel.remove();
xLabel = null;
}
if (cfg.yLabel) {
if (!yLabel) {
yLabel = g
.append("text")
.attr("class", "y-label")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.style("font-size", "12px")
.style("font-family", "sans-serif");
}
yLabel
.attr("x", -innerHeight / 2)
.attr("y", -cfg.margin.left + 15)
.attr("fill", cfg.axisColor)
.text(cfg.yLabel);
} else if (yLabel) {
yLabel.remove();
yLabel = null;
}
}
function drawViewportIndicator() {
const xDomain = xScale.domain();
const yDomain = yScale.domain();
const xVisible = xDomain[0] < 1 && xDomain[1] > -1;
const yVisible = yDomain[0] < 1 && yDomain[1] > -1;
if (xVisible && yVisible) {
const x1 = xScale(Math.max(-1, xDomain[0]));
const x2 = xScale(Math.min(1, xDomain[1]));
const y1 = yScale(Math.min(1, yDomain[1]));
const y2 = yScale(Math.max(-1, yDomain[0]));
viewport
.attr("x", x1)
.attr("y", y1)
.attr("width", Math.max(0, x2 - x1))
.attr("height", Math.max(0, y2 - y1))
.style("display", null);
} else {
viewport.style("display", "none");
}
}
// --- Helper: Handle Zoom Event ---
function handleZoomEvent(event) {
const sourceEvent = event.sourceEvent;
const transform = event.transform; // The transform calculated by D3 relative to its *internal* state
// ** Guard against events triggered by our own zoom.transform calls **
if (isProgrammaticZoom) {
if (cfg.debug) console.log(" Ignoring programmatic zoom event.");
return;
}
if (cfg.debug)
// Shortened log for clarity during sequence analysis
console.log(
`Zoom event: type=${event.type}, source=${sourceEvent?.type}, alt=${
sourceEvent?.altKey
}, shift=${sourceEvent?.shiftKey}, k=${transform.k.toFixed(3)}`
);
let newXDomain = xScale.domain(); // Start with current domains
let newYDomain = yScale.domain();
let domainChanged = false;
let isAltZoom = false; // Reset flag for this event
let isShiftZoom = false; // Flag for horizontal/X-axis zoom
// --- Alt+Scroll Logic ---
if (sourceEvent && sourceEvent.type === "wheel" && sourceEvent.altKey) {
isAltZoom = true; // Set flag for this specific event handling
sourceEvent.preventDefault();
const scaleFactor = cfg.independentAxisZoomFactor;
const wheelDeltaY = sourceEvent.deltaY || 0;
const wheelDeltaX = sourceEvent.deltaX || 0;
const pointerX_svg = sourceEvent.offsetX;
const pointerY_svg = sourceEvent.offsetY;
const pointerX_plot = pointerX_svg - cfg.margin.left;
const pointerY_plot = pointerY_svg - cfg.margin.top;
if (Math.abs(wheelDeltaY) > Math.abs(wheelDeltaX)) {
const zoomDirection = wheelDeltaY > 0 ? 1 / scaleFactor : scaleFactor;
const yValue_plot = referenceYScale.invert(pointerY_plot);
const [y0, y1] = referenceYScale.domain();
newYDomain = [
yValue_plot + (y0 - yValue_plot) / zoomDirection,
yValue_plot + (y1 - yValue_plot) / zoomDirection
];
domainChanged = true;
if (cfg.debug)
console.log(` Alt+Scroll Y: factor=${zoomDirection.toFixed(2)}`);
} else if (Math.abs(wheelDeltaX) > 0) {
const zoomDirection = wheelDeltaX < 0 ? 1 / scaleFactor : scaleFactor;
const xValue_plot = referenceXScale.invert(pointerX_plot);
const [x0, x1] = referenceXScale.domain();
newXDomain = [
xValue_plot + (x0 - xValue_plot) / zoomDirection,
xValue_plot + (x1 - xValue_plot) / zoomDirection
];
domainChanged = true;
if (cfg.debug)
console.log(` Alt+Scroll X: factor=${zoomDirection.toFixed(2)}`);
}
}
// --- Shift+Scroll Logic (X-axis zoom) ---
// Trigger on shift key OR if horizontal delta is clearly dominant
else if (
sourceEvent &&
sourceEvent.type === "wheel" &&
(sourceEvent.shiftKey ||
Math.abs(sourceEvent.deltaX) > Math.abs(sourceEvent.deltaY * 2))
) {
isShiftZoom = true; // Set flag
sourceEvent.preventDefault();
const scaleFactor = cfg.independentAxisZoomFactor;
const wheelDeltaX = sourceEvent.deltaX || 0;
const pointerX_svg = sourceEvent.offsetX;
const pointerX_plot = pointerX_svg - cfg.margin.left;
// Zoom direction based on horizontal scroll
const zoomDirection = wheelDeltaX < 0 ? scaleFactor : 1 / scaleFactor; // Left scroll zooms out, Right zooms in
const xValue_plot = referenceXScale.invert(pointerX_plot);
const [x0, x1] = referenceXScale.domain();
newXDomain = [
xValue_plot + (x0 - xValue_plot) / zoomDirection,
xValue_plot + (x1 - xValue_plot) / zoomDirection
];
domainChanged = true;
if (cfg.debug)
console.log(` Shift+Scroll X: factor=${zoomDirection.toFixed(2)}`);
}
// --- Standard Zoom/Pan Logic ---
else {
const calculatedTransform = event.transform; // Use the transform from the event
// Apply this transform relative to our potentially updated initial scales
newXDomain = calculatedTransform.rescaleX(initialXScale).domain();
newYDomain = calculatedTransform.rescaleY(initialYScale).domain();
domainChanged = true;
// Update internal state tracking based on the *event's* transform
currentZoomTransform = calculatedTransform; // Track the transform D3 thinks is active
lastZoomLevel = calculatedTransform.k;
if (cfg.debug && sourceEvent)
console.log(" Standard Zoom/Pan: Calculated from initial scales.");
}
// --- Apply Changes and Update References/Initial/ZoomState ---
if (domainChanged) {
xScale.domain(newXDomain);
yScale.domain(newYDomain);
// *** ALWAYS update reference scales to match the new drawing state ***
referenceXScale.domain(newXDomain);
referenceYScale.domain(newYDomain);
if (cfg.debug)
console.log(" Updated reference scales to match new drawing state.");
if (isAltZoom) {
// Handle Alt+Zoom specific state updates AFTER applying domain changes
if (cfg.debug)
console.log(
" Alt+Zoom event (Y/X): Updating initial scales, internal state, and resetting D3 transform."
);
// Update the initial scales to reflect this new base state
initialXScale.domain(newXDomain);
initialYScale.domain(newYDomain);
// Update our internal tracking to reflect the new base state immediately
currentZoomTransform = d3.zoomIdentity;
lastZoomLevel = 1;
// ** Reset D3's internal transform state *immediately* **
if (plot.zoomBehavior) {
isProgrammaticZoom = true; // Set flag before call
svg.call(plot.zoomBehavior.transform, d3.zoomIdentity);
isProgrammaticZoom = false; // Unset flag after call
}
} else if (isShiftZoom) {
// Handle Shift+Zoom specific state updates
if (cfg.debug)
console.log(
" Shift+Zoom event (X): Updating initial scales, internal state, and resetting D3 transform."
);
// Update the initial scales to reflect this new base state
initialXScale.domain(newXDomain);
initialYScale.domain(newYDomain); // Y domain didn't change, but update initialY to match current base
// Update our internal tracking to reflect the new base state immediately
currentZoomTransform = d3.zoomIdentity;
lastZoomLevel = 1;
// Reset D3's internal transform state *immediately*
if (plot.zoomBehavior) {
isProgrammaticZoom = true; // Set flag before call
svg.call(plot.zoomBehavior.transform, d3.zoomIdentity);
isProgrammaticZoom = false; // Unset flag after call
}
}
// (If Standard Zoom, currentZoomTransform and lastZoomLevel were already updated above)
// Log final state
if (cfg.debug) {
const format = (d) => d.toFixed(3);
console.log(
` Applied Viewport: X=[${format(xScale.domain()[0])}, ${format(
xScale.domain()[1]
)}], Y=[${format(yScale.domain()[0])}, ${format(yScale.domain()[1])}]`
);
// Log initial scales ONLY if alt zoom happened, as they should match Applied Viewport then
if (isAltZoom || isShiftZoom)
console.log(
` Initial Scales now: X=[${format(
initialXScale.domain()[0]
)}, ${format(initialXScale.domain()[1])}], Y=[${format(
initialYScale.domain()[0]
)}, ${format(initialYScale.domain()[1])}]`
);
}
requestRedraw();
// Dispatch the current meaningful transform (identity after alt-zoom, actual after std-zoom)
svg.dispatch("plotzoom", {
detail: {
transform: currentZoomTransform,
xScale: xScale,
yScale: yScale
}
});
} else if (cfg.debug) {
console.log(" No domain change detected in zoom event.");
}
}
// --- Helper: Reset Plot Zoom and Scales ---
function resetPlotZoomAndScales(duration = 500) {
if (cfg.debug) console.log("Attempting Plot Reset...");
// Reset config domains to the *initial* config values
cfg.xDomain = [...initialConfig.xDomain];
cfg.yDomain = [...initialConfig.yDomain];
if (cfg.debug)
console.log(
` Resetting cfg domains to initial: X=${cfg.xDomain}, Y=${cfg.yDomain}`
);
// Update all scales based on the reset config domains
setupScales();
// Reset zoom transform
if (cfg.zoomEnabled && plot.zoomBehavior) {
svg
.transition()
.duration(duration)
.call(plot.zoomBehavior.transform, d3.zoomIdentity)
.on("end interrupt", () => {
// Ensure final state reflects identity transform on new initial scales
const finalTransform = d3.zoomTransform(svg.node());
currentZoomTransform = d3.zoomIdentity;
lastZoomLevel = 1;
// Scales should already be correct from setupScales, but redraw for certainty
performRedraw();
if (cfg.debug)
console.log(
`Plot Reset complete (final zoom k=${finalTransform.k.toFixed(
3
)}).`
);
});
} else {
performRedraw(); // Redraw with reset scales
if (cfg.debug) console.log("Plot Reset Complete (no zoom).");
}
return plot;
}
// --- Helper: Set Plot X Domain (Updates base domain and resets zoom) ---
function setPlotXDomain(domain, duration = 0) {
if (
!Array.isArray(domain) ||
domain.length !== 2 ||
domain[0] >= domain[1]
) {
if (cfg.debug) console.error("Invalid xDomain:", domain);
return plot;
}
if (cfg.debug)
console.log(`Setting X Domain to: [${domain}]. Resetting zoom state.`);
// 1. Get the current Y domain to preserve it
const currentYDomain = yScale.domain();
// 1. Update the configuration (this is the new base domain)
cfg.xDomain = [...domain];
// 2. Update ONLY the X-related scales directly
xScale.domain(cfg.xDomain);
initialXScale.domain(cfg.xDomain);
referenceXScale.domain(cfg.xDomain);
// ** Also update Y initial/reference to current visual state **
initialYScale.domain(currentYDomain);
referenceYScale.domain(currentYDomain);
// 3. Reset the zoom behavior's internal transform to identity
if (cfg.zoomEnabled && plot.zoomBehavior) {
// This call triggers handleZoomEvent. Since initialXScale and initialYScale
// now reflect the desired state (new X, current Y), the event handler
// will correctly calculate and apply these domains.
svg.call(plot.zoomBehavior.transform, d3.zoomIdentity);
// currentZoomTransform and lastZoomLevel are set inside handleZoomEvent
}
// 4. Redraw the plot - This will use the scales updated directly
// and potentially updated again slightly by the zoom event handler
// (but the handler will now respect the flag)
performRedraw();
if (cfg.debug) {
const format = (d) => d.toFixed(3);
console.log(
` setXDomain Applied Viewport: X=[${format(
xScale.domain()[0]
)}, ${format(xScale.domain()[1])}], Y=[${format(
yScale.domain()[0]
)}, ${format(yScale.domain()[1])}]` // Log Y too to verify it didn't change
);
console.log(
` setXDomain Initial Scales now: X=[${format(
initialXScale.domain()[0]
)}, ${format(initialXScale.domain()[1])}], Y=[${format(
initialYScale.domain()[0]
)}, ${format(initialYScale.domain()[1])}]` // Log Y initial to verify it didn't change
);
}
return plot;
}
// --- Helper: Set Plot Y Domain (Updates base domain and resets zoom) ---
function setPlotYDomain(domain, duration = 0) {
if (
!Array.isArray(domain) ||
domain.length !== 2 ||
domain[0] >= domain[1]
) {
if (cfg.debug) console.error("Invalid yDomain:", domain);
return plot;
}
if (cfg.debug)
console.log(`Setting Y Domain to: [${domain}]. Resetting zoom state.`);
// 1. Get the current X domain to preserve it
const currentXDomain = xScale.domain();
// 1. Update the configuration
cfg.yDomain = [...domain];
// 2. Update ONLY the Y-related scales directly
yScale.domain(cfg.yDomain);
initialYScale.domain(cfg.yDomain);
referenceYScale.domain(cfg.yDomain);
// ** Also update X initial/reference to current visual state **
initialXScale.domain(currentXDomain);
referenceXScale.domain(currentXDomain);
// 3. Reset the zoom behavior's internal transform to identity
if (cfg.zoomEnabled && plot.zoomBehavior) {
// This call triggers handleZoomEvent. Since initialXScale and initialYScale
// now reflect the desired state (current X, new Y), the event handler
// will correctly calculate and apply these domains.
svg.call(plot.zoomBehavior.transform, d3.zoomIdentity);
// currentZoomTransform and lastZoomLevel are set inside handleZoomEvent
}
// 4. Redraw the plot
performRedraw();
if (cfg.debug) {
const format = (d) => d.toFixed(3);
console.log(
` setYDomain Applied Viewport: X=[${format(
xScale.domain()[0]
)}, ${format(xScale.domain()[1])}], Y=[${format(
yScale.domain()[0]
)}, ${format(yScale.domain()[1])}]`
);
console.log(
` setYDomain Initial Scales now: X=[${format(
initialXScale.domain()[0]
)}, ${format(initialXScale.domain()[1])}], Y=[${format(
initialYScale.domain()[0]
)}, ${format(initialYScale.domain()[1])}]`
);
}
return plot;
}
// --- Helper: Update Configuration ---
function updatePlotConfiguration(newConfig) {
const oldCfg = mergeConfiguration(newConfig); // Get previous state AFTER merge
let needsFullRedraw = false;
let needsFullReset = false; // Flag if domains change, requiring zoom reset
// Check for dimension changes
if (
cfg.width !== oldCfg.width ||
cfg.height !== oldCfg.height ||
JSON.stringify(cfg.margin) !== JSON.stringify(oldCfg.margin)
) {
if (cfg.debug)
console.warn("Dynamic resize requires full plot setup rebuild.");
calculateDimensions();
setupScales(); // Update scale ranges *and* initial/reference
setupSvgStructure();
needsFullReset = true; // Treat dimension change like domain change for zoom state
}
// Check if base domains changed - requires scale updates AND zoom reset
if (
JSON.stringify(cfg.xDomain) !== JSON.stringify(oldCfg.xDomain) ||
JSON.stringify(cfg.yDomain) !== JSON.stringify(oldCfg.yDomain)
) {
if (cfg.debug) console.log("Base domains changed via updateConfig.");
setupScales(); // Update all scales
needsFullReset = true;
}
// If zoom needs reset, do it now and skip other checks that lead to redraw
if (needsFullReset) {
if (cfg.zoomEnabled && plot.zoomBehavior) {
if (cfg.debug)
console.log("Resetting zoom transform due to config change.");
svg.call(plot.zoomBehavior.transform, d3.zoomIdentity);
currentZoomTransform = d3.zoomIdentity;
lastZoomLevel = 1;
}
needsFullRedraw = true; // Force redraw after reset
}
// Check for other changes requiring redraw
if (
!needsFullRedraw &&
(cfg.gridLines !== oldCfg.gridLines ||
cfg.xTicks !== oldCfg.xTicks ||
cfg.yTicks !== oldCfg.yTicks)
) {
needsFullRedraw = true;
}
// Check for axis appearance only change
let needsAxisUpdateOnly = false;
if (!needsFullRedraw && cfg.axisColor !== oldCfg.axisColor) {
needsAxisUpdateOnly = true;
}
// Check background color
if (cfg.backgroundColor !== oldCfg.backgroundColor) {
svg.style("background", cfg.backgroundColor);
}
// Always update labels
createOrUpdateLabels();
// Execute updates
if (needsFullRedraw) {
performRedraw();
} else if (needsAxisUpdateOnly) {
drawAxes();
}
return plot;
}
// --- Helper: Initialize Zoom Behavior ---
function initializeZoom() {
if (cfg.zoomEnabled) {
if (cfg.debug) console.log("Setting up zoom behavior...");
const zoomBehavior = d3
.zoom()
.scaleExtent(cfg.zoomExtent)
.extent([
[0, 0],
[cfg.width, cfg.height]
])
.on("zoom", handleZoomEvent); // Only zoom handler needed now
if (cfg.debug) console.log("Applying zoom behavior to SVG element...");
svg.call(zoomBehavior);
svg.on("dblclick.zoom", null);
plot.zoomBehavior = zoomBehavior;
if (cfg.debug) console.log("Zoom setup complete.");
} else {
plot.zoomBehavior = null;
if (cfg.debug) console.log("Zoom is disabled via config.");
}
}
// --- Initialization Sequence ---
mergeConfiguration(config, true); // Process initial user config, store initialConfig
calculateDimensions();
setupSvgStructure();
setupScales(); // Sets up initial, drawing, and reference scales
performRedraw(); // Initial draw
// --- Define Plot Object API ---
const plotAPI = {
element: null,
svg: svg,
g: g,
plotArea: plotArea,
xScale: xScale,
yScale: yScale,
zoomBehavior: null,
getXDomain: () => xScale.domain(),
getYDomain: () => yScale.domain(),
setXDomain: setPlotXDomain,
setYDomain: setPlotYDomain,
reset: resetPlotZoomAndScales,
getConfig: () => ({ ...cfg }),
updateConfig: updatePlotConfiguration
};
Object.assign(plot, plotAPI);
// --- Final Steps ---
initializeZoom(); // Setup zoom now
const div = document.createElement("div"); // Create container for the SVG
div.appendChild(svg.node());
plot.element = div; // Assign the container element to the plot object
if (cfg.debug) console.log("Plot creation complete.");
return plot; // Return the fully constructed plot object
}