UNPKG

simple-ascii-chart

Version:
1 lines 112 kB
{"version":3,"sources":["../src/index.ts","../src/constants/index.ts","../src/services/coords.ts","../src/services/settings.ts","../src/services/draw.ts","../src/services/overrides.ts","../src/services/defaults.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\n/**\n * Layout and formatting constants\n */\nexport const LAYOUT = {\n MIN_PLOT_HEIGHT: 3,\n DEFAULT_DECIMAL_PLACES: 3,\n K_FORMAT_THRESHOLD: 1000,\n DEFAULT_PADDING: 2,\n DEFAULT_Y_SHIFT_OFFSET: 1,\n} as const;\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 const flatArray = toFlat(array);\n for (let i = 0; i < flatArray.length; i++) {\n const [x, y] = flatArray[i];\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';\nimport { LAYOUT } from '../constants';\nimport { Color, ColorGetter, Formatter, MultiLine } from '../types';\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) >= LAYOUT.K_FORMAT_THRESHOLD) {\n const rounded = value / LAYOUT.K_FORMAT_THRESHOLD;\n return rounded % 1 === 0 ? `${rounded}k` : `${rounded.toFixed(LAYOUT.DEFAULT_DECIMAL_PLACES)}k`;\n }\n return Number(value.toFixed(LAYOUT.DEFAULT_DECIMAL_PLACES));\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 for (let index = 0; index < graph.length; index++) {\n const line = graph[index];\n for (let curr = 0; curr < line.length; 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 });\n }\n\n drawXAxisTick({\n debugMode,\n axis,\n xPosition: tickXPosition,\n graph,\n hideXAxisTicks,\n axisSymbols,\n });\n });\n };\n\n/**\n * Draws ticks on the graph based on the provided configuration.\n * @param {Object} params - Configuration options for drawing ticks.\n * @param {MultiLine} params.input - Input data points.\n * @param {Graph} params.graph - The graph matrix.\n * @param {number} params.plotWidth - The width of the plot area.\n * @param {number} params.plotHeight - The height of the plot area.\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 {MaybePoint} [params.axisCenter] - Optional axis center coordinates.\n * @param {number} params.yShift - Shift applied to the Y-axis.\n * @param {string} params.emptySymbol - Symbol used to fill empty cells.\n * @param {boolean} [params.debugMode=false] - If true, logs errors for out-of-bounds access.\n * @param {boolean} [params.hideXAxis] - If true, hides the X-axis.\n * @param {boolean} [params.hideYAxis] - If true, hides the Y-axis.\n * @param {number[]} params.expansionX - X-axis expansion range.\n * @param {number[]} params.expansionY - Y-axis expansion range.\n * @param {number[]} [params.customYAxisTicks] - Custom Y-axis ticks.\n * @param {number[]} [params.customXAxisTicks] - Custom X-axis ticks.\n * @param {boolean} [params.hideYAxisTicks] - If true, hides Y-axis ticks.\n * @param {boolean} [params.hideXAxisTicks] - If true, hides X-axis ticks.\n * @param {boolean} [params.showTickLabel] - If true, displays tick labels for all points.\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 {boolean} [params.hasToBeMoved] - If true, indicates that the graph needs to be moved.\n */\nexport const drawTicks = ({\n input,\n graph,\n plotWidth,\n plotHeight,\n axis,\n axisCenter,\n yShift,\n emptySymbol,\n debugMode,\n hideXAxis,\n expansionX,\n expansionY,\n hideYAxis,\n customYAxisTicks,\n customXAxisTicks,\n hideYAxisTicks,\n hideXAxisTicks,\n showTickLabel,\n axisSymbols,\n transformLabel,\n}: {\n input: MultiLine;\n graph: Graph;\n plotWidth: number;\n plotHeight: number;\n axis: { x: number; y: number };\n axisCenter?: MaybePoint;\n yShift: number;\n emptySymbol: string;\n axisSymbols: typeof AXIS;\n hideYAxis?: boolean;\n hideXAxis?: boolean;\n debugMode?: boolean;\n expansionX: number[];\n expansionY: number[];\n customYAxisTicks?: number[];\n customXAxisTicks?: number[];\n hideYAxisTicks?: boolean;\n hideXAxisTicks?: boolean;\n showTickLabel?: boolean;\n transformLabel: Formatter;\n}) => {\n const [minY, maxY] = [Math.min(...expansionY), Math.max(...expansionY)];\n const [minX, maxX] = [Math.min(...expansionX), Math.max(...expansionX)];\n\n // draw ticks\n const drawYAxisTicks = getDrawYAxisTicks({\n axis,\n axisSymbols,\n debugMode,\n expansionX,\n expansionY,\n graph,\n hideYAxisTicks,\n plotHeight,\n plotWidth,\n showTickLabel,\n transformLabel,\n yShift,\n });\n\n const drawXAxisTicks = getDrawXAxisTicks({\n axis,\n axisCenter,\n axisSymbols,\n debugMode,\n emptySymbol,\n expansionX,\n expansionY,\n graph,\n hideXAxisTicks,\n plotHeight,\n plotWidth,\n transformLabel,\n yShift,\n });\n\n // Main ticks logic\n input.forEach((line) => {\n line.forEach(([pointX, pointY]) => {\n if (!hideYAxis && !customYAxisTicks) {\n drawYAxisTicks([[pointX, pointY]]);\n }\n\n if (!hideXAxis && !customXAxisTicks) {\n drawXAxisTicks([[pointX, pointY]]);\n }\n });\n });\n\n // if outside the bounds, do not draw\n const filteredCustomYAxisTicks = (customYAxisTicks || []).filter((y) => y >= minY && y <= maxY);\n const filteredCustomXAxisTicks = (customXAxisTicks || []).filter((x) => x >= minX && x <= maxX);\n\n if (filteredCustomYAxisTicks.length && !hideYAxis) {\n drawYAxisTicks(filteredCustomYAxisTicks.map((y) => toPoint(undefined, y)));\n }\n\n if (filteredCustomXAxisTicks.length && !hideXAxis) {\n drawXAxisTicks(filteredCustomXAxisTicks.map((x) => toPoint(x)));\n }\n};\n\nexport const drawAxisCenter = ({\n graph,\n realXShift,\n axis,\n debugMode,\n axisSymbols,\n emptySymbol,\n backgroundSymbol,\n}: {\n graph: Graph;\n axis: { x: number; y: number };\n realXShift: number;\n axisSymbols: typeof AXIS;\n debugMode?: boolean;\n emptySymbol: string;\n backgroundSymbol: string;\n}) => {\n const positionX = axis.x + realXShift;\n const positionY = axis.y;\n\n graph.forEach((line, indexY) => {\n line.forEach((_, indexX) => {\n if (indexX === positionX && indexY === positionY) {\n let symbol = axisSymbols.nse;\n\n const get = (x: number, y: number) => graph[y]?.[x];\n\n const isEmpty = (value: string) =>\n !Object.values(axisSymbols).includes(value) ||\n value === backgroundSymbol ||\n value === emptySymbol;\n\n const emptyOnLeft = isEmpty(get(indexX - 1, indexY));\n const emptyOnBottom = isEmpty(get(indexX, indexY + 1));\n const emptyOnRight = isEmpty(get(indexX + 1, indexY));\n const emptyOnTop = isEmpty(get(indexX, indexY - 1));\n\n if (emptyOnLeft && emptyOnRight && !emptyOnBottom && !emptyOnTop) {\n symbol = axisSymbols.ns;\n } else if (emptyOnLeft && !emptyOnTop && !emptyOnBottom && !emptyOnRight) {\n symbol = axisSymbols.intersectionY;\n } else if (!emptyOnBottom && !emptyOnLeft && !emptyOnTop && !emptyOnRight) {\n symbol = axisSymbols.intersectionXY;\n } else if (emptyOnLeft && emptyOnTop && !emptyOnRight && emptyOnBottom) {\n symbol = axisSymbols.we;\n } else if (emptyOnLeft && emptyOnBottom && emptyOnRight && !emptyOnTop) {\n symbol = axisSymbols.ns;\n }\n\n // empty on all sides, do not draw\n if (emptyOnBottom && emptyOnLeft && emptyOnRight && emptyOnTop) {\n return;\n }\n\n drawPosition({\n graph,\n scaledX: indexX,\n scaledY: indexY,\n symbol,\n debugMode,\n });\n }\n });\n });\n};\n","import { CHART, THRESHOLDS }