simple-ascii-chart
Version:
Simple ascii chart generator
1 lines • 111 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/constants/index.ts","../src/services/coords.ts","../src/services/settings.ts","../src/services/defaults.ts","../src/services/draw.ts","../src/services/overrides.ts","../src/services/plot.ts"],"sourcesContent":["import { plot } from './services/plot';\n\nexport * from './types';\nexport * from './constants';\n\nexport default plot;\nexport { plot };\n","/**\n * Symbols for drawing the axes on the graph.\n */\nexport const AXIS = {\n n: '▲', // Symbol for the top end of the Y-axis\n ns: '│', // Vertical line for the Y-axis\n y: '┤', // Right tick mark on the Y-axis\n nse: '└', // Bottom corner for the Y-axis meeting the X-axis\n x: '┬', // Top tick mark on the X-axis\n we: '─', // Horizontal line for the X-axis\n e: '▶', // Arrow symbol for the end of the X-axis\n intersectionXY: '┼', // Intersection of the X and Y axes\n intersectionX: '┴', // Bottom tick mark on the X-axis\n intersectionY: '├', // Left tick mark on the Y-axis\n};\n\n/**\n * Symbols for rendering chart elements, including lines and areas.\n */\nexport const CHART = {\n we: '━', // Bold horizontal line in the chart\n wns: '┓', // Top-right corner for vertical-to-horizontal connection\n ns: '┃', // Bold vertical line in the chart\n nse: '┗', // Bottom-left corner for vertical-to-horizontal connection\n wsn: '┛', // Bottom-right corner for vertical-to-horizontal connection\n sne: '┏', // Top-left corner for vertical-to-horizontal connection\n area: '█', // Filled area symbol for chart representation\n};\n\n/**\n * Symbol representing an empty space on the graph.\n */\nexport const EMPTY = ' ';\n\n/**\n * Symbols for drawing thresholds on the graph.\n */\nexport const THRESHOLDS = {\n x: '━', // Symbol for horizontal threshold line\n y: '┃', // Symbol for vertical threshold line\n};\n\n/**\n * Symbol for the point of the graph.\n */\nexport const POINT = '●';\n","import { SingleLine, Point, MultiLine } from '../types/index';\nimport { EMPTY } from '../constants/index';\n\n/**\n * Normalizes the input value to an array of strings.\n * @param value - The value to normalize, which can be a string, an array of strings, or undefined.\n * @returns {string[]} - An array of strings. If the input is undefined, an empty array is returned.\n */\nexport const normalize = (value?: string[] | string): string[] =>\n value === undefined ? [] : Array.isArray(value) ? value : [value];\n\n/**\n * Pads or trims an array of labels to a specified target length.\n * @param {string[]} labels - The array of labels to pad or trim.\n * @param {number} targetLength - The target length for the array.\n * @returns {string[]} - The padded or trimmed array of labels.\n */\nexport const padOrTrim = (labels: string[], targetLength: number): string[] => {\n if (labels.length > targetLength) return labels.slice(0, targetLength);\n if (labels.length < targetLength)\n return [...labels, ...Array(targetLength - labels.length).fill('')];\n return labels;\n};\n\n/**\n * Creates an array filled with a specified string.\n * @param {number} size - The size of the array.\n * @param {string} empty - The value to fill the array with (default: EMPTY).\n * @returns {string[]} - An array filled with the specified string.\n */\nexport const toEmpty = (size: number, empty: string = EMPTY): string[] =>\n Array(size >= 0 ? size : 0).fill(empty);\n\n/**\n * Converts a number or string to an array of its characters.\n * @param {number | string} input - The input to convert.\n * @returns {string[]} - An array of characters.\n */\nexport const toArray = (input: number | string): string[] => {\n return input.toString().split('');\n};\n\n/**\n * Removes duplicate values from an array.\n * @param {number[]} array - The array of numbers.\n * @returns {number[]} - An array containing only unique values.\n */\nexport const toUnique = (array: number[]): number[] => [...new Set(array)];\n\n/**\n * Calculates the distance between two integer coordinates by rounding to the nearest integers.\n * @param {number} x - The x-coordinate of the first point.\n * @param {number} y - The y-coordinate of the second point.\n * @returns {number} - The absolute distance between the rounded points.\n */\nexport const distance = (x: number, y: number): number => Math.abs(Math.round(x) - Math.round(y));\n\n/**\n * Flattens a multi-line array of points into a single array of points.\n * @param {MultiLine} array - The multi-line array.\n * @returns {Point[]} - A flat array of points.\n */\nexport const toFlat = (array: MultiLine): Point[] => ([] as Point[]).concat(...array);\n\n/**\n * Converts a multi-line array into arrays of unique x and y values.\n * @param {MultiLine} array - The multi-line array.\n * @returns {[number[], number[]]} - Arrays of unique x and y values.\n */\nexport const toArrays = (array: MultiLine): [number[], number[]] => {\n const rangeX: number[] = [];\n const rangeY: number[] = [];\n\n toFlat(array).forEach(([x, y]) => {\n rangeX.push(x);\n rangeY.push(y);\n });\n\n return [toUnique(rangeX), toUnique(rangeY)];\n};\n\n/**\n * Sorts a single-line array of points in ascending order based on the x-coordinate.\n * @param {SingleLine} array - The single-line array to sort.\n * @returns {SingleLine} - The sorted array.\n */\nexport const toSorted = (array: SingleLine): SingleLine =>\n array.sort(([x1], [x2]) => {\n if (x1 < x2) return -1;\n if (x1 > x2) return 1;\n return 0;\n });\n\n/**\n * Converts a number or undefined value to a point represented as an array.\n * @param {number} [x] - The x-coordinate (default: 0).\n * @param {number} [y] - The y-coordinate (default: 0).\n * @returns {Point} - The point represented as an array [x, y].\n */\nexport const toPoint = (x?: number, y?: number): Point => [x ?? 0, y ?? 0];\n\n/**\n * Returns a function that converts a coordinate (x, y) to scaled plot coordinates.\n * @param {number} plotWidth - The width of the plot.\n * @param {number} plotHeight - The height of the plot.\n * @returns {function} - A function that takes (x, y) and returns scaled plot coordinates [scaledX, scaledY].\n */\nexport const toPlot =\n (plotWidth: number, plotHeight: number) =>\n (x: number, y: number): Point => [\n Math.round((x / plotWidth) * plotWidth),\n plotHeight - 1 - Math.round((y / plotHeight) * plotHeight),\n ];\n\n/**\n * Returns a function that converts scaled plot coordinates (scaledX, scaledY) back to the original coordinates.\n * @param {number} plotWidth - The width of the plot.\n * @param {number} plotHeight - The height of the plot.\n * @returns {function} - A function that takes (scaledX, scaledY) and returns original coordinates [x, y].\n */\nexport const fromPlot =\n (plotWidth: number, plotHeight: number) =>\n (scaledX: number, scaledY: number): [number, number] => {\n const x = (scaledX / plotWidth) * plotWidth;\n const y = plotHeight - 1 - (scaledY / plotHeight) * (plotHeight - 1);\n return [Math.round(x), Math.round(y)];\n };\n\n/**\n * Finds the maximum or minimum value in a single-line array of points.\n * @param {SingleLine} arr - The single-line array to search for extrema.\n * @param {'max' | 'min'} type - 'max' to find the maximum value, 'min' for minimum (default is 'max').\n * @param {number} position - The position of the value within each point (default is 1).\n * @returns {number} - The maximum or minimum value found in the array.\n */\nexport const getExtrema = (arr: SingleLine, type: 'max' | 'min' = 'max', position = 1) =>\n arr.reduce(\n (previous, curr) => Math[type](previous, curr[position]),\n type === 'max' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY,\n );\n\n/**\n * Finds the maximum value in an array of numbers.\n * @param {number[]} arr - The array of numbers.\n * @returns {number} - The maximum value in the array.\n */\nexport const getMax = (arr: number[]) =>\n arr.reduce((previous, curr) => Math.max(previous, curr), Number.NEGATIVE_INFINITY);\n\n/**\n * Finds the minimum value in an array of numbers.\n * @param {number[]} arr - The array of numbers.\n * @returns {number} - The minimum value in the array.\n */\nexport const getMin = (arr: number[]) =>\n arr.reduce((previous, curr) => Math.min(previous, curr), Number.POSITIVE_INFINITY);\n\n/**\n * Returns a function that scales coordinates to fit within a specified range.\n * @param {[number, number]} domain - The original value range (min and max).\n * @param {[number, number]} range - The range to scale the values into.\n * @returns {(value: number) => number} - A function for scaling coordinates.\n */\nexport const scaler = ([domainMin, domainMax]: number[], [rangeMin, rangeMax]: number[]) => {\n const domainLength = Math.sqrt(Math.abs((domainMax - domainMin) ** 2)) || 1;\n const rangeLength = Math.sqrt((rangeMax - rangeMin) ** 2);\n\n return (domainValue: number) =>\n rangeMin + (rangeLength * (domainValue - domainMin)) / domainLength;\n};\n\n/**\n * Scales a point's coordinates to fit within the specified plot dimensions.\n * @param {Point} point - The point to scale.\n * @param {number} plotWidth - The width of the plot.\n * @param {number} plotHeight - The height of the plot.\n * @param {number[]} rangeX - The range of x values.\n * @param {number[]} rangeY - The range of y values.\n * @returns {Point} - The scaled point.\n */\nexport const toCoordinates = (\n point: Point,\n plotWidth: number,\n plotHeight: number,\n rangeX: number[],\n rangeY: number[],\n): Point => {\n const getXCoord = scaler(rangeX, [0, plotWidth - 1]);\n const getYCoord = scaler(rangeY, [0, plotHeight - 1]);\n\n return [Math.round(getXCoord(point[0])), Math.round(getYCoord(point[1]))];\n};\n\n/**\n * Scales a list of coordinates to fit within the specified plot dimensions.\n * @param {SingleLine} coordinates - The list of coordinates to scale.\n * @param {number} plotWidth - The width of the plot.\n * @param {number} plotHeight - The height of the plot.\n * @param {number[]} [rangeX] - The range of x values (defaults to min and max from coordinates).\n * @param {number[]} [rangeY] - The range of y values (defaults to min and max from coordinates).\n * @returns {SingleLine} - The scaled list of coordinates.\n */\nexport const getPlotCoords = (\n coordinates: SingleLine,\n plotWidth: number,\n plotHeight: number,\n rangeX?: number[],\n rangeY?: number[],\n): SingleLine => {\n const getXCoord = scaler(\n rangeX || [getExtrema(coordinates, 'min', 0), getExtrema(coordinates, 'max', 0)],\n [0, plotWidth - 1],\n );\n const getYCoord = scaler(rangeY || [getExtrema(coordinates, 'min'), getExtrema(coordinates)], [\n 0,\n plotHeight - 1,\n ]);\n\n return coordinates.map(([x, y]) => [getXCoord(x), getYCoord(y)]);\n};\n\n/**\n * Computes the axis center point based on specified plot dimensions and ranges.\n * @param {MaybePoint} axisCenter - The center point for the axis.\n * @param {number} plotWidth - The width of the plot.\n * @param {number} plotHeight - The height of the plot.\n * @param {number[]} rangeX - The range of x values.\n * @param {number[]} rangeY - The range of y values.\n * @param {number[]} initialValue - The initial axis values.\n * @returns {Point} - The center point of the axis.\n */\nexport const getAxisCenter = (\n axisCenter: Point | [number | undefined, number | undefined] | undefined,\n plotWidth: number,\n plotHeight: number,\n rangeX: number[],\n rangeY: number[],\n initialValue: [number, number],\n): { x: number; y: number } => {\n const axis = { x: initialValue[0], y: initialValue[1] };\n\n if (axisCenter) {\n const [x, y] = axisCenter;\n\n if (typeof x === 'number') {\n const xScaler = scaler(rangeX, [0, plotWidth - 1]);\n axis.x = Math.round(xScaler(x));\n }\n\n if (typeof y === 'number') {\n const yScaler = scaler(rangeY, [0, plotHeight - 1]);\n axis.y = plotHeight - Math.round(yScaler(y));\n }\n }\n\n return axis;\n};\n","import { CHART } from '../constants/index';\nimport { Color, ColorGetter, Formatter, MultiLine } from '../types/index';\n\nconst colorMap: Record<Color, string> = {\n ansiBlack: '\\u001b[30m',\n ansiRed: '\\u001b[31m',\n ansiGreen: '\\u001b[32m',\n ansiYellow: '\\u001b[33m',\n ansiBlue: '\\u001b[34m',\n ansiMagenta: '\\u001b[35m',\n ansiCyan: '\\u001b[36m',\n ansiWhite: '\\u001b[37m',\n};\n\n/**\n * Maps a color name to its corresponding ANSI color code.\n * @param {Color} color - The color to map.\n * @returns {string} - The ANSI escape code for the specified color.\n */\nexport const getAnsiColor = (color: Color): string => colorMap[color] || colorMap.ansiWhite;\n\n/**\n * Configures and applies colors to chart symbols based on the specified color or series.\n * @param {Color | Color[] | ColorGetter | undefined} color - The color setting for the series.\n * @param {number} series - The index of the series.\n * @param {Partial<typeof CHART> | undefined} chartSymbols - Custom symbols for the chart, if any.\n * @param {MultiLine} input - The dataset used in the chart.\n * @param {boolean} [fillArea] - If true, fills the area under the plot with the chart's fill symbol.\n * @returns {object} - An object with chart symbols applied in the specified color(s).\n */\nexport const getChartSymbols = (\n color: Color | Color[] | undefined | ColorGetter,\n series: number,\n chartSymbols: Partial<typeof CHART> | void,\n input: MultiLine,\n fillArea?: boolean,\n) => {\n const chart = { ...CHART, ...chartSymbols };\n\n // Apply the fill area symbol to all chart symbols if fillArea is true\n if (fillArea) {\n Object.entries(chart).forEach(([key]) => {\n chart[key as keyof typeof chart] = chart.area;\n });\n }\n\n // Determine the color for the current series and apply it to all chart symbols\n if (color) {\n let currentColor: Color = 'ansiWhite';\n\n if (Array.isArray(color)) {\n currentColor = color[series];\n } else if (typeof color === 'function') {\n currentColor = color(series, input);\n } else {\n currentColor = color;\n }\n\n Object.entries(chart).forEach(([key, symbol]) => {\n chart[key as keyof typeof chart] = `${getAnsiColor(currentColor)}${symbol}\\u001b[0m`;\n });\n }\n\n return chart;\n};\n\n/**\n * Formats a number for display, converting values >= 1000 to a \"k\" notation.\n * @param {number} value - The value to format.\n * @returns {string | number} - The formatted value as a string with \"k\" for thousands or as a rounded number.\n */\nexport const defaultFormatter: Formatter = (value) => {\n if (Math.abs(value) >= 1000) {\n const rounded = value / 1000;\n return rounded % 1 === 0 ? `${rounded}k` : `${rounded.toFixed(3)}k`;\n }\n return Number(value.toFixed(3));\n};\n","import { AXIS, EMPTY, POINT, THRESHOLDS } from '../constants';\nimport {\n Symbols,\n MultiLine,\n Formatter,\n Coordinates,\n GraphPoint,\n Threshold,\n MaybePoint,\n} from '../types';\nimport { toArrays, getMin, getMax, toArray, padOrTrim, normalize } from './coords';\n\n/**\n * Merges custom symbols with default axis symbols and defines plot symbols.\n * @param {object} options - An object containing optional custom symbols.\n * @param {Symbols} options.symbols - Custom symbols for the plot.\n * @returns {object} - Object containing the merged axis symbols, and defined symbols for empty, background, and border.\n */\nexport const getSymbols = ({ symbols }: { symbols?: Symbols }) => {\n const emptySymbol = symbols?.empty || EMPTY;\n return {\n axisSymbols: { ...AXIS, ...symbols?.axis },\n emptySymbol,\n backgroundSymbol: symbols?.background || emptySymbol,\n borderSymbol: symbols?.border,\n thresholdSymbols: {\n x: symbols?.thresholds?.x || THRESHOLDS.x,\n y: symbols?.thresholds?.y || THRESHOLDS.y,\n },\n pointSymbol: symbols?.point || POINT,\n };\n};\n\n/**\n * Determines plot size and range based on provided data and dimensions.\n * @param {object} options - An object containing input data and optional dimensions.\n * @param {MultiLine} options.input - The multiline array of points.\n * @param {number} [options.width] - Optional width of the plot.\n * @param {number} [options.height] - Optional height of the plot.\n * @param {MaybePoint} [options.axisCenter] - Optional axis center point.\n * @param {[number, number]} [options.yRange] - Optional range for the y-axis.\n * @returns {object} - Object containing min x value, plot width, plot height, and x and y expansions.\n */\nexport const getChartSize = ({\n input,\n width,\n height,\n yRange,\n axisCenter,\n}: {\n input: MultiLine;\n width?: number;\n height?: number;\n axisCenter?: MaybePoint;\n yRange?: [number, number];\n}) => {\n const [inputRangeX, inputRangeY] = toArrays(input);\n\n const rangeX = [...inputRangeX, axisCenter?.[0]].filter((v) => typeof v === 'number') as number[];\n const rangeY = [...inputRangeY, axisCenter?.[1]].filter((v) => typeof v === 'number') as number[];\n\n const minX = getMin(rangeX);\n const maxX = getMax(rangeX);\n const minY = getMin(rangeY);\n const maxY = getMax(rangeY);\n\n const expansionX = [minX, maxX];\n\n const expansionY = yRange || [minY, maxY];\n\n // Set default plot dimensions if not provided\n const plotWidth = width || rangeX.length;\n\n let plotHeight = Math.round(height || maxY - minY + 1);\n\n // Adjust plot height for small value ranges if no height is provided\n if (!height && plotHeight < 3) {\n plotHeight = rangeY.length;\n }\n\n return {\n minX,\n minY,\n plotWidth,\n plotHeight,\n expansionX,\n expansionY,\n };\n};\n\n/**\n * Calculates shifts for x and y labels, based on the longest label length.\n * @param {object} options - The input data and formatting options.\n * @param {MultiLine} options.input - The multiline array of points.\n * @param {Formatter} options.transformLabel - A function to transform label values.\n * @param {number[]} options.expansionX - The x-axis range.\n * @param {number[]} options.expansionY - The y-axis range.\n * @param {number} options.minX - The minimum x value for label calculation.\n * @param {boolean} [options.showTickLabel] - Flag to indicate if tick labels should be shown.\n * @returns {object} - Object containing the calculated xShift and yShift.\n */\nexport const getLabelShift = ({\n input,\n transformLabel,\n expansionX,\n expansionY,\n minX,\n showTickLabel,\n}: {\n input: MultiLine;\n transformLabel: Formatter;\n expansionX: number[];\n expansionY: number[];\n minX: number;\n showTickLabel?: boolean;\n}) => {\n // Helper to compute the length of a formatted label\n const getLength = (value: number, axis: 'x' | 'y'): number => {\n const formatted = transformLabel(value, { axis, xRange: expansionX, yRange: expansionY });\n return toArray(formatted).length;\n };\n\n // Combine all points into one array for iteration\n const points = input.flat<MultiLine>();\n\n // Determine the maximum label lengths for x and y\n const { x: xShift, y: longestY } = points.reduce(\n (acc, [x, y]) => ({\n x: Math.max(acc.x, getLength(x, 'x')),\n y: Math.max(acc.y, getLength(y, 'y')),\n }),\n { x: 0, y: 0 },\n );\n\n if (!showTickLabel) {\n // For minimal mode, ensure space for the axis symbol and labels\n const minXLength = getLength(minX, 'x');\n const baseShift = Math.max(0, minXLength - 2);\n return {\n xShift,\n yShift: Math.max(baseShift, longestY),\n };\n }\n\n // Full mode: add extra padding for tick labels\n return { xShift, yShift: longestY + 1 };\n};\n\n/**\n * Normalizes raw input data into a consistent multi-line format.\n * @param {object} options - Contains the raw input data.\n * @param {Coordinates} options.rawInput - Input coordinates, either single or multi-line.\n * @returns {MultiLine} - The formatted data as a multi-line array of points.\n */\nexport const getInput = ({ rawInput }: { rawInput: Coordinates }) => {\n let input = rawInput;\n\n // Convert single-line data to a multi-line format if needed\n if (typeof input[0]?.[0] === 'number') {\n input = [rawInput] as MultiLine;\n }\n\n return input as MultiLine;\n};\n\n/**\n * Generates legend data based on the provided points, thresholds, and series.\n * @param {object} options - Contains points, thresholds, and series data.\n * @param {Coordinates} options.points - The coordinates of the points.\n * @param {THRESHOLDS} options.thresholds - The thresholds for the plot.\n * @param {string[]} options.series - The series names for the plot.\n * @param {string[]} options.pointsSeries - The series names for the points.\n * @param {string[]} options.thresholdsSeries - The series names for the thresholds.\n * @param {string[]} options.dataSeries - The series names for the data.\n * @param {MultiLine} options.input - The input data for the plot.\n * @returns {object} - Object containing the series, points, and thresholds for the legend.\n *\n */\nexport const getLegendData = ({\n input,\n thresholds,\n points,\n pointsSeries,\n thresholdsSeries,\n dataSeries,\n}: {\n input: MultiLine;\n points?: GraphPoint[];\n thresholds?: Threshold[];\n pointsSeries?: string[] | string;\n thresholdsSeries?: string[] | string;\n dataSeries?: string[] | string;\n}) => {\n const legendSeries = dataSeries && input ? padOrTrim(normalize(dataSeries), input.length) : [];\n\n const legendPoints =\n pointsSeries && points ? padOrTrim(normalize(pointsSeries), points.length) : [];\n\n const legendThresholds =\n thresholdsSeries && thresholds ? padOrTrim(normalize(thresholdsSeries), thresholds.length) : [];\n\n return {\n series: legendSeries,\n points: legendPoints,\n thresholds: legendThresholds,\n };\n};\n","import { AXIS, CHART, POINT } from '../constants';\nimport {\n CustomSymbol,\n Formatter,\n Graph,\n GraphMode,\n LineFormatterArgs,\n MaybePoint,\n MultiLine,\n Point,\n Symbols,\n} from '../types';\nimport { distance, getPlotCoords, toArray, toEmpty, toPlot, toPoint } from './coords';\n\n/**\n * Places a symbol at a specific position on the graph.\n * @param {Object} params - Object containing parameters.\n * @param {Graph} params.graph - The graph matrix where the symbol will be drawn.\n * @param {number} params.scaledX - X-coordinate on the graph (scaled).\n * @param {number} params.scaledY - Y-coordinate on the graph (scaled).\n * @param {string} params.symbol - Symbol to draw on the graph.\n * @param {boolean} [params.debugMode=false] - If true, logs errors for out-of-bounds access.\n */\nexport const drawPosition = ({\n graph,\n scaledX,\n scaledY,\n symbol,\n debugMode = false,\n}: {\n graph: Graph;\n scaledX: number;\n scaledY: number;\n symbol: string;\n debugMode?: boolean;\n}) => {\n if (debugMode) {\n // Handle out-of-bounds for Y\n if (scaledY >= graph.length || scaledY < 0) {\n console.log(`Drawing at [${scaledX}, ${scaledY}]`, 'Error: out of bounds Y', {\n graph,\n scaledX,\n scaledY,\n });\n return;\n }\n // Handle out-of-bounds for X\n if (scaledX >= graph[scaledY].length || scaledX < 0) {\n console.log(`Drawing at [${scaledX}, ${scaledY}]`, 'Error: out of bounds X', {\n graph,\n scaledX,\n scaledY,\n });\n return;\n }\n }\n\n // Draw the symbol if within bounds\n try {\n graph[scaledY][scaledX] = symbol;\n } catch (error) {\n // Fail silently without logging if debugMode is false\n }\n};\n\n/**\n * Draws a tick mark at the end of the X-axis, handling bounds and axis center.\n * @param {Object} params - Configuration options for drawing the X-axis tick.\n * @param {boolean} params.hasPlaceToRender - True if there is enough space to render the tick.\n * @param {Point | [number | undefined, number | undefined]} [params.axisCenter] - Coordinates of the axis center (optional).\n * @param {number} params.yPos - The Y-position of the tick mark.\n * @param {Graph} params.graph - The graph matrix being drawn on.\n * @param {number} params.yShift - The Y-axis shift offset.\n * @param {number} params.i - The current iteration index.\n * @param {number} params.scaledX - The scaled X-position for rendering the tick.\n * @param {number} params.shift - X-axis offset to adjust tick positioning.\n * @param {boolean} [params.hideXAxisTicks] - If true, hides the X-axis ticks.\n * @param {number} params.signShift - Additional shift based on the sign of the axis.\n * @param {Symbols['axis']} params.axisSymbols - Symbols used for the axis rendering.\n * @param {string[]} params.pointXShift - Array of characters representing the X-axis labels.\n * @param {boolean} [params.debugMode=false] - If true, logs errors for out-of-bounds access.\n */\nexport const drawXAxisEnd = ({\n hasPlaceToRender,\n yPos,\n graph,\n yShift,\n i,\n scaledX,\n hideXAxisTicks,\n pointXShift,\n debugMode,\n axisCenter,\n}: {\n hasPlaceToRender: boolean;\n yPos: number;\n graph: Graph;\n hideXAxisTicks?: boolean;\n yShift: number;\n i: number;\n scaledX: number;\n axisCenter?: MaybePoint;\n pointXShift: string[];\n debugMode?: boolean;\n}) => {\n if (hideXAxisTicks) {\n return;\n }\n\n // Adjusts Y position based on render space and axis center presence\n const yShiftWhenOccupied = hasPlaceToRender ? -1 : 0;\n const yShiftWhenHasAxisCenter = 0;\n\n let graphY = yPos + yShiftWhenOccupied + yShiftWhenHasAxisCenter;\n\n // Ensure graphY stays within valid bounds\n if (graphY < 0) graphY = 0;\n else if (graphY >= graph.length) graphY = graph.length - 1;\n\n // Adjust X position for rendering the tick\n let graphX = scaledX + yShift - i + (axisCenter ? 1 : 2);\n if (graphX < 0) graphX = 0;\n else if (graphX >= graph[graphY].length) graphX = graph[graphY].length - 1;\n\n // Draw the tick label\n drawPosition({\n debugMode,\n graph,\n scaledX: graphX,\n scaledY: graphY,\n symbol: pointXShift[pointXShift.length - 1 - i],\n });\n};\n\nexport const drawXAxisTick = ({\n graph,\n xPosition,\n hideXAxisTicks,\n axisSymbols,\n debugMode,\n axis,\n}: {\n axis: { x: number; y: number };\n graph: Graph;\n hideXAxisTicks?: boolean;\n xPosition: number;\n axisSymbols: Symbols['axis'];\n debugMode?: boolean;\n}) => {\n if (hideXAxisTicks) {\n return;\n }\n\n if (\n graph[axis.y][xPosition] === axisSymbols?.ns ||\n graph[axis.y][xPosition] === axisSymbols?.we\n ) {\n drawPosition({\n debugMode,\n graph,\n scaledX: xPosition,\n scaledY: axis.y,\n symbol: axisSymbols?.x || AXIS.x,\n });\n }\n};\n\n/**\n * Draws tick marks for the Y-axis based on axis configurations and scales.\n * @param {Object} params - Configuration options for drawing the Y-axis ticks.\n * @param {Graph} params.graph - The graph matrix.\n * @param {number} params.scaledY - Scaled Y-coordinate.\n * @param {number} params.yShift - Shift applied to the Y-axis.\n * @param {Object} params.axis - Object defining the axis position.\n * @param {number} params.pointY - The actual Y-coordinate of the point.\n * @param {Formatter} params.transformLabel - Function to format the label for the Y-axis.\n * @param {Symbols['axis']} params.axisSymbols - Symbols used for drawing the axis.\n * @param {number[]} params.expansionX - Array of X-axis expansion factors.\n * @param {number[]} params.expansionY - Array of Y-axis expansion factors.\n * @param {number} params.plotHeight - The height of the plot.\n * @param {boolean} [params.showTickLabel] - If true, displays tick labels for all points.\n * @param {number} params.axis.x - X-position of the Y-axis on the graph.\n * @param {number} params.axis.y - Y-position of the X-axis on the graph.\n * @param {boolean} [params.hideYAxisTicks] - If true, hides Y-axis ticks.\n * @param {boolean} [params.debugMode=false] - If true, logs errors for out-of-bounds access.\n */\nexport const drawYAxisEnd = ({\n graph,\n scaledY,\n yShift,\n axis,\n pointY,\n transformLabel,\n axisSymbols,\n expansionX,\n expansionY,\n plotHeight,\n showTickLabel,\n hideYAxisTicks,\n debugMode,\n}: {\n graph: Graph;\n scaledY: number;\n yShift: number;\n plotHeight: number;\n axis: { x: number; y: number };\n pointY: number;\n transformLabel: Formatter;\n axisSymbols: Symbols['axis'];\n expansionX: number[];\n expansionY: number[];\n hideYAxisTicks?: boolean;\n showTickLabel?: boolean;\n debugMode?: boolean;\n}) => {\n if (showTickLabel) {\n const yMax = Math.max(...expansionY);\n const yMin = Math.min(...expansionY);\n const numTicks = plotHeight;\n const yStep = (yMax - yMin) / numTicks;\n\n // Draw ticks for each label\n for (let i = 0; i <= numTicks; i++) {\n const yValue = Math.round(yMax - i * yStep); // Ensure whole numbers\n const scaledYPos = ((yMax - yValue) / (yMax - yMin)) * (plotHeight - 1);\n const graphYPos = Math.floor(scaledYPos) + 1;\n\n // Ensure the Y position is within bounds\n if (graphYPos >= 0 && graphYPos < graph.length) {\n const pointYShift = toArray(\n transformLabel(yValue, { axis: 'y', xRange: expansionX, yRange: expansionY }),\n );\n for (let j = 0; j < pointYShift.length; j++) {\n const colIndex = axis.x + yShift - j;\n if (colIndex >= 0 && colIndex < graph[graphYPos].length) {\n drawPosition({\n debugMode,\n graph,\n scaledX: colIndex,\n scaledY: graphYPos,\n symbol: pointYShift[pointYShift.length - 1 - j],\n });\n }\n }\n\n const tickMarkIndex = axis.x + yShift + 1;\n if (tickMarkIndex >= 0 && tickMarkIndex < graph[graphYPos].length) {\n if (\n graph[graphYPos][tickMarkIndex] === axisSymbols?.ns ||\n graph[graphYPos][tickMarkIndex] === axisSymbols?.we\n ) {\n drawPosition({\n debugMode,\n graph,\n scaledX: tickMarkIndex,\n scaledY: graphYPos,\n symbol: axisSymbols?.y || AXIS.y,\n });\n }\n }\n }\n }\n return;\n }\n\n if (hideYAxisTicks) {\n return;\n }\n\n // make sure that values are within bounds\n const row = scaledY + 1;\n const col = axis.x + yShift + 1;\n\n // Render ticks for specific data values\n if (\n row >= 0 &&\n row < graph.length &&\n col >= 0 &&\n graph[row] &&\n col < graph[row].length &&\n graph[row][col] !== axisSymbols?.y\n ) {\n const pointYShift = toArray(\n transformLabel(pointY, { axis: 'y', xRange: expansionX, yRange: expansionY }),\n );\n for (let i = 0; i < pointYShift.length; i++) {\n drawPosition({\n debugMode,\n graph,\n scaledX: axis.x + yShift - i,\n scaledY: scaledY + 1,\n symbol: pointYShift[pointYShift.length - 1 - i],\n });\n }\n if (\n graph[scaledY + 1][axis.x + yShift + 1] === axisSymbols?.ns ||\n graph[scaledY + 1][axis.x + yShift + 1] === axisSymbols?.we\n ) {\n drawPosition({\n debugMode,\n graph,\n scaledX: axis.x + yShift + 1,\n scaledY: scaledY + 1,\n symbol: axisSymbols?.y || AXIS.y,\n });\n }\n }\n};\n\n/**\n * Draws both X and Y axes on the graph according to visibility and center configurations.\n * @param {Object} params - Configuration options for drawing axes.\n * @param {Graph} params.graph - The graph matrix.\n * @param {boolean} [params.hideXAxis] - If true, hides the X-axis.\n * @param {boolean} [params.hideYAxis] - If true, hides the Y-axis.\n * @param {MaybePoint} [params.axisCenter] - Optional axis center coordinates.\n * @param {Symbols['axis']} params.axisSymbols - Symbols used for axis rendering.\n * @param {Object} params.axis - Object defining the axis position (x and y coordinates).\n * @param {number} params.axis.x - X-position of the Y-axis on the graph.\n * @param {number} params.axis.y - Y-position of the X-axis on the graph.\n * @param {boolean} [params.debugMode=false] - If true, logs errors for out-of-bounds access.\n */\nexport const drawAxis = ({\n graph,\n hideXAxis,\n hideYAxis,\n axisCenter,\n axisSymbols,\n axis,\n debugMode,\n}: {\n graph: Graph;\n axis: { x: number; y: number };\n hideXAxis?: boolean;\n axisCenter?: MaybePoint;\n hideYAxis?: boolean;\n axisSymbols: Symbols['axis'];\n debugMode?: boolean;\n}) => {\n graph.forEach((line, index) => {\n line.forEach((_, curr) => {\n let lineChar = '';\n\n if (curr === axis.x && !hideYAxis) {\n if (index === 0) {\n lineChar = axisSymbols?.n || AXIS.n;\n } else if (index === graph.length - 1 && !axisCenter && !(hideYAxis || hideXAxis)) {\n lineChar = axisSymbols?.nse || AXIS.nse;\n } else {\n lineChar = axisSymbols?.ns || AXIS.ns;\n }\n } else if (index === axis.y && !hideXAxis) {\n if (curr === line.length - 1) {\n lineChar = axisSymbols?.e || AXIS.e;\n } else {\n lineChar = axisSymbols?.we || AXIS.we;\n }\n }\n\n if (lineChar) {\n drawPosition({ debugMode, graph, scaledX: curr, scaledY: index, symbol: lineChar });\n }\n });\n });\n};\n\n/**\n * Initializes an empty graph based on plot dimensions and a given symbol.\n * @param {Object} params - Configuration options for the graph.\n * @param {number} params.plotWidth - Width of the plot area.\n * @param {number} params.plotHeight - Height of the plot area.\n * @param {string} params.emptySymbol - Symbol used to fill empty cells.\n * @returns {Graph} - An initialized empty graph matrix.\n */\nexport const drawGraph = ({\n plotWidth,\n plotHeight,\n emptySymbol,\n}: {\n plotWidth: number;\n plotHeight: number;\n emptySymbol: string;\n}) => {\n const callback = () => toEmpty(plotWidth + 2, emptySymbol);\n return Array.from({ length: plotHeight + 2 }, callback);\n};\n\n/**\n * Renders the graph into a string format for output.\n * @param {Object} params - Configuration options for rendering the graph.\n * @param {Graph} params.graph - The graph matrix to render.\n * @returns {string} - The rendered graph as a string.\n */\nexport const drawChart = ({ graph }: { graph: Graph }) =>\n `\\n${graph.map((line) => line.join('')).join('\\n')}\\n`;\n\n/**\n * Renders a custom line on the graph based on formatter specifications.\n * @param {Object} params - Configuration options for rendering custom lines.\n * @param {Point[]} params.sortedCoords - Sorted list of coordinates.\n * @param {number} params.scaledX - X-axis scaling.\n * @param {number} params.scaledY - Y-axis scaling.\n * @param {number} params.minY - Minimum Y value.\n * @param {number} params.minX - Minimum X value.\n * @param {MultiLine} params.input - Input data points.\n * @param {number[]} params.expansionX - X-axis expansion range.\n * @param {number[]} params.expansionY - Y-axis expansion range.\n * @param {function} params.toPlotCoordinates - Function to convert coordinates to plot positions.\n * @param {number} params.index - Current index in the coordinate array.\n * @param {function} params.lineFormatter - Custom function for line formatting.\n * @param {Graph} params.graph - The graph matrix to modify.\n * @param {boolean} [params.debugMode=false] - If true, logs errors for out-of-bounds access.\n */\nexport const drawCustomLine = ({\n sortedCoords,\n scaledX,\n scaledY,\n input,\n index,\n lineFormatter,\n graph,\n toPlotCoordinates,\n expansionX,\n expansionY,\n minY,\n minX,\n debugMode,\n}: {\n sortedCoords: Point[];\n scaledX: number;\n scaledY: number;\n input: MultiLine;\n index: number;\n minY: number;\n minX: number;\n expansionX: number[];\n expansionY: number[];\n toPlotCoordinates: (x: number, y: number) => Point;\n lineFormatter: (args: LineFormatterArgs) => CustomSymbol | CustomSymbol[];\n graph: Graph;\n debugMode?: boolean;\n}) => {\n const lineFormatterArgs = {\n x: sortedCoords[index][0],\n y: sortedCoords[index][1],\n plotX: scaledX + 1,\n plotY: scaledY + 1,\n index,\n input: input[0],\n minY,\n minX,\n toPlotCoordinates,\n expansionX,\n expansionY,\n };\n const customSymbols = lineFormatter(lineFormatterArgs);\n if (Array.isArray(customSymbols)) {\n customSymbols.forEach(({ x: symbolX, y: symbolY, symbol }: CustomSymbol) => {\n drawPosition({ debugMode, graph, scaledX: symbolX, scaledY: symbolY, symbol });\n });\n } else {\n drawPosition({\n debugMode,\n graph,\n scaledX: customSymbols.x,\n scaledY: customSymbols.y,\n symbol: customSymbols.symbol,\n });\n }\n};\n\n/**\n * Renders a line between two points on the graph using defined chart symbols.\n * @param {Object} params - Configuration options for drawing a line.\n * @param {number} params.index - Current index in the coordinate array.\n * @param {Point[]} params.arr - List of points for the line.\n * @param {Graph} params.graph - The graph matrix to modify.\n * @param {number} params.scaledX - X-axis scaling.\n * @param {number} params.scaledY - Y-axis scaling.\n * @param {number} params.plotHeight - Height of the plot area.\n * @param {string} params.emptySymbol - Symbol used to fill empty cells.\n * @param {Object} params.axis - Axis position.\n * @param {Symbols['chart']} params.chartSymbols - Symbols used for chart rendering.\n * @param {MaybePoint} params.axisCenter - Axis position selected by user.\n * @param {number} params.axis.x - X-position of the Y-axis on the graph.\n * @param {number} params.axis.y - Y-position of the X-axis on the graph.\n * @param {string} params.mode - Graph mode (e.g., 'line', 'point').\n * @param {boolean} [params.debugMode=false] - If true, logs errors for out-of-bounds access.\n */\nexport const drawLine = ({\n index,\n arr,\n graph,\n scaledX,\n scaledY,\n plotHeight,\n emptySymbol,\n chartSymbols,\n axisCenter,\n debugMode,\n axis,\n mode,\n}: {\n index: number;\n arr: Point[];\n graph: Graph;\n scaledX: number;\n scaledY: number;\n plotHeight: number;\n emptySymbol: string;\n chartSymbols: Symbols['chart'];\n axisCenter: MaybePoint;\n axis: { x: number; y: number };\n debugMode?: boolean;\n mode: GraphMode;\n}) => {\n const [currX, currY] = arr[index];\n if (mode === 'bar' || mode === 'horizontalBar') {\n const positions: [number, number][] = [];\n const axisCenterShift = axisCenter ? 0 : 1;\n // For vertical bar chart\n if (mode === 'bar') {\n let i;\n // Check if the value is positive or negative\n if (scaledY >= axis.y) {\n // For positive values, draw from the value down to the axis\n i = scaledY;\n while (i >= axis.y) {\n positions.push([i, scaledX + axisCenterShift]);\n i -= 1;\n }\n } else {\n // For negative values, draw from the value up to the axis\n i = scaledY;\n while (i <= axis.y) {\n positions.push([i, scaledX + axisCenterShift]);\n i += 1;\n }\n }\n }\n\n // For horizontal bar chart\n if (mode === 'horizontalBar') {\n let i;\n if (scaledX >= axis.x) {\n // For positive values, draw rightward from the value to the axis\n i = scaledX;\n while (i >= axis.x) {\n positions.push([scaledY + 1, i]);\n i -= 1;\n }\n } else {\n // For negative values, draw leftward from the value to the axis\n i = scaledX;\n while (i <= axis.x) {\n positions.push([scaledY + 1, i]);\n i += 1;\n }\n }\n }\n\n // Draw all calculated positions\n positions.forEach(([y, x]) => {\n drawPosition({\n debugMode,\n graph,\n scaledX: x,\n scaledY: y,\n symbol: chartSymbols?.area || CHART.area,\n });\n });\n\n return;\n }\n\n if (mode === 'point') {\n drawPosition({\n debugMode,\n graph,\n scaledX: scaledX + 1,\n scaledY: scaledY + 1,\n symbol: POINT,\n });\n return;\n }\n\n if (index - 1 >= 0) {\n const [prevX, prevY] = arr[index - 1];\n Array(distance(currY, prevY))\n .fill('')\n .forEach((_, steps, array) => {\n if (Math.round(prevY) > Math.round(currY)) {\n drawPosition({\n debugMode,\n graph,\n scaledX,\n scaledY: scaledY + 1,\n symbol: chartSymbols?.nse || CHART.nse,\n });\n if (steps === array.length - 1) {\n drawPosition({\n debugMode,\n graph,\n scaledX,\n scaledY: scaledY - steps,\n symbol: chartSymbols?.wns || CHART.wns,\n });\n } else {\n drawPosition({\n debugMode,\n graph,\n scaledX,\n scaledY: scaledY - steps,\n symbol: chartSymbols?.ns || CHART.ns,\n });\n }\n } else {\n drawPosition({\n debugMode,\n graph,\n scaledX,\n scaledY: scaledY + steps + 2,\n symbol: chartSymbols?.wsn || CHART.wsn,\n });\n\n drawPosition({\n debugMode,\n graph,\n scaledX,\n scaledY: scaledY + steps + 1,\n symbol: chartSymbols?.ns || CHART.ns,\n });\n }\n });\n\n if (Math.round(prevY) < Math.round(currY)) {\n drawPosition({\n debugMode,\n graph,\n scaledX,\n scaledY: scaledY + 1,\n symbol: chartSymbols?.sne || CHART.sne,\n });\n } else if (Math.round(prevY) === Math.round(currY)) {\n if (graph[scaledY + 1][scaledX] === emptySymbol) {\n drawPosition({\n debugMode,\n graph,\n scaledX,\n scaledY: scaledY + 1,\n symbol: chartSymbols?.we || CHART.we,\n });\n }\n }\n\n const distanceX = distance(currX, prevX);\n Array(distanceX ? distanceX - 1 : 0)\n .fill('')\n .forEach((_, steps) => {\n const thisY = plotHeight - Math.round(prevY);\n drawPosition({\n debugMode,\n graph,\n scaledX: Math.round(prevX) + steps + 1,\n scaledY: thisY,\n symbol: chartSymbols?.we || CHART.we,\n });\n });\n }\n\n if (arr.length - 1 === index) {\n drawPosition({\n debugMode,\n graph,\n scaledX: scaledX + 1,\n scaledY: scaledY + 1,\n symbol: chartSymbols?.we || CHART.we,\n });\n }\n};\n\n/**\n * Applies shifts to the graph and adjusts empty symbols and scaling factors.\n * @param {Object} params - Configuration options for applying shifts.\n * @param {Graph} params.graph - The graph matrix.\n * @param {number} params.plotWidth - The width of the plot area.\n * @param {string} params.emptySymbol - The symbol used to fill empty cells.\n * @param {number[][]} params.scaledCoords - Scaled coordinates for shifting.\n * @param {number} params.xShift - X-axis shift offset.\n * @param {number} params.yShift - Y-axis shift offset.\n * @returns {Object} - An object indicating if the graph needs to be moved.\n */\nexport const drawShift = ({\n graph,\n plotWidth,\n emptySymbol,\n scaledCoords,\n xShift,\n yShift,\n}: {\n graph: Graph;\n plotWidth: number;\n emptySymbol: string;\n scaledCoords: number[][];\n xShift: number;\n yShift: number;\n}) => {\n let realYShift = 0;\n graph.push(toEmpty(plotWidth + 2, emptySymbol)); // bottom shift\n realYShift += 1;\n\n let step = plotWidth;\n scaledCoords.forEach(([x], index) => {\n if (scaledCoords[index - 1]) {\n const current = x - scaledCoords[index - 1][0];\n step = current <= step ? current : step;\n }\n });\n\n const hasToBeMoved = step < xShift;\n if (hasToBeMoved) {\n realYShift += 1;\n graph.push(toEmpty(plotWidth + 1, emptySymbol));\n }\n\n const realXShift = yShift + 1;\n\n graph.forEach((line) => {\n for (let i = 0; i <= yShift; i++) {\n line.unshift(emptySymbol);\n }\n });\n\n return { hasToBeMoved, realYShift, realXShift };\n};\n\n/**\n * Draws ticks on the Y-axis based on axis configurations and scaling.\n * @param {Object} params - Configuration options for drawing the Y-axis ticks.\n * @param {boolean} [params.debugMode=false] - If true, logs errors for out-of-bounds access.\n * @param {boolean} [params.showTickLabel] - If true, shows tick labels for all points.\n * @param {boolean} [params.hideYAxisTicks] - If true, hides the Y-axis ticks.\n * @param {number} params.plotHeight - The height of the plot area.\n * @param {Graph} params.graph - The graph matrix.\n * @param {number} params.plotWidth - The width of the plot area.\n * @param {number} params.yShift - Shift applied to the Y-axis.\n * @param {Object} params.axis - Object defining the axis position.\n * @param {number} params.axis.x - X-position of the Y-axis on the graph.\n * @param {number} params.axis.y - Y-position of the X-axis on the graph.\n * @param {function} params.transformLabel - Function to format the label for the ticks.\n * @param {Symbols['axis']} params.axisSymbols - Symbols used for axis rendering.\n * @param {number[]} params.expansionX - X-axis expansion range.\n * @param {number[]} params.expansionY - Y-axis expansion range.\n * @returns {Void} - Applies the drawing of Y-axis ticks.\n */\nexport const getDrawYAxisTicks =\n ({\n debugMode,\n showTickLabel,\n hideYAxisTicks,\n plotHeight,\n graph,\n plotWidth,\n yShift,\n axis,\n transformLabel,\n axisSymbols,\n expansionX,\n expansionY,\n }: {\n debugMode?: boolean;\n showTickLabel?: boolean;\n hideYAxisTicks?: boolean;\n plotHeight: number;\n graph: Graph;\n plotWidth: number;\n yShift: number;\n axis: { x: number; y: number };\n transformLabel: Formatter;\n axisSymbols: Symbols['axis'];\n expansionX: number[];\n expansionY: number[];\n }) =>\n (points: Point[]) => {\n const coords = getPlotCoords(points, plotWidth, plotHeight, expansionX, expansionY);\n points.forEach(([_, pointY], i) => {\n const [x, y] = coords[i];\n const [, scaledY] = toPlot(plotWidth, plotHeight)(x, y);\n drawYAxisEnd({\n debugMode,\n showTickLabel,\n hideYAxisTicks,\n plotHeight,\n graph,\n scaledY,\n yShift,\n axis,\n pointY,\n transformLabel,\n axisSymbols,\n expansionX,\n expansionY,\n });\n });\n };\n\n/**\n * Draws ticks on the X-axis based on axis configurations and scaling.\n * @param {Object} params - Configuration options for drawing the X-axis ticks.\n * @param {boolean} [params.debugMode=false] - If true, logs errors for out-of-bounds access.\n * @param {boolean} [params.hideXAxisTicks] - If true, hides the X-axis ticks.\n * @param {number} params.plotWidth - The width of the plot area.\n * @param {number} params.plotHeight - The height of the plot area.\n * @param {number} params.yShift - Shift applied to the Y-axis.\n * @param {Graph} params.graph - The graph matrix.\n * @param {MaybePoint} [params.axisCenter] - Optional axis center coordinates.\n * @param {string} params.emptySymbol - Symbol used to fill empty cells.\n * @param {Point[]} params.points - List of points for the X-axis.\n * @param {Symbols['axis']} params.axisSymbols - Symbols used for axis rendering.\n * @param {boolean} [params.hasToBeMoved] - If true, indicates that the graph needs to be moved.\n * @param {Object} params.axis - Object defining the axis position.\n * @param {number} params.axis.x - X-position of the Y-axis on the graph.\n * @param {number} params.axis.y - Y-position of the X-axis on the graph.\n * @param {function} params.transformLabel - Function to format the label for the ticks.\n * @param {number[]} params.expansionX - X-axis expansion range.\n * @param {number[]} params.expansionY - Y-axis expansion range.\n * @param {boolean} [params.hideXAxis] - If true, hides the X-axis.\n * @returns {Void} - Applies the drawing of X-axis ticks.\n */\nexport const getDrawXAxisTicks =\n ({\n debugMode,\n hideXAxisTicks,\n plotWidth,\n plotHeight,\n yShift,\n graph,\n axisCenter,\n emptySymbol,\n axisSymbols,\n axis,\n transformLabel,\n expansionX,\n expansionY,\n }: {\n debugMode?: boolean;\n hideXAxisTicks?: boolean;\n plotWidth: number;\n plotHeight: number;\n emptySymbol: string;\n yShift: number;\n graph: Graph;\n axisCenter?: MaybePoint;\n axis: { x: number; y: number };\n transformLabel: Formatter;\n axisSymbols: typeof AXIS;\n expansionX: number[];\n expansionY: number[];\n }) =>\n (points: Point[]) => {\n const coords = getPlotCoords(points, plotWidth, plotHeight, expansionX, expansionY);\n points.forEach(([pointX], i) => {\n const [x, y] = coords[i];\n const [scaledX] = toPlot(plotWidth, plotHeight)(x, y);\n const pointXShift = toArray(\n transformLabel(pointX, { axis: 'x', xRange: expansionX, yRange: expansionY }),\n );\n const yPos = axisCenter ? axis.y + 1 : graph.length - 1;\n\n const tickXPosition = scaledX + yShift + (axisCenter ? 1 : 2);\n\n const hasPlaceToRender = pointXShift.every((_, j) =>\n [emptySymbol, axisSymbols.ns].includes(graph[yPos - 1][scaledX + yShift - j + 2]),\n );\n\n for (let j = 0; j < pointXShift.length; j++) {\n const isOccupied = graph[yPos - 2][tickXPosition] === axisSymbols.x;\n\n if (isOccupied) break;\n\n drawXAxisEnd({\n debugMode,\n hasPlaceToRender,\n yPos,\n graph,\n axisCenter,\n yShift,\n i: j,\n scaledX,\n hideXAxisTicks,\n pointXShift,\n });