From 8cf4856d41aebdb8a7ee30466a70d5f79e755ec5 Mon Sep 17 00:00:00 2001 From: gtktsc Date: Sun, 27 Oct 2024 14:46:02 +0100 Subject: [PATCH] ascii-62: add bar charts --- README.md | 110 ++++- package.json | 3 +- src/examples.ts | 381 +++++++++++++++++ src/services/__tests__/defaults.test.ts | 4 + src/services/__tests__/draw.test.ts | 103 +++++ src/services/__tests__/overrides.test.ts | 18 +- src/services/__tests__/plot.test.ts | 176 +++++++- src/services/defaults.ts | 1 + src/services/draw.ts | 505 +++++++++++++++++------ src/services/overrides.ts | 106 ++++- src/services/plot.ts | 52 ++- src/types/index.ts | 8 + 12 files changed, 1285 insertions(+), 182 deletions(-) create mode 100644 src/examples.ts diff --git a/README.md b/README.md index 3380d1f..20716c4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ **Simple ASCII Chart** is a TypeScript package for creating customizable ASCII charts in the terminal. It supports two-dimensional data, multiple series, custom colors, and formatters, making it a versatile solution for terminal-based data visualization. -[Interactive demo.](https://simple-ascii-chart.vercel.app/) +[Playground and documentation](https://simple-ascii-chart.vercel.app/) +[NPM](https://www.npmjs.com/package/simple-ascii-chart) ## Installation @@ -25,9 +26,14 @@ import plot from 'simple-ascii-chart'; const graph = plot(input, settings); ``` +## CLI + +[CLI tool is available too](https://github.com/gtktsc/simple-ascii-chart-cli) +[NPM](https://www.npmjs.com/package/simple-ascii-chart-cli) + ## Playground -Create charts interactively in the [playground](https://simple-ascii-chart.vercel.app/). +Create charts interactively in the [playground](https://simple-ascii-chart.vercel.app/playground). ## API Endpoint @@ -108,10 +114,13 @@ Customize the `plot` function with a variety of settings: | `yLabel` | Label for the y-axis. | | `thresholds` | Defines threshold lines or points with optional colors at specific x or y coordinates. | | `fillArea` | Fills the area under each line, suitable for area charts. | +| `barChart` | Draws bar chart. | +| `horizontalBarChart`| Draws horizontal bar chart. | | `hideXAxis` | Hides the x-axis. | | `hideYAxis` | Hides the y-axis. | | `symbols` | Symbols for customizing the chart’s appearance, including axis, background, and chart symbols. | | `legend` | Configuration for a legend, showing series names and position options (`left`, `right`, `top`, `bottom`). | +| `debugMode` | Enables debug mode (`default = false`). | ### Advanced Settings @@ -864,3 +873,100 @@ Expected Output: └┬───┬───┬───┬────┬───┬───┬───┬▶ 1 2 3 4 5 6 7 8 ``` + +### Bar chart + +Input: + +```typescript +plot( + [ + [0, 3], + [1, 2], + [2, 3], + [3, 4], + [4, -2], + [5, -5], + [6, 2], + [7, 0], +], +{ + title: 'bar chart with axis', + barChart: true, + showTickLabel: true, + width: 40, + axisCenter: [0, 0], +}, +); +``` + +Expected Output: + +```bash +bar chart with axis + ▲ █ + 4┤ █ █ + 3┤ █ █ █ █ + 2┤ █ █ █ █ + 1┤ █ █ █ █ █ + 0┤─────┬────┬─────┬────┬─────┬────┬─────┬─▶ +-1┤ 1 2 3 4 5 6 7 +-2┤ █ +-3┤ █ +-4┤ █ +-5┤ + │ +``` + +### Bar chart + +Input: + +```typescript +plot( + [ + [0, 3], + [1, 2], + [2, 3], + [3, 4], + [4, -2], + [5, -5], + [6, 2], + [7, 0], + ], + { + horizontalBarChart: true, + showTickLabel: true, + width: 40, + height: 20, + axisCenter: [3, 1], + }, +); +``` + +Expected Output: + +```bash + ▲ + 4┤ + │ +████████████████3┤ + │ + ██████████2┤████████████████ + │ +┬─────┬────┬────1┤────┬─────┬────┬─────┬─▶ +0 1 2 3 4 5 6 7 + 0┤██████████████████████ + │ + -1┤ + │ + -2┤ + │█████ + -3┤ + │ + -4┤ + │ + │ + -5┤███████████ + │ +``` \ No newline at end of file diff --git a/package.json b/package.json index f16c447..6b7b05e 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "simple-ascii-chart", - "version": "4.1.1", + "version": "4.2.0", "description": "Simple ascii chart generator", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "start": "tsc-watch -p tsconfig.json --preserveWatchOutput -w --onSuccess 'node ./dist/index.js'", + "examples": "tsc-watch -p tsconfig.json --preserveWatchOutput -w --onSuccess 'node ./dist/examples.js'", "lint": "eslint .", "lint:fix": "eslint . --fix", "test": "jest --coverage", diff --git a/src/examples.ts b/src/examples.ts new file mode 100644 index 0000000..d41ebe7 --- /dev/null +++ b/src/examples.ts @@ -0,0 +1,381 @@ +import { plot } from './services/plot'; +import { Coordinates, Settings } from './types'; + +const examples: Array<[Coordinates, Settings & { only?: boolean }]> = [ + [ + [ + [1, 2], + [2, 3], + ], + { title: 'simple example' }, + ], + [ + [ + [1, 2], + [2, 3], + [3, 4], + [4, 1], + ], + { title: 'bar chart', width: 10, barChart: true, height: 10 }, + ], + [ + [ + [-1, 2], + [2, 3], + [3, 4], + [4, 1], + ], + { title: 'horizontal bar chart', width: 20, horizontalBarChart: true, height: 10 }, + ], + [ + [ + [1, 2], + [2, 3], + [3, 4], + [4, 1], + ], + { title: 'area', width: 20, fillArea: true, height: 10 }, + ], + [ + [ + [1, 2], + [2, 3], + [3, 4], + [4, 1], + ], + { title: 'labels', width: 20, xLabel: 'x', yLabel: 'y', height: 10 }, + ], + [ + [ + [ + [1, 2], + [2, 3], + [3, 4], + [4, 1], + ], + [ + [1, -2], + [2, -3], + [3, 3], + [4, 0], + ], + ], + + { + title: 'legend', + width: 20, + legend: { position: 'bottom', series: ['first', 'second'] }, + xLabel: 'x', + yLabel: 'y', + height: 10, + }, + ], + [ + [ + [ + [1, 2], + [2, 3], + [3, 4], + [4, 1], + ], + [ + [1, -2], + [2, -3], + [3, 3], + [4, 0], + ], + [ + [1, -6], + [2, -3], + [3, 3], + [4, 0], + ], + [ + [1, -2], + [2, -3], + [3, 3], + [4, 0], + [5, 3], + ], + ], + + { + title: 'multiline', + width: 20, + + height: 10, + }, + ], + [ + [ + [1, 2], + [2, 3], + [3, 4], + [4, 8], + ], + { title: 'yRange', width: 20, height: 10, yRange: [1, 3] }, + ], + [ + [ + [1, 2], + [2, 3], + [3, 4], + [4, 1], + ], + { title: 'yRange', width: 20, height: 10, yRange: [0, 5] }, + ], + [ + [ + [1, 2], + [2, 3], + [3, 4], + [4, 1], + [5, 5], + [6, 10], + ], + { title: 'showTickLabel', width: 20, height: 10, showTickLabel: true }, + ], + [ + [ + [1, 2], + [2, 3], + [5, 5], + [6, 10], + ], + { title: 'hideXAxis', width: 20, height: 10, hideXAxis: true }, + ], + [ + [ + [1, 2], + [2, 3], + [5, 5], + [6, 10], + ], + { title: 'hideYAxis', width: 20, height: 10, hideYAxis: true }, + ], + [ + [ + [-1, 2], + [1, 2], + [2, 3], + [5, 5], + [6, -2], + ], + { title: 'axisCenter', width: 20, height: 10, axisCenter: [0, 0], showTickLabel: true }, + ], + [ + [ + [-1, 2], + [1, 2], + [2, 3], + [5, 5], + [6, -2], + ], + { + title: 'lineFormatter', + width: 20, + height: 10, + lineFormatter: (props) => { + const output = [{ x: props.plotX, y: props.plotY, symbol: '█' }]; + const [minX] = props.toPlotCoordinates(props.minX, props.minY); + let i = minX; + + while (i <= props.plotX) { + output.push({ x: i, y: props.plotY, symbol: '█' }); + i += 1; + } + + return output; + }, + }, + ], + [ + [ + [-1, 2], + [1, 2], + [2, 3], + [5, 5], + [6, -2], + ], + { + title: 'lineFormatter', + width: 20, + height: 10, + lineFormatter: (props) => { + const output = [{ x: props.plotX, y: props.plotY, symbol: '█' }]; + const [_, maxY] = props.toPlotCoordinates(props.x, props.minY); + let i = props.plotY; + + while (i <= maxY + 1) { + output.push({ x: props.plotX, y: i, symbol: '█' }); + i += 1; + } + + return output; + }, + }, + ], + [ + [ + [-1, 2], + [1, 2], + [2, 3], + [5, 5], + [6, -2], + ], + { + title: 'symbols', + width: 20, + height: 10, + symbols: { + background: '█', + border: 'A', + empty: 'B', + }, + }, + ], + [ + [ + [1, 2], + [2, 3], + [3, 4], + [4, 1], + ], + { title: 'colors', width: 20, color: 'ansiGreen', height: 10 }, + ], + [ + [ + [ + [1, 2], + [2, 3], + [3, 4], + [4, 1], + ], + [ + [1, 3], + [2, 1], + [3, 0], + [4, 4], + ], + ], + { + title: 'colors with legend', + width: 20, + thresholds: [{ x: 2, y: 2, color: 'ansiBlue' }], + color: ['ansiGreen', 'ansiMagenta'], + height: 10, + legend: { position: 'bottom', series: ['first', 'second'] }, + }, + ], + [ + [ + [1, 1], + [2, 4], + [3, 4], + [4, 2], + [5, -1], + [6, 3], + [7, -1], + [8, 9], + ], + { + width: 40, + title: 'thresholds', + thresholds: [ + { + y: 5, + x: 5, + color: 'ansiBlue', + }, + { + y: 2, + color: 'ansiGreen', + }, + ], + }, + ], + [ + [ + [0, 3], + [1, 2], + [2, 3], + [3, 4], + [4, -2], + [5, -5], + [6, 2], + [7, 0], + ], + { + title: 'with axis center', + color: 'ansiGreen', + showTickLabel: true, + width: 40, + axisCenter: [0, 2], + }, + ], + [ + [ + [0, 3], + [1, 2], + [2, 3], + [3, 4], + [4, -2], + [5, -5], + [6, 2], + [7, 0], + ], + { + title: 'bar chart with colors', + color: 'ansiGreen', + barChart: true, + showTickLabel: true, + width: 40, + axisCenter: [0, 0], + }, + ], + [ + [ + [0, 3], + [1, 2], + [2, 3], + [3, 4], + [4, -2], + [5, -5], + [6, 2], + [7, 0], + ], + { + title: 'horizontal bar chart with axis center', + horizontalBarChart: true, + showTickLabel: true, + width: 40, + height: 20, + axisCenter: [3, 1], + }, + ], + [ + [ + [1, 0], + [2, 20], + [3, 29], + ], + { height: 10, horizontalBarChart: true, width: 20, showTickLabel: true }, + ], + [ + [ + [1, 0], + [2, 20], + [3, 29], + ], + { height: 10, barChart: true, width: 20, showTickLabel: true }, + ], +]; + +const hasFilter = examples.some(([, { only }]) => only !== undefined); + +console.clear(); +examples + .filter((example) => (hasFilter ? example[1].only : true)) + .forEach(([data, options]) => { + console.log(plot(data, options)); + }); diff --git a/src/services/__tests__/defaults.test.ts b/src/services/__tests__/defaults.test.ts index e37b1e4..75348bf 100644 --- a/src/services/__tests__/defaults.test.ts +++ b/src/services/__tests__/defaults.test.ts @@ -43,6 +43,7 @@ describe('Chart Helper Functions', () => { const size = getChartSize({ input }); expect(size).toEqual({ minX: 1, + minY: 2, plotWidth: 3, // length of rangeX plotHeight: 5, // maxY - minY + 1 expansionX: [1, 3], @@ -62,6 +63,7 @@ describe('Chart Helper Functions', () => { const size = getChartSize({ input, width: 10, height: 10 }); expect(size).toEqual({ minX: 1, + minY: 2, plotWidth: 10, plotHeight: 10, expansionX: [1, 3], @@ -79,6 +81,7 @@ describe('Chart Helper Functions', () => { const size = getChartSize({ input }); expect(size).toEqual({ minX: 1, + minY: 2, plotWidth: 2, // length of rangeX plotHeight: 3, // length of rangeY since it's less than 3 without provided height expansionX: [1, 2], @@ -98,6 +101,7 @@ describe('Chart Helper Functions', () => { const size = getChartSize({ input }); expect(size).toEqual({ minX: -3, + minY: -2, plotWidth: 4, // length of rangeX plotHeight: 7, // maxY - minY + 1 expansionX: [-3, 3], diff --git a/src/services/__tests__/draw.test.ts b/src/services/__tests__/draw.test.ts index 1da24b9..914df2c 100644 --- a/src/services/__tests__/draw.test.ts +++ b/src/services/__tests__/draw.test.ts @@ -7,11 +7,76 @@ import { drawCustomLine, drawLine, drawShift, + drawPosition, } from '../draw'; import { AXIS, CHART } from '../../constants'; import { MultiLine, Point } from '../../types'; describe('Drawing functions', () => { + describe('drawPosition', () => { + it('should correctly draw a symbol at the specified position in the graph', () => { + const graph = [ + [' ', ' ', ' '], + [' ', ' ', ' '], + [' ', ' ', ' '], + ]; + drawPosition({ graph, scaledX: 1, scaledY: 1, symbol: 'X' }); + expect(graph[1][1]).toEqual('X'); + }); + + it('should handle out-of-bounds Y position in debug mode', () => { + const graph = [ + [' ', ' ', ' '], + [' ', ' ', ' '], + [' ', ' ', ' '], + ]; + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + drawPosition({ graph, scaledX: 1, scaledY: 3, symbol: 'X', debugMode: true }); + expect(consoleSpy).toHaveBeenCalledWith( + 'Drawing at [1, 3]', + 'Error: out of bounds Y', + expect.objectContaining({ + graph, + scaledX: 1, + scaledY: 3, + }), + ); + consoleSpy.mockRestore(); + }); + + it('should handle out-of-bounds X position in debug mode', () => { + const graph = [ + [' ', ' ', ' '], + [' ', ' ', ' '], + [' ', ' ', ' '], + ]; + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + drawPosition({ graph, scaledX: 4, scaledY: 1, symbol: 'X', debugMode: true }); + expect(consoleSpy).toHaveBeenCalledWith( + 'Drawing at [4, 1]', + 'Error: out of bounds X', + expect.objectContaining({ + graph, + scaledX: 4, + scaledY: 1, + }), + ); + consoleSpy.mockRestore(); + }); + + it('should not log any errors if debugMode is off and out-of-bounds error occurs', () => { + const graph = [ + [' ', ' ', ' '], + [' ', ' ', ' '], + [' ', ' ', ' '], + ]; + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + drawPosition({ graph, scaledX: 4, scaledY: 1, symbol: 'X' }); + expect(consoleSpy).not.toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + }); + describe('drawXAxisEnd', () => { it('should draw the X-axis end correctly', () => { const graph = [ @@ -63,6 +128,35 @@ describe('Drawing functions', () => { }); describe('drawYAxisEnd', () => { + it('should draw tick labels for each step when showTickLabel is true', () => { + const graph = [ + [' ', ' ', ' ', ' ', ' '], + [' ', ' ', ' ', ' ', ' '], + [' ', ' ', ' ', ' ', ' '], + [' ', ' ', ' ', ' ', ' '], + ]; + const args = { + graph, + scaledY: 1, + yShift: 1, + axis: { x: 1, y: 1 }, + pointY: 2, + plotHeight: 4, + transformLabel: (value: number) => value.toString(), + axisSymbols: { y: 'Y' }, + expansionX: [0], + expansionY: [0, 1, 2, 3], + showTickLabel: true, + }; + drawYAxisEnd(args); + + // Expect Y-axis labels to be drawn starting from [1][2], not [0][2] + expect(graph[1][2]).toEqual('3'); // Top of the axis (Y value 3) + expect(graph[2][2]).toEqual('2'); // Mid Y value (Y value 2) + expect(graph[3][2]).toEqual('1'); // Near bottom (Y value 1) + // The bottom Y value '0' might not be drawn, depending on the graph size + }); + it('should draw the Y-axis end correctly', () => { const graph = [ [' ', ' ', ' ', ' '], @@ -149,7 +243,12 @@ describe('Drawing functions', () => { scaledY: 1, input: [[1, 1]] as unknown as MultiLine, index: 0, + minY: 0, + minX: 0, + expansionX: [0], + expansionY: [0], lineFormatter: () => ({ x: 1, y: 1, symbol: 'X' }), + toPlotCoordinates: () => [1, 1] as Point, graph, }; drawCustomLine(args); @@ -172,7 +271,11 @@ describe('Drawing functions', () => { [1, 1], ] as Point[], graph, + horizontalBarChart: false, + barChart: false, scaledX: 1, + axis: { x: 0, y: 5 }, + axisCenter: undefined, scaledY: 1, plotHeight: 3, emptySymbol: ' ', diff --git a/src/services/__tests__/overrides.test.ts b/src/services/__tests__/overrides.test.ts index 9cb159b..a62fe7a 100644 --- a/src/services/__tests__/overrides.test.ts +++ b/src/services/__tests__/overrides.test.ts @@ -90,15 +90,24 @@ describe('Graph Utility Functions', () => { describe('addThresholds', () => { it('should add thresholds correctly', () => { - const thresholds = [{ x: 1, y: 2, color: 'ansiRed' }] as Threshold[]; + const thresholds = [{ x: 2, color: 'ansiRed' }] as Threshold[]; const axis = { x: 0, y: 0 }; const plotHeight = 10; const expansionX = [0, plotWidth]; const expansionY = [0, plotHeight]; addThresholds({ graph, thresholds, axis, plotWidth, plotHeight, expansionX, expansionY }); + expect(graph[2][3]).toContain(CHART.ns); + }); + it('should add thresholds correctly - y', () => { + const thresholds = [{ y: 5, color: 'ansiRed' }] as Threshold[]; + const axis = { x: 0, y: 0 }; + const plotHeight = 10; + const expansionX = [0, plotWidth]; + const expansionY = [0, plotHeight]; - expect(graph[2][1]).toContain(CHART.we); + addThresholds({ graph, thresholds, axis, plotWidth, plotHeight, expansionX, expansionY }); + expect(graph[2][3]).toContain(CHART.we); }); it('should add color', () => { const thresholds = [{ x: 1, y: 2, color: 'ansiBlue' }] as Threshold[]; @@ -123,9 +132,8 @@ describe('Graph Utility Functions', () => { const expansionY = [0, plotHeight]; addThresholds({ graph, thresholds, axis, plotWidth, plotHeight, expansionX, expansionY }); - - expect(graph[0][2]).toContain('\u001b[34m'); - expect(graph[0][3]).toContain('\u001b[31m'); + expect(graph[0][3]).toContain('\u001b[34m'); + expect(graph[0][4]).toContain('\u001b[31m'); }); it('should not add color if not set', () => { diff --git a/src/services/__tests__/plot.test.ts b/src/services/__tests__/plot.test.ts index 97c5625..8d79662 100644 --- a/src/services/__tests__/plot.test.ts +++ b/src/services/__tests__/plot.test.ts @@ -409,7 +409,122 @@ s-30┤━━━━━━━━━━━━━━━━━━┛ ███████████████████████████ `, ], - + [ + 'show tick labels', + [ + [1, 0], + [2, 20], + [3, 29], + ], + { height: 10, width: 20, showTickLabel: true }, + ` + ▲ +26┤ ┏━ +23┤ ┃ +20┤ ┃ +17┤ ┏━━━━━━━━┛ +15┤ ┃ +12┤ ┃ + 9┤ ┃ + 6┤ ┃ + 3┤ ┃ + 0┤━━━━━━━━━┛ + └┬─────────┬────────┬▶ + 1 2 3 +`, + ], + [ + 'bar chart with ticks', + [ + [1, 0], + [2, 20], + [3, 29], + ], + { height: 10, barChart: true, width: 20, showTickLabel: true }, + ` + ▲ █ +26┤ █ +23┤ █ +20┤ █ █ +17┤ █ █ +15┤ █ █ +12┤ █ █ + 9┤ █ █ + 6┤ █ █ + 3┤█ █ █ + 0┤█ █ █ + └┬─────────┬────────┬▶ + 1 2 3 +`, + ], + [ + 'horizontal bar chart with ticks', + [ + [1, 0], + [2, 20], + [3, 29], + ], + { height: 10, horizontalBarChart: true, width: 20, showTickLabel: true }, + ` + ▲ +26┤███████████████████ +23┤ +20┤ +17┤██████████ +15┤ +12┤ + 9┤ + 6┤ + 3┤ + 0┤ + └┬─────────┬────────┬▶ + 1 2 3 +`, + ], + [ + 'horizontal bar chart with axis center', + [ + [0, 3], + [1, 2], + [2, 3], + [3, 4], + [4, -2], + [5, -5], + [6, 2], + [7, 0], + ], + { + horizontalBarChart: true, + showTickLabel: true, + width: 40, + height: 20, + axisCenter: [3, 1], + }, + ` + ▲ + 4┤ + │ +████████████████3┤ + │ + ██████████2┤████████████████ + │ +┬─────┬────┬────1┤────┬─────┬────┬─────┬─▶ +0 1 2 3 4 5 6 7 + 0┤██████████████████████ + │ + -1┤ + │ + -2┤ + │█████ + -3┤ + │ + -4┤ + │ + │ + -5┤███████████ + │ +`, + ], [ 'adds single legend at top and overrides symbols', [ @@ -437,6 +552,41 @@ s-30┤━━━━━━━━━━━━━━━━━━┛ 0┤ ┗━ └┬────────┬▶ 1 3 +`, + ], + [ + 'bar chart with axis', + [ + [0, 3], + [1, 2], + [2, 3], + [3, 4], + [4, -2], + [5, -5], + [6, 2], + [7, 0], + ], + { + title: 'bar chart with axis', + barChart: true, + showTickLabel: true, + width: 40, + axisCenter: [0, 0], + }, + ` +bar chart with axis + ▲ █ + 4┤ █ █ + 3┤ █ █ █ █ + 2┤ █ █ █ █ + 1┤ █ █ █ █ █ + 0┤─────┬────┬─────┬────┬─────┬────┬─────┬─▶ +-1┤ 1 2 3 4 5 6 7 +-2┤ █ +-3┤ █ +-4┤ █ +-5┤ + │ `, ], [ @@ -522,18 +672,18 @@ s-30┤━━━━━━━━━━━━━━━━━━┛ ], }, ` - ▲ ┃ ┃ - 9┤ ┃ ┃ ┏━ - │ ┃ ┃ ┃ - │ ┃ ┃ ┃ - │━━━━━┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - │ ┃ ┃ ┃ - 4┤ ┃━━━━━━━━━━┓ ┃ ┃ - 3┤ ┃ ┃ ┃ ┏━━━━┓ ┃ - 2┤ ┃ ┗━━━━┃ ┃ ┃ ┃ - 1┤━━━━━┃ ┃ ┃ ┃ ┃ - │ ┃ ┃ ┃ ┃ ┃ --1┤ ┃ ┃━━━━━┛ ┗━━━━━┛ + ▲ ┃ ┃ + 9┤ ┃ ┃ ┏━ + │ ┃ ┃ ┃ + │ ┃ ┃ ┃ + │ ┃ ┃ ┃ + │━━━━━━┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 4┤ ┏┃━━━━━━━━━┓ ┃ ┃ + 3┤ ┃┃ ┃ ┃ ┏━━━━┓ ┃ + 2┤ ┃┃ ┗━━━━┓┃ ┃ ┃ ┃ + 1┤━━━━━┛┃ ┃┃ ┃ ┃ ┃ + │ ┃ ┃┃ ┃ ┃ ┃ +-1┤ ┃ ┗┃━━━━┛ ┗━━━━━┛ └┬─────┬────┬─────┬────┬─────┬────┬─────┬▶ 1 2 3 4 5 6 7 8 `, diff --git a/src/services/defaults.ts b/src/services/defaults.ts index dcd6fd4..f5cd038 100644 --- a/src/services/defaults.ts +++ b/src/services/defaults.ts @@ -64,6 +64,7 @@ export const getChartSize = ({ return { minX, + minY, plotWidth, plotHeight, expansionX, diff --git a/src/services/draw.ts b/src/services/draw.ts index 769ad38..e6868d0 100644 --- a/src/services/draw.ts +++ b/src/services/draw.ts @@ -1,21 +1,80 @@ import { AXIS, CHART } from '../constants'; -import { CustomSymbol, Formatter, Graph, MaybePoint, MultiLine, Point, Symbols } from '../types'; +import { + CustomSymbol, + Formatter, + Graph, + LineFormatterArgs, + MaybePoint, + MultiLine, + Point, + Symbols, +} from '../types'; import { distance, toArray, toEmpty } from './coords'; /** - * Draws an X-axis tick mark at the end of each label. - * @param {object} options - Configuration options. - * @param {boolean} options.hasPlaceToRender - Indicates if there is space available for rendering. - * @param {Point | [number | undefined, number | undefined]} [options.axisCenter] - Optional axis center coordinates. - * @param {number} options.yPos - Y position for the axis tick. - * @param {Graph} options.graph - The graph array where ticks will be rendered. - * @param {number} options.yShift - Y-axis shift offset. - * @param {number} options.i - Current index for iteration. - * @param {number} options.scaledX - Scaled X position for the tick. - * @param {number} options.shift - X-axis shift offset. - * @param {number} options.signShift - Sign shift adjustment. - * @param {Symbols['axis']} options.axisSymbols - Symbols used for the axis. - * @param {string[]} options.pointXShift - Shifted X-axis points for tick display. + * Places a symbol at a specific position on the graph. + * @param {Object} params - Object containing parameters. + * @param {Graph} params.graph - The graph matrix where the symbol will be drawn. + * @param {number} params.scaledX - X-coordinate on the graph (scaled). + * @param {number} params.scaledY - Y-coordinate on the graph (scaled). + * @param {string} params.symbol - Symbol to draw on the graph. + */ +export const drawPosition = ({ + graph, + scaledX, + scaledY, + symbol, + debugMode = false, +}: { + graph: Graph; + scaledX: number; + scaledY: number; + symbol: string; + debugMode?: boolean; +}) => { + if (debugMode) { + // Handle out-of-bounds for Y + if (scaledY >= graph.length || scaledY < 0) { + console.log(`Drawing at [${scaledX}, ${scaledY}]`, 'Error: out of bounds Y', { + graph, + scaledX, + scaledY, + }); + return; + } + // Handle out-of-bounds for X + if (scaledX >= graph[scaledY].length || scaledX < 0) { + console.log(`Drawing at [${scaledX}, ${scaledY}]`, 'Error: out of bounds X', { + graph, + scaledX, + scaledY, + }); + return; + } + } + + // Draw the symbol if within bounds + try { + graph[scaledY][scaledX] = symbol; + } catch (error) { + // Fail silently without logging if debugMode is false + } +}; + +/** + * Draws a tick mark at the end of the X-axis, handling bounds and axis center. + * @param {Object} params - Configuration options for drawing the X-axis tick. + * @param {boolean} params.hasPlaceToRender - True if there is enough space to render the tick. + * @param {Point | [number | undefined, number | undefined]} [params.axisCenter] - Coordinates of the axis center (optional). + * @param {number} params.yPos - The Y-position of the tick mark. + * @param {Graph} params.graph - The graph matrix being drawn on. + * @param {number} params.yShift - The Y-axis shift offset. + * @param {number} params.i - The current iteration index. + * @param {number} params.scaledX - The scaled X-position for rendering the tick. + * @param {number} params.shift - X-axis offset to adjust tick positioning. + * @param {number} params.signShift - Additional shift based on the sign of the axis. + * @param {Symbols['axis']} params.axisSymbols - Symbols used for the axis rendering. + * @param {string[]} params.pointXShift - Array of characters representing the X-axis labels. */ export const drawXAxisEnd = ({ hasPlaceToRender, @@ -29,6 +88,7 @@ export const drawXAxisEnd = ({ signShift, axisSymbols, pointXShift, + debugMode, }: { hasPlaceToRender: boolean; axisCenter?: Point | [number | undefined, number | undefined]; @@ -41,56 +101,62 @@ export const drawXAxisEnd = ({ signShift: number; axisSymbols: Symbols['axis']; pointXShift: string[]; + debugMode?: boolean; }) => { + // Adjusts Y position based on render space and axis center presence const yShiftWhenOccupied = hasPlaceToRender ? -1 : 0; const yShiftWhenHasAxisCenter = axisCenter && axisCenter[1] !== undefined ? 1 : 0; - let graphY = yPos + yShiftWhenOccupied + yShiftWhenHasAxisCenter; - // Boundary check - if (graphY < 0) { - graphY = 0; - } else if (graphY >= graph.length) { - graphY = graph.length - 1; - } + // Ensure graphY stays within valid bounds + if (graphY < 0) graphY = 0; + else if (graphY >= graph.length) graphY = graph.length - 1; + // Adjust X position for rendering the tick let graphX = scaledX + yShift - i + 2 + shift; + if (graphX < 0) graphX = 0; + else if (graphX >= graph[graphY].length) graphX = graph[graphY].length - 1; - // Ensure graphX is within bounds - if (graphX < 0) { - graphX = 0; - } else if (graphX >= graph[graphY].length) { - graphX = graph[graphY].length - 1; - } - - graph[graphY][graphX] = pointXShift[pointXShift.length - 1 - i]; + // Draw the tick label + drawPosition({ + debugMode, + graph, + scaledX: graphX, + scaledY: graphY, + symbol: pointXShift[pointXShift.length - 1 - i], + }); - // Add tick mark only for the last value in the X-axis + // If it's the last tick, draw the tick mark if (pointXShift.length - 1 === i) { const xTickY = yPos + signShift; const xTickX = scaledX + yShift + 2 + shift; - if (xTickY >= 0 && xTickY < graph.length && xTickX >= 0 && xTickX < graph[xTickY].length) { - graph[xTickY][xTickX] = axisSymbols?.x || AXIS.x; + drawPosition({ + debugMode, + graph, + scaledX: xTickX, + scaledY: xTickY, + symbol: axisSymbols?.x || AXIS.x, + }); } } }; /** - * Draws Y-axis tick marks based on scale and axis configurations. - * @param {object} options - Configuration options. - * @param {Graph} options.graph - The graph array to modify. - * @param {number} options.scaledY - Scaled Y position for the tick. - * @param {number} options.yShift - Y-axis shift offset. - * @param {object} options.axis - The axis position. - * @param {MaybePoint} [options.axisCenter] - Optional axis center coordinates. - * @param {number} options.pointY - Y-coordinate for the current point. - * @param {Formatter} options.transformLabel - Label transformation function. - * @param {Symbols['axis']} options.axisSymbols - Symbols used for the axis. - * @param {number[]} options.expansionX - X-axis expansion range. - * @param {number[]} options.expansionY - Y-axis expansion range. - * @param {number} options.plotHeight - Height of the plot area. - * @param {boolean} [options.showTickLabel] - Whether to display all tick labels. + * Draws tick marks for the Y-axis based on axis configurations and scales. + * @param {Object} params - Configuration options for drawing the Y-axis ticks. + * @param {Graph} params.graph - The graph matrix. + * @param {number} params.scaledY - Scaled Y-coordinate. + * @param {number} params.yShift - Shift applied to the Y-axis. + * @param {Object} params.axis - Object defining the axis position. + * @param {MaybePoint} [params.axisCenter] - Optional axis center coordinates. + * @param {number} params.pointY - The actual Y-coordinate of the point. + * @param {Formatter} params.transformLabel - Function to format the label for the Y-axis. + * @param {Symbols['axis']} params.axisSymbols - Symbols used for drawing the axis. + * @param {number[]} params.expansionX - Array of X-axis expansion factors. + * @param {number[]} params.expansionY - Array of Y-axis expansion factors. + * @param {number} params.plotHeight - The height of the plot. + * @param {boolean} [params.showTickLabel] - If true, displays tick labels for all points. */ export const drawYAxisEnd = ({ graph, @@ -105,6 +171,7 @@ export const drawYAxisEnd = ({ expansionY, plotHeight, showTickLabel, + debugMode, }: { graph: Graph; scaledY: number; @@ -118,64 +185,85 @@ export const drawYAxisEnd = ({ expansionX: number[]; expansionY: number[]; showTickLabel?: boolean; + debugMode?: boolean; }) => { - // Render all tick labels if showTickLabel is true if (showTickLabel) { const yMax = Math.max(...expansionY); const yMin = Math.min(...expansionY); const numTicks = plotHeight; const yStep = (yMax - yMin) / numTicks; - for (let i = 0; i <= numTicks; i += 1) { - const yValue = yMax - i * yStep; + // Draw ticks for each label + for (let i = 0; i <= numTicks; i++) { + const yValue = Math.round(yMax - i * yStep); // Ensure whole numbers const scaledYPos = ((yMax - yValue) / (yMax - yMin)) * (plotHeight - 1); - const labelShift = axisCenter?.[1] !== undefined && axisCenter?.[1] > 0 ? 1 : 0; - const graphYPos = Math.floor(scaledYPos) + 1 + labelShift; + const graphYPos = Math.floor(scaledYPos) + 1; + // Ensure the Y position is within bounds if (graphYPos >= 0 && graphYPos < graph.length) { - if (graph[graphYPos][axis.x + yShift + 1] !== axisSymbols?.y) { - const pointYShift = toArray( - transformLabel(yValue, { axis: 'y', xRange: expansionX, yRange: expansionY }), - ); - - for (let j = 0; j < pointYShift.length; j += 1) { - const colIndex = axis.x + yShift - j; - if (colIndex >= 0 && colIndex < graph[graphYPos].length) { - graph[graphYPos][colIndex] = pointYShift[pointYShift.length - 1 - j]; - } - } - - const tickMarkIndex = axis.x + yShift + 1; - if (tickMarkIndex >= 0 && tickMarkIndex < graph[graphYPos].length) { - graph[graphYPos][tickMarkIndex] = axisSymbols?.y || AXIS.y; + const pointYShift = toArray( + transformLabel(yValue, { axis: 'y', xRange: expansionX, yRange: expansionY }), + ); + for (let j = 0; j < pointYShift.length; j++) { + const colIndex = axis.x + yShift - j; + if (colIndex >= 0 && colIndex < graph[graphYPos].length) { + drawPosition({ + debugMode, + graph, + scaledX: colIndex, + scaledY: graphYPos, + symbol: pointYShift[pointYShift.length - 1 - j], + }); } } + const tickMarkIndex = axis.x + yShift + 1; + if (tickMarkIndex >= 0 && tickMarkIndex < graph[graphYPos].length) { + drawPosition({ + debugMode, + graph, + scaledX: tickMarkIndex, + scaledY: graphYPos, + symbol: axisSymbols?.y || AXIS.y, + }); + } } } return; } - // Render specific values present in the data + // Render ticks for specific data values if (graph[scaledY + 1][axis.x + yShift + 1] !== axisSymbols?.y) { const pointYShift = toArray( transformLabel(pointY, { axis: 'y', xRange: expansionX, yRange: expansionY }), ); - for (let i = 0; i < pointYShift.length; i += 1) { - graph[scaledY + 1][axis.x + yShift - i] = pointYShift[pointYShift.length - 1 - i]; + for (let i = 0; i < pointYShift.length; i++) { + drawPosition({ + debugMode, + graph, + scaledX: axis.x + yShift - i, + scaledY: scaledY + 1, + symbol: pointYShift[pointYShift.length - 1 - i], + }); } - graph[scaledY + 1][axis.x + yShift + 1] = axisSymbols?.y || AXIS.y; + drawPosition({ + debugMode, + graph, + scaledX: axis.x + yShift + 1, + scaledY: scaledY + 1, + symbol: axisSymbols?.y || AXIS.y, + }); } }; /** * Draws both X and Y axes on the graph according to visibility and center configurations. - * @param {object} options - Configuration options. - * @param {Graph} options.graph - The graph array where axes will be drawn. - * @param {boolean} [options.hideXAxis] - If true, hides the X-axis. - * @param {boolean} [options.hideYAxis] - If true, hides the Y-axis. - * @param {MaybePoint} [options.axisCenter] - Optional axis center coordinates. - * @param {Symbols['axis']} options.axisSymbols - Symbols used for the axis. - * @param {object} options.axis - The axis position. + * @param {Object} params - Configuration options for drawing axes. + * @param {Graph} params.graph - The graph matrix. + * @param {boolean} [params.hideXAxis] - If true, hides the X-axis. + * @param {boolean} [params.hideYAxis] - If true, hides the Y-axis. + * @param {MaybePoint} [params.axisCenter] - Optional axis center coordinates. + * @param {Symbols['axis']} params.axisSymbols - Symbols used for axis rendering. + * @param {Object} params.axis - Object defining the axis position (x and y coordinates). */ export const drawAxis = ({ graph, @@ -184,6 +272,7 @@ export const drawAxis = ({ axisCenter, axisSymbols, axis, + debugMode, }: { graph: Graph; axis: { x: number; y: number }; @@ -191,6 +280,7 @@ export const drawAxis = ({ axisCenter?: MaybePoint; hideYAxis?: boolean; axisSymbols: Symbols['axis']; + debugMode?: boolean; }) => { graph.forEach((line, index) => { line.forEach((_, curr) => { @@ -213,7 +303,7 @@ export const drawAxis = ({ } if (lineChar) { - line[curr] = lineChar; + drawPosition({ debugMode, graph, scaledX: curr, scaledY: index, symbol: lineChar }); } }); }); @@ -221,11 +311,11 @@ export const drawAxis = ({ /** * Initializes an empty graph based on plot dimensions and a given symbol. - * @param {object} options - Configuration options. - * @param {number} options.plotWidth - Width of the plot area. - * @param {number} options.plotHeight - Height of the plot area. - * @param {string} options.emptySymbol - Symbol used to fill empty cells. - * @returns {Graph} - An initialized empty graph array. + * @param {Object} params - Configuration options for the graph. + * @param {number} params.plotWidth - Width of the plot area. + * @param {number} params.plotHeight - Height of the plot area. + * @param {string} params.emptySymbol - Symbol used to fill empty cells. + * @returns {Graph} - An initialized empty graph matrix. */ export const drawGraph = ({ plotWidth, @@ -241,24 +331,29 @@ export const drawGraph = ({ }; /** - * Renders the graph array into a single string. - * @param {object} options - Configuration options. - * @param {Graph} options.graph - The graph array to render. - * @returns {string} - The rendered graph. + * Renders the graph into a string format for output. + * @param {Object} params - Configuration options for rendering the graph. + * @param {Graph} params.graph - The graph matrix to render. + * @returns {string} - The rendered graph as a string. */ export const drawChart = ({ graph }: { graph: Graph }) => `\n${graph.map((line) => line.join('')).join('\n')}\n`; /** * Renders a custom line on the graph based on formatter specifications. - * @param {object} options - Configuration options. - * @param {Point[]} options.sortedCoords - Sorted list of coordinates. - * @param {number} options.scaledX - X-axis scaling. - * @param {number} options.scaledY - Y-axis scaling. - * @param {MultiLine} options.input - Input data points. - * @param {number} options.index - Current index in the coordinate array. - * @param {function} options.lineFormatter - Custom function for line formatting. - * @param {Graph} options.graph - The graph array to modify. + * @param {Object} params - Configuration options for rendering custom lines. + * @param {Point[]} params.sortedCoords - Sorted list of coordinates. + * @param {number} params.scaledX - X-axis scaling. + * @param {number} params.scaledY - Y-axis scaling. + * @param {number} params.minY - Minimum Y value. + * @param {number} params.minX - Minimum X value. + * @param {MultiLine} params.input - Input data points. + * @param {number[]} params.expansionX - X-axis expansion range. + * @param {number[]} params.expansionY - Y-axis expansion range. + * @param {function} params.toPlotCoordinates - Function to convert coordinates to plot positions. + * @param {number} params.index - Current index in the coordinate array. + * @param {function} params.lineFormatter - Custom function for line formatting. + * @param {Graph} params.graph - The graph matrix to modify. */ export const drawCustomLine = ({ sortedCoords, @@ -268,21 +363,26 @@ export const drawCustomLine = ({ index, lineFormatter, graph, + toPlotCoordinates, + expansionX, + expansionY, + minY, + minX, + debugMode, }: { sortedCoords: Point[]; scaledX: number; scaledY: number; input: MultiLine; index: number; - lineFormatter: (args: { - x: number; - y: number; - plotX: number; - plotY: number; - index: number; - input: Point[]; - }) => CustomSymbol | CustomSymbol[]; + minY: number; + minX: number; + expansionX: number[]; + expansionY: number[]; + toPlotCoordinates: (x: number, y: number) => Point; + lineFormatter: (args: LineFormatterArgs) => CustomSymbol | CustomSymbol[]; graph: Graph; + debugMode?: boolean; }) => { const lineFormatterArgs = { x: sortedCoords[index][0], @@ -291,28 +391,43 @@ export const drawCustomLine = ({ plotY: scaledY + 1, index, input: input[0], + minY, + minX, + toPlotCoordinates, + expansionX, + expansionY, }; const customSymbols = lineFormatter(lineFormatterArgs); if (Array.isArray(customSymbols)) { customSymbols.forEach(({ x: symbolX, y: symbolY, symbol }: CustomSymbol) => { - graph[symbolY][symbolX] = symbol; + drawPosition({ debugMode, graph, scaledX: symbolX, scaledY: symbolY, symbol }); }); } else { - graph[customSymbols.y][customSymbols.x] = customSymbols.symbol; + drawPosition({ + debugMode, + graph, + scaledX: customSymbols.x, + scaledY: customSymbols.y, + symbol: customSymbols.symbol, + }); } }; /** * Renders a line between two points on the graph using defined chart symbols. - * @param {object} options - Configuration options. - * @param {number} options.index - Current index in the coordinate array. - * @param {Point[]} options.arr - List of points for the line. - * @param {Graph} options.graph - The graph array to modify. - * @param {number} options.scaledX - X-axis scaling. - * @param {number} options.scaledY - Y-axis scaling. - * @param {number} options.plotHeight - Height of the plot. - * @param {string} options.emptySymbol - Symbol used to fill empty cells. - * @param {Symbols['chart']} options.chartSymbols - Symbols used for the chart. + * @param {Object} params - Configuration options for drawing a line. + * @param {number} params.index - Current index in the coordinate array. + * @param {Point[]} params.arr - List of points for the line. + * @param {Graph} params.graph - The graph matrix to modify. + * @param {number} params.scaledX - X-axis scaling. + * @param {number} params.scaledY - Y-axis scaling. + * @param {boolean} params.horizontalBarChart - Whether to fill the width of the bars. + * @param {boolean} params.barChart - Whether to fill the width of the bars. + * @param {number} params.plotHeight - Height of the plot area. + * @param {string} params.emptySymbol - Symbol used to fill empty cells. + * @param {Object} params.axis - Axis position. + * @param {MaybePoint} axisCenter - Axis position selected by user. + * @param {Symbols['chart']} params.chartSymbols - Symbols used for chart rendering. */ export const drawLine = ({ index, @@ -323,6 +438,11 @@ export const drawLine = ({ plotHeight, emptySymbol, chartSymbols, + horizontalBarChart, + barChart, + axisCenter, + debugMode, + axis, }: { index: number; arr: Point[]; @@ -332,32 +452,137 @@ export const drawLine = ({ plotHeight: number; emptySymbol: string; chartSymbols: Symbols['chart']; + horizontalBarChart?: boolean; + barChart?: boolean; + axisCenter: MaybePoint; + axis: { x: number; y: number }; + debugMode?: boolean; }) => { + const [currX, currY] = arr[index]; + if (barChart || horizontalBarChart) { + const positions: [number, number][] = []; + const axisCenterShift = axisCenter ? 0 : 1; + // For vertical bar chart + if (barChart) { + let i; + // Check if the value is positive or negative + if (scaledY >= axis.y) { + // For positive values, draw from the value down to the axis + i = scaledY; + while (i >= axis.y) { + positions.push([i, scaledX + axisCenterShift]); + i -= 1; + } + } else { + // For negative values, draw from the value up to the axis + i = scaledY; + while (i <= axis.y) { + positions.push([i, scaledX + axisCenterShift]); + i += 1; + } + } + } + + // For horizontal bar chart + if (horizontalBarChart) { + let i; + if (scaledX >= axis.x) { + // For positive values, draw rightward from the value to the axis + i = scaledX; + while (i >= axis.x) { + positions.push([scaledY + 1, i]); + i -= 1; + } + } else { + // For negative values, draw leftward from the value to the axis + i = scaledX; + while (i <= axis.x) { + positions.push([scaledY + 1, i]); + i += 1; + } + } + } + + // Draw all calculated positions + positions.forEach(([y, x]) => { + drawPosition({ + debugMode, + graph, + scaledX: x, + scaledY: y, + symbol: chartSymbols?.area || CHART.area, + }); + }); + + return; + } + if (index - 1 >= 0) { const [prevX, prevY] = arr[index - 1]; - const [currX, currY] = arr[index]; - Array(distance(currY, prevY)) .fill('') .forEach((_, steps, array) => { if (Math.round(prevY) > Math.round(currY)) { - graph[scaledY + 1][scaledX] = chartSymbols?.nse || CHART.nse; + drawPosition({ + debugMode, + graph, + scaledX, + scaledY: scaledY + 1, + symbol: chartSymbols?.nse || CHART.nse, + }); if (steps === array.length - 1) { - graph[scaledY - steps][scaledX] = chartSymbols?.wns || CHART.wns; + drawPosition({ + debugMode, + graph, + scaledX, + scaledY: scaledY - steps, + symbol: chartSymbols?.wns || CHART.wns, + }); } else { - graph[scaledY - steps][scaledX] = chartSymbols?.ns || CHART.ns; + drawPosition({ + debugMode, + graph, + scaledX, + scaledY: scaledY - steps, + symbol: chartSymbols?.ns || CHART.ns, + }); } } else { - graph[scaledY + steps + 2][scaledX] = chartSymbols?.wsn || CHART.wsn; - graph[scaledY + steps + 1][scaledX] = chartSymbols?.ns || CHART.ns; + drawPosition({ + debugMode, + graph, + scaledX, + scaledY: scaledY + steps + 2, + symbol: chartSymbols?.wsn || CHART.wsn, + }); + + drawPosition({ + debugMode, + graph, + scaledX, + scaledY: scaledY + steps + 1, + symbol: chartSymbols?.ns || CHART.ns, + }); } }); if (Math.round(prevY) < Math.round(currY)) { - graph[scaledY + 1][scaledX] = chartSymbols?.sne || CHART.sne; + drawPosition({ + debugMode, + graph, + scaledX, + scaledY: scaledY + 1, + symbol: chartSymbols?.sne || CHART.sne, + }); } else if (Math.round(prevY) === Math.round(currY)) { if (graph[scaledY + 1][scaledX] === emptySymbol) { - graph[scaledY + 1][scaledX] = chartSymbols?.we || CHART.we; + drawPosition({ + debugMode, + graph, + scaledX, + scaledY: scaledY + 1, + symbol: chartSymbols?.we || CHART.we, + }); } } @@ -366,25 +591,37 @@ export const drawLine = ({ .fill('') .forEach((_, steps) => { const thisY = plotHeight - Math.round(prevY); - graph[thisY][Math.round(prevX) + steps + 1] = chartSymbols?.we || CHART.we; + drawPosition({ + debugMode, + graph, + scaledX: Math.round(prevX) + steps + 1, + scaledY: thisY, + symbol: chartSymbols?.we || CHART.we, + }); }); } if (arr.length - 1 === index) { - graph[scaledY + 1][scaledX + 1] = chartSymbols?.we || CHART.we; + drawPosition({ + debugMode, + graph, + scaledX: scaledX + 1, + scaledY: scaledY + 1, + symbol: chartSymbols?.we || CHART.we, + }); } }; /** * Applies shifts to the graph and adjusts empty symbols and scaling factors. - * @param {object} options - Configuration options. - * @param {Graph} options.graph - The graph array to modify. - * @param {number} options.plotWidth - Width of the plot area. - * @param {string} options.emptySymbol - Symbol used to fill empty cells. - * @param {number[][]} options.scaledCoords - Scaled coordinates for shifting. - * @param {number} options.xShift - X-axis shift offset. - * @param {number} options.yShift - Y-axis shift offset. - * @returns {object} - Indicates whether graph movement is required. + * @param {Object} params - Configuration options for applying shifts. + * @param {Graph} params.graph - The graph matrix. + * @param {number} params.plotWidth - The width of the plot area. + * @param {string} params.emptySymbol - The symbol used to fill empty cells. + * @param {number[][]} params.scaledCoords - Scaled coordinates for shifting. + * @param {number} params.xShift - X-axis shift offset. + * @param {number} params.yShift - Y-axis shift offset. + * @returns {Object} - An object indicating if the graph needs to be moved. */ export const drawShift = ({ graph, @@ -417,7 +654,7 @@ export const drawShift = ({ graph.forEach((line) => { for (let i = 0; i <= yShift; i += 1) { - line.unshift(emptySymbol); // left + line.unshift(emptySymbol); // left shift } }); return { hasToBeMoved }; diff --git a/src/services/overrides.ts b/src/services/overrides.ts index b47c3f8..929c7d7 100644 --- a/src/services/overrides.ts +++ b/src/services/overrides.ts @@ -11,6 +11,7 @@ import { FormatterHelpers, } from '../types'; import { getPlotCoords, toArray, toEmpty, toPlot } from './coords'; +import { drawPosition } from './draw'; import { defaultFormatter, getAnsiColor, getChartSymbols } from './settings'; /** @@ -28,16 +29,24 @@ export const setTitle = ({ backgroundSymbol, plotWidth, yShift, + debugMode, }: { title: string; graph: Graph; backgroundSymbol: string; plotWidth: number; yShift: number; + debugMode?: boolean; }) => { graph.unshift(toEmpty(plotWidth + yShift + 2, backgroundSymbol)); Array.from(title).forEach((letter, index) => { - graph[0][index] = letter; + drawPosition({ + debugMode, + graph, + scaledX: index, + scaledY: 0, + symbol: letter, + }); }); }; @@ -56,12 +65,14 @@ export const addXLable = ({ yShift, backgroundSymbol, xLabel, + debugMode, }: { xLabel: string; graph: Graph; backgroundSymbol: string; plotWidth: number; yShift: number; + debugMode?: boolean; }) => { const totalWidth = graph[0].length; const labelLength = toArray(xLabel).length; @@ -69,7 +80,13 @@ export const addXLable = ({ graph.push(toEmpty(plotWidth + yShift + 2, backgroundSymbol)); Array.from(xLabel).forEach((letter, index) => { - graph[graph.length - 1][startingPosition + index] = letter; + drawPosition({ + debugMode, + graph, + scaledX: startingPosition + index, + scaledY: graph.length - 1, + symbol: letter, + }); }); }; @@ -84,10 +101,12 @@ export const addYLabel = ({ graph, backgroundSymbol, yLabel, + debugMode, }: { graph: Graph; backgroundSymbol: string; yLabel: string; + debugMode?: boolean; }) => { const totalHeight = graph.length; const labelLength = toArray(yLabel).length; @@ -97,7 +116,13 @@ export const addYLabel = ({ graph.forEach((line, position) => { line.unshift(backgroundSymbol); if (position > startingPosition && label[position - startingPosition - 1]) { - graph[position][0] = label[position - startingPosition - 1]; + drawPosition({ + debugMode, + graph, + scaledX: 0, + scaledY: position, + symbol: label[position - startingPosition - 1], + }); } }); }; @@ -121,6 +146,7 @@ export const addLegend = ({ symbols, fillArea, input, + debugMode, }: { graph: Graph; legend: Legend; @@ -129,6 +155,7 @@ export const addLegend = ({ color?: Colors; symbols?: Symbols; fillArea?: boolean; + debugMode?: boolean; }) => { const series = Array.isArray(legend.series) ? legend.series : [legend.series]; const legendWidth = 2 + series.reduce((acc, label) => Math.max(acc, toArray(label).length), 0); @@ -188,8 +215,13 @@ export const addLegend = ({ graph[index].forEach((_, symbolIndex) => { if (newSymbol[symbolIndex]) { - // eslint-disable-next-line no-param-reassign - graph[0][symbolIndex] = newSymbol[symbolIndex]; + drawPosition({ + debugMode, + graph, + scaledX: symbolIndex, + scaledY: 0, + symbol: newSymbol[symbolIndex], + }); } }); }); @@ -205,8 +237,13 @@ export const addLegend = ({ graph[index].forEach((_, symbolIndex) => { if (newSymbol[symbolIndex]) { - // eslint-disable-next-line no-param-reassign - graph[graph.length - 1][symbolIndex] = newSymbol[symbolIndex]; + drawPosition({ + debugMode, + graph, + scaledX: symbolIndex, + scaledY: graph.length - 1, + symbol: newSymbol[symbolIndex], + }); } }); }); @@ -239,15 +276,24 @@ export const addBackgroundSymbol = ({ graph, backgroundSymbol, emptySymbol, + debugMode, }: { graph: Graph; backgroundSymbol: string; emptySymbol: string; + debugMode?: boolean; }) => { - graph.forEach((line) => { + graph.forEach((line, curr) => { for (let index = 0; index < line.length; index += 1) { - if (line[index] === emptySymbol) line[index] = backgroundSymbol; - else break; + if (line[index] === emptySymbol) { + drawPosition({ + debugMode, + graph, + scaledX: index, + scaledY: curr, + symbol: backgroundSymbol, + }); + } else break; } }); }; @@ -271,6 +317,7 @@ export const addThresholds = ({ plotHeight, expansionX, expansionY, + debugMode, }: { graph: Graph; thresholds: Threshold[]; @@ -279,6 +326,7 @@ export const addThresholds = ({ plotHeight: number; expansionX: number[]; expansionY: number[]; + debugMode?: boolean; }) => { const mappedThreshold = thresholds.map(({ x: thresholdX, y: thresholdY }) => { let { x, y } = axis; @@ -299,22 +347,30 @@ export const addThresholds = ({ if (thresholds[thresholdNumber]?.x && graph[0][scaledX]) { graph.forEach((_, index) => { if (graph[index][scaledX]) { - graph[index][scaledX] = thresholds[thresholdNumber]?.color - ? `${getAnsiColor(thresholds[thresholdNumber]?.color || 'ansiRed')}${ - CHART.ns - }\u001b[0m` - : CHART.ns; + drawPosition({ + debugMode, + graph, + scaledX: scaledX + 1, + scaledY: index, + symbol: thresholds[thresholdNumber]?.color + ? `${getAnsiColor(thresholds[thresholdNumber]?.color || 'ansiRed')}${CHART.ns}\u001b[0m` + : CHART.ns, + }); } }); } if (thresholds[thresholdNumber]?.y && graph[scaledY]) { graph[scaledY].forEach((_, index) => { if (graph[scaledY][index]) { - graph[scaledY][index] = thresholds[thresholdNumber]?.color - ? `${getAnsiColor(thresholds[thresholdNumber]?.color || 'ansiRed')}${ - CHART.we - }\u001b[0m` - : CHART.we; + drawPosition({ + debugMode, + graph, + scaledX: index, + scaledY: scaledY + 1, + symbol: thresholds[thresholdNumber]?.color + ? `${getAnsiColor(thresholds[thresholdNumber]?.color || 'ansiRed')}${CHART.we}\u001b[0m` + : CHART.we, + }); } }); } @@ -331,9 +387,11 @@ export const addThresholds = ({ export const setFillArea = ({ graph, chartSymbols, + debugMode, }: { graph: Graph; chartSymbols: Symbols['chart']; + debugMode?: boolean; }) => { graph.forEach((xValues, yIndex) => { xValues.forEach((xSymbol, xIndex) => { @@ -344,7 +402,13 @@ export const setFillArea = ({ xSymbol === chartSymbols?.area ) { if (graph[yIndex + 1]?.[xIndex]) { - graph[yIndex + 1][xIndex] = chartSymbols.area || CHART.area; + drawPosition({ + debugMode, + graph, + scaledX: xIndex, + scaledY: yIndex + 1, + symbol: chartSymbols.area || CHART.area, + }); } } }); diff --git a/src/services/plot.ts b/src/services/plot.ts index bd791bf..243da8b 100644 --- a/src/services/plot.ts +++ b/src/services/plot.ts @@ -40,12 +40,15 @@ export const plot: Plot = ( symbols, title, fillArea, + horizontalBarChart, + barChart, hideXAxis, hideYAxis, xLabel, yLabel, legend, thresholds, + debugMode, } = {}, ) => { // Multiline @@ -67,7 +70,7 @@ export const plot: Plot = ( let scaledCoords = [[0, 0]]; - const { minX, plotWidth, plotHeight, expansionX, expansionY } = getChartSize({ + const { minX, minY, plotWidth, plotHeight, expansionX, expansionY } = getChartSize({ width, height, input, @@ -93,16 +96,45 @@ export const plot: Plot = ( scaledCoords = getPlotCoords(sortedCoords, plotWidth, plotHeight, expansionX, expansionY).map( ([x, y], index, arr) => { - const [scaledX, scaledY] = toPlot(plotWidth, plotHeight)(x, y); + const toPlotCoordinates = toPlot(plotWidth, plotHeight); + const [scaledX, scaledY] = toPlotCoordinates(x, y); if (!lineFormatter) { - drawLine({ index, arr, graph, scaledX, scaledY, plotHeight, emptySymbol, chartSymbols }); + drawLine({ + debugMode, + index, + arr, + graph, + scaledX, + scaledY, + plotHeight, + emptySymbol, + chartSymbols, + horizontalBarChart, + axis, + axisCenter, + barChart, + }); // fill empty area under the line if fill area is true if (fillArea) { - setFillArea({ graph, chartSymbols }); + setFillArea({ graph, chartSymbols, debugMode }); } } else { - drawCustomLine({ sortedCoords, scaledX, scaledY, input, index, lineFormatter, graph }); + drawCustomLine({ + debugMode, + sortedCoords, + scaledX, + scaledY, + input, + index, + lineFormatter, + graph, + toPlotCoordinates, + expansionX, + expansionY, + minY, + minX, + }); } return [scaledX, scaledY]; @@ -112,6 +144,7 @@ export const plot: Plot = ( if (thresholds) { addThresholds({ + debugMode, graph, thresholds, axis, @@ -124,6 +157,7 @@ export const plot: Plot = ( // axis drawAxis({ + debugMode, graph, hideXAxis, hideYAxis, @@ -150,7 +184,7 @@ export const plot: Plot = ( // apply background symbol if override if (backgroundSymbol) { - addBackgroundSymbol({ graph, backgroundSymbol, emptySymbol }); + addBackgroundSymbol({ debugMode, graph, backgroundSymbol, emptySymbol }); } // shift coords @@ -162,6 +196,7 @@ export const plot: Plot = ( const [scaledX, scaledY] = toPlot(plotWidth, plotHeight)(x, y); if (!hideYAxis) { drawYAxisEnd({ + debugMode, showTickLabel, plotHeight, graph, @@ -228,6 +263,7 @@ export const plot: Plot = ( } drawXAxisEnd({ + debugMode, hasPlaceToRender, axisCenter, yPos, @@ -251,6 +287,7 @@ export const plot: Plot = ( // Adds title above the graph if (title) { setTitle({ + debugMode, title, graph, backgroundSymbol, @@ -262,6 +299,7 @@ export const plot: Plot = ( // Adds x axis label below the graph if (xLabel) { addXLable({ + debugMode, xLabel, graph, backgroundSymbol, @@ -273,6 +311,7 @@ export const plot: Plot = ( // Adds x axis label below the graph if (yLabel) { addYLabel({ + debugMode, yLabel, graph, backgroundSymbol, @@ -281,6 +320,7 @@ export const plot: Plot = ( if (legend) { addLegend({ + debugMode, input, graph, legend, diff --git a/src/types/index.ts b/src/types/index.ts index 83246e6..3350abc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -48,6 +48,11 @@ export type LineFormatterArgs = { plotY: number; // y-coordinate on the plot input: SingleLine; // line input containing points index: number; // index of the current point + minY: number; // minimum y-value + minX: number; // minimum y-value + expansionX: number[]; // expansion values for x-axis + expansionY: number[]; // expansion values for y-axis + toPlotCoordinates: (x: number, y: number) => Point; // function to convert coordinates to plot coordinates }; /** @@ -128,11 +133,14 @@ export type Settings = { yLabel?: string; // Label for the y-axis thresholds?: Threshold[]; // Array of threshold lines or points fillArea?: boolean; // Option to fill the area under lines + horizontalBarChart?: boolean; // Option to draw horizontal bar chart + barChart?: boolean; // Option to draw bar chart legend?: Legend; // Legend settings axisCenter?: MaybePoint; // Center point for axes alignment formatter?: Formatter; // Custom formatter for axis values lineFormatter?: (args: LineFormatterArgs) => CustomSymbol | CustomSymbol[]; // Custom line formatter symbols?: Symbols; // Custom symbols for chart elements + debugMode?: boolean; // Option to enable debug mode }; /**