├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── __snapshots__ ├── 00_simple_example.txt ├── 01_small_numbers.txt ├── 02_Tick_Label.txt ├── 03_Border.txt ├── 04_Simple_chart.txt ├── 05_bar_chart.txt ├── 06_horizontal_bar_chart.txt ├── 07_area.txt ├── 08_labels.txt ├── 09_legend.txt ├── 10_multiline.txt ├── 11_multiline_points.txt ├── 12_yRange.txt ├── 13_yRange.txt ├── 14_showTickLabel.txt ├── 15_hideXAxis.txt ├── 16_hideYAxis.txt ├── 17_axisCenter.txt ├── 18_lineFormatter.txt ├── 19_lineFormatter.txt ├── 20_symbols.txt ├── 21_colors.txt ├── 22_custom_y_axis.txt ├── 23_custom_ticks_and_axis_center.txt ├── 24_custom_ticks_and_axis_center_hide_x_axis.txt ├── 25_custom_ticks_and_axis_center_hide_y_axis.txt ├── 26_custom_ticks_and_axis_center_hide_y_axis_show_tick_label.txt ├── 27_colors_with_legend.txt ├── 28_thresholds.txt ├── 29_thresholds.txt ├── 30_with_axis_center.txt ├── 31_bar_chart_with_colors.txt ├── 32_horizontal_bar_chart_with_axis_center.txt ├── 33_horizontal_bar_chart.txt ├── 34_bar_chart.txt ├── 35_axis_center.txt ├── 36_axis_center.txt ├── 37_axis_center.txt ├── 38_axis_center.txt ├── 39_axis_center.txt ├── 40_axis_center.txt ├── 40_draws_two_complicated_graphs_with_moved_axis.txt ├── 41_Small_and_big_values.txt ├── 41_showTickLabel_true.txt ├── 42_showTickLabel_false.txt ├── 42_showTickLabel_true.txt ├── 43_hideXAxis_true.txt ├── 43_showTickLabel_false.txt ├── 44_hideXAxis_false.txt ├── 44_hideXAxis_true.txt ├── 45_hideXAxisTicks_true.txt ├── 45_hideXAxis_false.txt ├── 46_hideXAxisTicks_false.txt ├── 46_hideXAxisTicks_true.txt ├── 47_hideXAxisTicks_false.txt ├── 47_hideYAxis_true.txt ├── 48_hideYAxis_false.txt ├── 48_hideYAxis_true.txt ├── 49_hideYAxisTicks_true.txt ├── 49_hideYAxis_false.txt ├── 50_hideYAxisTicks_false.txt ├── 50_hideYAxisTicks_true.txt ├── 51_fillArea_true.txt ├── 51_hideYAxisTicks_false.txt ├── 52_fillArea_false.txt ├── 52_fillArea_true.txt ├── 53_fillArea_false.txt ├── 53_width_10.txt ├── 54_width_10.txt ├── 54_width_50.txt ├── 55_height_10.txt ├── 55_width_50.txt ├── 56_height_10.txt ├── 56_height_50.txt ├── 57_height_50.txt ├── 57_xLabel_label_.txt ├── 58_xLabel_label_.txt ├── 58_yLabel_label_.txt ├── 59_yLabel_label_.txt ├── 59_yRange_0_10_.txt ├── 60_yRange_-5_5_.txt ├── 60_yRange_0_10_.txt ├── 61_axisCenter_0_10_.txt ├── 61_yRange_-5_5_.txt ├── 62_axisCenter_-5_5_.txt ├── 62_axisCenter_0_10_.txt ├── 63_axisCenter_-5_5_.txt ├── 64_axis_center.txt ├── 65_axis_center.txt ├── 66_axis_center.txt ├── 67_axis_center.txt ├── 68_axis_center.txt ├── 69_raws_two_complicated_graphs_with_moved_axis.txt ├── 70_hide_axis.txt └── 71_bar_chart_with_axis.txt ├── eslint.config.js ├── jest.config.js ├── package.json ├── src ├── __tests__ │ ├── services │ │ ├── coords.test.ts │ │ ├── defaults.test.ts │ │ ├── draw.test.ts │ │ ├── overrides.test.ts │ │ ├── plot.test.ts │ │ └── settings.test.ts │ └── snapshot.test.ts ├── constants │ └── index.ts ├── examples.ts ├── index.ts ├── scripts │ ├── generate-snapshots.ts │ └── show-examples.ts ├── services │ ├── coords.ts │ ├── defaults.ts │ ├── draw.ts │ ├── overrides.ts │ ├── plot.ts │ └── settings.ts └── types │ └── index.ts ├── tsconfig.json └── yarn.lock /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.x, 20.x] 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Use Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: '18.x' 25 | - name: Install dependencies 26 | run: yarn --frozen-lockfile 27 | - name: Run tests and collect coverage 28 | run: yarn test 29 | - name: Upload coverage reports to Codecov 30 | uses: codecov/codecov-action@v4 31 | env: 32 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # main 2 | dist/ 3 | node_modules/ 4 | 5 | # cache 6 | *.tsbuildinfo 7 | .eslintcache 8 | .stylelintcache 9 | 10 | # logs 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # miscellaneous 15 | .idea 16 | .DS_Store 17 | coverage 18 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | coverage 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bartosz Gryta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple ASCII Chart 2 | 3 | ![NPM License](https://img.shields.io/npm/l/simple-ascii-chart) 4 | ![NPM Version](https://img.shields.io/npm/v/simple-ascii-chart) 5 | ![npm package minimized gzipped size (select exports)](https://img.shields.io/bundlejs/size/simple-ascii-chart) 6 | ![Codecov](https://img.shields.io/codecov/c/github/gtktsc/ascii-chart) 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. 9 | 10 | [Playground and documentation](https://simple-ascii-chart.vercel.app/) 11 | [NPM](https://www.npmjs.com/package/simple-ascii-chart) 12 | 13 | ## Installation 14 | 15 | Install the package using `yarn` (or `npm`): 16 | 17 | ```bash 18 | yarn add simple-ascii-chart 19 | # or 20 | npm install simple-ascii-chart 21 | ``` 22 | 23 | ## Usage 24 | 25 | In ESM (e.g., TypeScript, modern Node.js) 26 | 27 | ```javascript 28 | import plot from 'simple-ascii-chart'; 29 | // or, if you prefer named imports: 30 | import { plot } from 'simple-ascii-chart'; 31 | 32 | const graph = plot(input, settings); 33 | console.log(graph); 34 | ``` 35 | 36 | In CommonJS (e.g., legacy Node.js) 37 | 38 | ```javascript 39 | // Option 1: access default export 40 | const plot = require('simple-ascii-chart').default; 41 | 42 | // Option 2: use named export 43 | const { plot } = require('simple-ascii-chart'); 44 | 45 | const graph = plot(input, settings); 46 | console.log(graph); 47 | ``` 48 | 49 | ## CLI 50 | 51 | [CLI tool is available too](https://github.com/gtktsc/simple-ascii-chart-cli) 52 | [NPM](https://www.npmjs.com/package/simple-ascii-chart-cli) 53 | 54 | ## Playground 55 | 56 | Create charts interactively in the [playground](https://simple-ascii-chart.vercel.app/playground). 57 | 58 | ## API Endpoint 59 | 60 | Generate charts via the API by sending a POST request with your input data: 61 | 62 | ```bash 63 | curl -d input='[[1,2],[2,3],[3,4]]' -G https://simple-ascii-chart.vercel.app/api 64 | ``` 65 | 66 | Or pass it as a URL parameter: 67 | 68 | ```bash 69 | https://simple-ascii-chart.vercel.app/api?input=[[1,2],[2,3],[3,4]]&settings={%22width%22:50} 70 | ``` 71 | 72 | ## Input Format 73 | 74 | Input data should be a two-dimensional array of points or an array of arrays for multiple series: 75 | 76 | ```typescript 77 | type Point = [x: number, y: number]; 78 | type Input = Point[] | Point[][]; 79 | ``` 80 | 81 | Single series 82 | 83 | ```typescript 84 | const input = [ 85 | [1, 1], 86 | [2, 4], 87 | [3, 40], 88 | ]; 89 | 90 | ``` 91 | 92 | Multiple series 93 | 94 | ```typescript 95 | const input = [ 96 | [ 97 | [0, 18], 98 | [1, 1], 99 | [2, 3], 100 | ], 101 | [ 102 | [4, 1], 103 | [5, 0], 104 | [6, 1], 105 | ], 106 | ]; 107 | ``` 108 | 109 | ### Detailed Input Parameters 110 | 111 | - **`Point`**: A single point with x and y coordinates, represented as `[x, y]`. 112 | - **`MaybePoint`**: Allows partial or undefined values within a point, accommodating incomplete data. 113 | - **`SingleLine`**: A series of connected points representing a single line. 114 | - **`MultiLine`**: A collection of `SingleLine` arrays for multiple data series. 115 | 116 | ## Configuration (Settings) 117 | 118 | Customize the `plot` function with a variety of settings: 119 | 120 | ### Available Settings 121 | 122 | | Option | Description | 123 | |------------------|---------------------------------------------------------------------------------------------------------------| 124 | | `color` | Colors for the graph. Options include `'ansiRed'`, `'ansiGreen'`, etc. Multiple colors are accepted for series. | 125 | | `width` | Sets the graph width. | 126 | | `height` | Sets the graph height. | 127 | | `axisCenter` | Specifies the center of the axis as `[x, y]`, with default as bottom-left. | 128 | | `formatter` | A function to format axis labels, offering custom display styles. | 129 | | `lineFormatter` | Function to define custom styles for each line. | 130 | | `title` | Title of the chart, displayed above the graph. | 131 | | `xLabel` | Label for the x-axis. | 132 | | `yLabel` | Label for the y-axis. | 133 | | `thresholds` | Defines threshold lines or points with optional colors at specific x or y coordinates. | 134 | | `points` | Defines points with optional colors at specific x or y coordinates. | 135 | | `fillArea` | Fills the area under each line, suitable for area charts. | 136 | | `hideXAxis` | Hides the x-axis. | 137 | | `hideYAxis` | Hides the y-axis. | 138 | | `mode` | Sets the plotting mode (line, point, bar, horizontal bar), defaults to line | 139 | | `symbols` | Symbols for customizing the chart’s appearance, including axis, background, and chart symbols. | 140 | | `legend` | Configuration for a legend, showing series names and position options (`left`, `right`, `top`, `bottom`). | 141 | | `debugMode` | Enables debug mode (`default = false`). | 142 | 143 | ### Advanced Settings 144 | 145 | | Setting | Description | 146 | |----------------------|-----------------------------------------------------------------------------------------------------------| 147 | | `yRange` | Specifies the y-axis range as `[min, max]`. | 148 | | `showTickLabel` | Enables tick labels on the axis, improving readability for larger plots. | 149 | | `legend` | Configures legend display with position and series names, such as `{ position: 'top', series: ['Series 1', 'Series 2'] }, thresholds: ['first', 'second'], points: ['1', '2']]`. | 150 | | `ColorGetter` | A function for dynamic color assignment based on series or coordinates. | 151 | | `axisCenter` | Sets a custom origin point for the chart, shifting the chart layout to focus around a particular point. | 152 | | `lineFormatter` | Customize each line using the format `lineFormatter: (args: LineFormatterArgs) => CustomSymbol | CustomSymbol[]`. | 153 | | `formatterHelpers` | Provides helpers such as axis type and range for detailed formatting of axis labels. | 154 | 155 | ### Symbols 156 | 157 | Overrides default symbols. Three independent sections are: `empty` - background, `axis` - symbols used to draw axis, `chart` - symbols used to draw graph. 158 | 159 | ```typescript 160 | symbols: { 161 | background: ' ', 162 | border: undefined, 163 | point: '●', 164 | thresholds:{ 165 | x: '━', 166 | y: '┃', 167 | }, 168 | empty: ' ', 169 | axis: { 170 | n: '▲', 171 | ns: '│', 172 | y: '┤', 173 | nse: '└', 174 | x: '┬', 175 | we: '─', 176 | e: '▶', 177 | }, 178 | chart: { 179 | we: '━', 180 | wns: '┓', 181 | ns: '┃', 182 | nse: '┗', 183 | wsn: '┛', 184 | sne: '┏', 185 | area: '█' 186 | } 187 | } 188 | ``` 189 | 190 | ### Summary 191 | 192 | ```typescript 193 | Settings = { 194 | color?: Colors; // Colors for the plot lines or areas 195 | width?: number; // Width of the plot 196 | height?: number; // Height of the plot 197 | yRange?: [number, number]; // Range of y-axis values 198 | showTickLabel?: boolean; // Option to show tick labels on the axis 199 | hideXAxis?: boolean; // Option to hide the x-axis 200 | hideYAxis?: boolean; // Option to hide the y-axis 201 | title?: string; // Title of the plot 202 | xLabel?: string; // Label for the x-axis 203 | yLabel?: string; // Label for the y-axis 204 | thresholds?: Threshold[]; // Array of threshold lines 205 | points?: GraphPoint[] // Array of points to render 206 | fillArea?: boolean; // Option to fill the area under lines 207 | legend?: Legend; // Legend settings 208 | axisCenter?: MaybePoint; // Center point for axes alignment 209 | formatter?: Formatter; // Custom formatter for axis values 210 | lineFormatter?: (args: LineFormatterArgs) => CustomSymbol | CustomSymbol[]; // Custom line formatter 211 | symbols?: Symbols; // Custom symbols for chart elements 212 | }; 213 | ``` 214 | 215 | ## Examples 216 | 217 | ### Simple Plot 218 | 219 | Input: 220 | 221 | ```typescript 222 | plot( 223 | [ 224 | [1, 1], 225 | [2, 4], 226 | [3, 4], 227 | [4, 2], 228 | [5, -1], 229 | ], 230 | { width: 9, height: 6 }, 231 | ); 232 | ``` 233 | 234 | Expected Output: 235 | 236 | ```bash 237 | ▲ 238 | 4┤ ┏━━━┓ 239 | │ ┃ ┃ 240 | 2┤ ┃ ┗━┓ 241 | 1┤━┛ ┃ 242 | │ ┃ 243 | -1┤ ┗━ 244 | └┬─┬─┬─┬─┬▶ 245 | 1 2 3 4 5 246 | ``` 247 | 248 | ### Plot with Title and Custom Size 249 | 250 | Input: 251 | 252 | ```typescript 253 | plot( 254 | [ 255 | [1, 1], 256 | [2, 4], 257 | [3, 4], 258 | [4, 2], 259 | [5, -1], 260 | [6, 3], 261 | [7, -1], 262 | [8, 9], 263 | ], 264 | { title: 'Important data', width: 20, height: 8 }, 265 | ); 266 | ``` 267 | 268 | Expected Output: 269 | 270 | ```bash 271 | Important data 272 | ▲ 273 | 9┤ ┏━ 274 | │ ┃ 275 | │ ┃ 276 | 4┤ ┏━━━━┓ ┃ 277 | 3┤ ┃ ┃ ┏━┓ ┃ 278 | 2┤ ┃ ┗━━┓ ┃ ┃ ┃ 279 | 1┤━━┛ ┃ ┃ ┃ ┃ 280 | -1┤ ┗━━┛ ┗━━┛ 281 | └┬──┬─┬──┬──┬──┬─┬──┬▶ 282 | 1 2 3 4 5 6 7 8 283 | ``` 284 | 285 | ### Plot with Axis Labels 286 | 287 | Input: 288 | 289 | ```typescript 290 | plot( 291 | [ 292 | [1, 1], 293 | [2, 4], 294 | [3, 4], 295 | [4, 2], 296 | [5, -1], 297 | [6, 3], 298 | [7, -1], 299 | [8, 9], 300 | ], 301 | { xLabel: 'x', yLabel: 'y', width: 20, height: 8 }, 302 | ); 303 | ``` 304 | 305 | Expected Output: 306 | 307 | ```bash 308 | ▲ 309 | 9┤ ┏━ 310 | │ ┃ 311 | │ ┃ 312 | 4┤ ┏━━━━┓ ┃ 313 | 3┤ ┃ ┃ ┏━┓ ┃ 314 | y 2┤ ┃ ┗━━┓ ┃ ┃ ┃ 315 | 1┤━━┛ ┃ ┃ ┃ ┃ 316 | -1┤ ┗━━┛ ┗━━┛ 317 | └┬──┬─┬──┬──┬──┬─┬──┬▶ 318 | 1 2 3 4 5 6 7 8 319 | x 320 | ``` 321 | 322 | ### Plot with colors 323 | 324 | Input: 325 | 326 | ```typescript 327 | plot( 328 | [ 329 | [ 330 | [1, 1], 331 | [2, 2], 332 | [3, 4], 333 | [4, 6], 334 | ], 335 | [ 336 | [5, 4], 337 | [6, 1], 338 | [7, 2], 339 | [8, 3], 340 | ], 341 | ], 342 | { 343 | width: 20, 344 | fillArea: true, 345 | color: ['ansiGreen', 'ansiBlue'], 346 | legend: { position: 'bottom', series: ['first', 'second'] }, 347 | }, 348 | ); 349 | ``` 350 | 351 | Expected Output: 352 | 353 | ```bash 354 | ▲ 355 | 6┤ ██ 356 | │ ██ 357 | 4┤ █████ ███ 358 | 3┤ █████ ███ ██ 359 | 2┤ ███████ ███ █████ 360 | 1┤█████████ █████████ 361 | └┬──┬─┬──┬──┬──┬─┬──┬▶ 362 | 1 2 3 4 5 6 7 8 363 | █ first 364 | █ second 365 | 366 | ``` 367 | 368 | ### Plot with borders 369 | 370 | Input: 371 | 372 | ```typescript 373 | plot( 374 | [ 375 | [1, 1], 376 | [2, 4], 377 | [3, 4], 378 | [4, 2], 379 | [5, -1], 380 | [6, 3], 381 | [7, -1], 382 | [8, 9], 383 | ], 384 | { symbols: { border: '█' }, xLabel: 'x', yLabel: 'y', width: 20, height: 8 }, 385 | ); 386 | ``` 387 | 388 | Expected Output: 389 | 390 | ```bash 391 | ███████████████████████████ 392 | █ ▲ █ 393 | █ 9┤ ┏━ █ 394 | █ │ ┃ █ 395 | █ │ ┃ █ 396 | █ 4┤ ┏━━━━┓ ┃ █ 397 | █ 3┤ ┃ ┃ ┏━┓ ┃ █ 398 | █y 2┤ ┃ ┗━━┓ ┃ ┃ ┃ █ 399 | █ 1┤━━┛ ┃ ┃ ┃ ┃ █ 400 | █ -1┤ ┗━━┛ ┗━━┛ █ 401 | █ └┬──┬─┬──┬──┬──┬─┬──┬▶█ 402 | █ 1 2 3 4 5 6 7 8 █ 403 | █ x █ 404 | ███████████████████████████ 405 | ``` 406 | 407 | ### Plot with filled area 408 | 409 | Input: 410 | 411 | ```typescript 412 | plot( 413 | [ 414 | [ 415 | [1, 1], 416 | [2, 2], 417 | [3, 4], 418 | [4, 6], 419 | ], 420 | [ 421 | [1, 4], 422 | [2, 1], 423 | [3, 2], 424 | [4, 3], 425 | ], 426 | ], 427 | { 428 | fillArea: true, 429 | color: ['ansiGreen', 'ansiBlue'], 430 | }, 431 | ); 432 | ``` 433 | 434 | Expected Output: 435 | 436 | ```bash 437 | ▲ 438 | 6┤ ██ 439 | │ ██ 440 | 4┤████ 441 | 3┤████ 442 | 2┤████ 443 | 1┤████ 444 | └┬┬┬┬▶ 445 | 1234 446 | ``` 447 | 448 | ### Scaled up plot 449 | 450 | Input: 451 | 452 | ```typescript 453 | plot( 454 | [ 455 | [1, 1], 456 | [2, 4], 457 | [3, 40], 458 | [4, 2], 459 | [5, -1], 460 | [6, 3], 461 | [7, -1], 462 | [8, -1], 463 | [9, 9], 464 | [10, 9], 465 | ], 466 | { width: 40, height: 10 }, 467 | ); 468 | ``` 469 | 470 | Expected Output: 471 | 472 | ```bash 473 | ▲ 474 | 40┤ ┏━━━┓ 475 | │ ┃ ┃ 476 | │ ┃ ┃ 477 | │ ┃ ┃ 478 | │ ┃ ┃ 479 | │ ┃ ┃ 480 | │ ┃ ┃ 481 | 9┤ ┃ ┃ ┏━━━━━ 482 | 3┤ ┏━━━━┛ ┗━━━┓ ┏━━━┓ ┃ 483 | -1┤━━━┛ ┗━━━━┛ ┗━━━━━━━━┛ 484 | └┬───┬────┬───┬───┬────┬───┬───┬────┬───┬▶ 485 | 1 2 3 4 5 6 7 8 9 10 486 | ``` 487 | 488 | ### Add thresholds 489 | 490 | Input: 491 | 492 | ```typescript 493 | plot( 494 | [ 495 | [1, 1], 496 | [2, 4], 497 | [3, 4], 498 | [4, 2], 499 | [5, -1], 500 | [6, 3], 501 | [7, -1], 502 | [8, 9], 503 | ], 504 | { 505 | width: 40, 506 | thresholds: [ 507 | { 508 | y: 5, 509 | x: 5, 510 | }, 511 | { 512 | x: 2, 513 | }, 514 | ], 515 | }, 516 | ); 517 | ``` 518 | 519 | Expected Output: 520 | 521 | ```bash 522 | ▲ ┃ ┃ 523 | 9┤ ┃ ┃ ┏━ 524 | │ ┃ ┃ ┃ 525 | │ ┃ ┃ ┃ 526 | │━━━━━┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 527 | │ ┃ ┃ ┃ 528 | 4┤ ┃━━━━━━━━━━┓ ┃ ┃ 529 | 3┤ ┃ ┃ ┃ ┏━━━━┓ ┃ 530 | 2┤ ┃ ┗━━━━┃ ┃ ┃ ┃ 531 | 1┤━━━━━┃ ┃ ┃ ┃ ┃ 532 | │ ┃ ┃ ┃ ┃ ┃ 533 | -1┤ ┃ ┃━━━━━┛ ┗━━━━━┛ 534 | └┬─────┬────┬─────┬────┬─────┬────┬─────┬▶ 535 | 1 2 3 4 5 6 7 8 536 | ``` 537 | 538 | ### Add points, threshold and legend 539 | 540 | Input: 541 | 542 | ```typescript 543 | plot( 544 | [ 545 | [ 546 | [1, 2], 547 | [2, -2], 548 | [3, 4], 549 | [4, 1], 550 | ], 551 | [ 552 | [1, 6], 553 | [2, -3], 554 | [3, 0], 555 | [4, 0], 556 | ], 557 | ], 558 | { 559 | width: 40, 560 | color: ['ansiGreen', 'ansiMagenta', 'ansiBlack', 'ansiYellow'], 561 | legend: { 562 | position: 'left', 563 | series: ['series 1', 'series 2'], 564 | points: ['point 1', 'point 2', 'point 3'], 565 | thresholds: ['threshold 1', 'threshold 2'], 566 | }, 567 | title: 'Points', 568 | thresholds: [ 569 | { 570 | y: 5, 571 | x: 2, 572 | color: 'ansiBlue', 573 | }, 574 | { 575 | y: 2, 576 | color: 'ansiGreen', 577 | }, 578 | ], 579 | points: [ 580 | { 581 | y: 5, 582 | x: 5, 583 | color: 'ansiBlue', 584 | }, 585 | { 586 | y: -1, 587 | x: 1, 588 | color: 'ansiCyan', 589 | }, 590 | { 591 | y: 2, 592 | x: 2, 593 | color: 'ansiRed', 594 | }, 595 | ], 596 | }, 597 | ); 598 | ``` 599 | 600 | Expected Output: 601 | 602 | ```bash 603 | Points 604 | █ series 1 ▲ ┃ 605 | █ series 2 6┤━━━━━━━━━━━━┓┃ 606 | │━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━● 607 | ┃ threshold 1 4┤ ┃┃ ┏━━━━━━━━━━━━┓ 608 | ┃ threshold 2 │ ┃┃ ┃ ┃ 609 | 2┤━━━━━━━━━━━━━●━━━━━━━━━━━━━━━━━━━━━━━━━━━ 610 | ● point 1 1┤ ┃┃ ┃ ┗━ 611 | ● point 2 0┤ ┃┃ ┏━━━━━━━━━━━━━━ 612 | ● point 3 │● ┃┃ ┃ 613 | -2┤ ┃┃━━━━━━━━━━━┃ 614 | -3┤ ┗┃━━━━━━━━━━━┛ 615 | └┬────────────┬────────────┬────────────┬▶ 616 | 1 2 3 4 617 | ``` 618 | 619 | 620 | ### Add Points 621 | 622 | Input: 623 | 624 | ```typescript 625 | plot( 626 | [ 627 | [1, 1], 628 | [2, 4], 629 | [3, 4], 630 | [4, 2], 631 | [5, -1], 632 | [6, 3], 633 | [7, -1], 634 | [8, 9], 635 | ], 636 | { 637 | width: 40, 638 | title: 'Points', 639 | points: [ 640 | { 641 | y: 5, 642 | x: 5, 643 | color: 'ansiBlue', 644 | }, 645 | { 646 | y: -1, 647 | x: 1, 648 | color: 'ansiBlue', 649 | }, 650 | { 651 | y: 205, 652 | x: 1005, 653 | color: 'ansiBlue', 654 | }, 655 | { 656 | y: 2, 657 | x: 2, 658 | color: 'ansiRed', 659 | }, 660 | ], 661 | }, 662 | ); 663 | ``` 664 | 665 | Expected Output: 666 | 667 | ```bash 668 | Points 669 | ▲ 670 | 9┤ ┏━ 671 | │ ┃ 672 | │ ┃ 673 | │ ┃ 674 | │ ● ┃ 675 | 4┤ ┏━━━━━━━━━━┓ ┃ 676 | 3┤ ┃ ┃ ┏━━━━┓ ┃ 677 | 2┤ ┃● ┗━━━━┓ ┃ ┃ ┃ 678 | 1┤━━━━━┛ ┃ ┃ ┃ ┃ 679 | │ ┃ ┃ ┃ ┃ 680 | -1┤● ┗━━━━━┛ ┗━━━━━┛ 681 | └┬─────┬────┬─────┬────┬─────┬────┬─────┬▶ 682 | 1 2 3 4 5 6 7 8 683 | ``` 684 | 685 | ### Multi-series plot 686 | 687 | Input: 688 | 689 | ```typescript 690 | plot( 691 | [ 692 | [ 693 | [0, 18], 694 | [1, 1], 695 | [2, 3], 696 | [3, 11], 697 | [4, 5], 698 | [5, 16], 699 | [6, 17], 700 | [7, 14], 701 | [8, 7], 702 | [9, 4], 703 | ], 704 | [ 705 | [0, 0], 706 | [1, 1], 707 | [2, 1], 708 | [3, 1], 709 | [4, 1], 710 | [5, 0], 711 | [6, 1], 712 | [7, 0], 713 | [8, 1], 714 | [9, 0], 715 | ], 716 | ], 717 | { width: 40, height: 10, color: ['ansiBlue', 'ansiGreen'] }, 718 | ); 719 | ``` 720 | 721 | Expected Output: 722 | 723 | ```bash 724 | ▲ 725 | 17┤━━━━┓ ┏━━━━┓ 726 | 16┤ ┃ ┏━━━━━┛ ┃ 727 | 14┤ ┃ ┃ ┗━━━━━┓ 728 | 11┤ ┃ ┏━━━━━┓ ┃ ┃ 729 | │ ┃ ┃ ┃ ┃ ┃ 730 | 7┤ ┃ ┃ ┃ ┃ ┗━━━━┓ 731 | 5┤ ┃ ┃ ┗━━━━┛ ┃ 732 | 4┤ ┃ ┏━━━━┛ ┗━ 733 | 1┤ ┏━━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━┓ ┏━━━━┓ 734 | 0┤━━━━┛ ┗━━━━━┛ ┗━━━━━┛ ┗━ 735 | └┬────┬─────┬────┬─────┬────┬─────┬────┬─────┬────┬▶ 736 | 0 1 2 3 4 5 6 7 8 9 737 | ``` 738 | 739 | ### Plot with formatting applied 740 | 741 | Input: 742 | 743 | ```typescript 744 | plot( 745 | [ 746 | [ 747 | [0, -10], 748 | [1, 0.001], 749 | [2, 10], 750 | [3, 200], 751 | [4, 10000], 752 | [5, 2000000], 753 | [6, 50000000], 754 | ], 755 | ], 756 | { 757 | width: 30, 758 | height: 20, 759 | formatter: (n: number, { axis }: FormatterHelpers) => { 760 | const labels = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; 761 | if (axis === 'y') return n; 762 | return labels[n] || 'X'; 763 | }, 764 | }, 765 | ); 766 | ``` 767 | 768 | Expected Output: 769 | 770 | ```bash 771 | ▲ 772 | 50000000┤ ┏━ 773 | │ ┃ 774 | │ ┃ 775 | │ ┃ 776 | │ ┃ 777 | │ ┃ 778 | │ ┃ 779 | │ ┃ 780 | │ ┃ 781 | │ ┃ 782 | │ ┃ 783 | │ ┃ 784 | │ ┃ 785 | │ ┃ 786 | │ ┃ 787 | │ ┃ 788 | │ ┃ 789 | │ ┃ 790 | 2000000┤ ┏━━━━┛ 791 | -10┤━━━━━━━━━━━━━━━━━━━━━━━┛ 792 | └┬────┬────┬────┬───┬────┬────┬▶ 793 | A B C D E F G 794 | ``` 795 | 796 | ### Plot with axis center 797 | 798 | Input: 799 | 800 | ```typescript 801 | plot( 802 | [ 803 | [ 804 | [-8, -8], 805 | [-4, -4], 806 | [-3, -3], 807 | [-2, -2], 808 | [-1, -1], 809 | [0, 0], 810 | [2, 2], 811 | [3, 3], 812 | [4, 4], 813 | [8, 8], 814 | ], 815 | ], 816 | { width: 60, height: 20, axisCenter: [0, 0] }, 817 | ); 818 | ``` 819 | 820 | Expected Output: 821 | 822 | ```bash 823 | ▲ 824 | 8┤ ┏━ 825 | │ ┃ 826 | │ ┃ 827 | │ ┃ 828 | │ ┃ 829 | 4┤ ┏━━━━━━━━━━━━━━┛ 830 | 3┤ ┏━━┛ 831 | 2┤ ┏━━━┛ 832 | │ ┃ 833 | ┬──────────────┬──┬───┬───┬───0│─────────┬──┬──────────────┬─▶ 834 | -8 -4 -3 -2 -1 0│ 2 3 4 8 835 | ┏━━-1┤ 836 | ┏━━━┛ -2┤ 837 | ┏━━━┛ -3┤ 838 | ┏━━┛ -4┤ 839 | ┃ │ 840 | ┃ │ 841 | ┃ │ 842 | ┃ │ 843 | ━━━━━━━━━━━━━━┛ -8┤ 844 | │ 845 | ``` 846 | 847 | ## Plot with custom symbols 848 | 849 | Input: 850 | 851 | ```typescript 852 | plot( 853 | [ 854 | [1, 2], 855 | [2, 0], 856 | [3, 5], 857 | [4, 2], 858 | [5, -2], 859 | [6, 3], 860 | ], 861 | { 862 | symbols: { 863 | empty: 'x', 864 | empty: '-', 865 | axis: { 866 | n: 'A', 867 | ns: 'i', 868 | y: 't', 869 | nse: 'o', 870 | x: 'j', 871 | we: 'm', 872 | e: 'B', 873 | }, 874 | chart: { 875 | we: '1', 876 | wns: '2', 877 | ns: '3', 878 | nse: '4', 879 | wsn: '5', 880 | sne: '6', 881 | }, 882 | }, 883 | width: 40, 884 | height: 10, 885 | }, 886 | ); 887 | ``` 888 | 889 | Expected Output: 890 | 891 | ```bash 892 | xxA----------------------------------------- 893 | x5t---------------61111112------------------ 894 | xxi---------------3------3------------------ 895 | xxi---------------3------3------------------ 896 | x3t---------------3------3---------------61- 897 | x2t11111112-------3------411111112-------3-- 898 | xxi-------3-------3--------------3-------3-- 899 | x0t-------411111115--------------3-------3-- 900 | xxi------------------------------3-------3-- 901 | xxi------------------------------3-------3-- 902 | -2t------------------------------411111115-- 903 | xxojmmmmmmmjmmmmmmmjmmmmmmjmmmmmmmjmmmmmmmjB 904 | xxx1xxxxxxx2xxxxxxx3xxxxxx4xxxxxxx5xxxxxxx6x 905 | ``` 906 | 907 | ### Plot without axis 908 | 909 | Input: 910 | 911 | ```typescript 912 | plot( 913 | [ 914 | [-5, 2], 915 | [2, -3], 916 | [13, 0.1], 917 | [4, 2], 918 | [5, -2], 919 | [6, 12], 920 | ], 921 | { 922 | width: 40, 923 | height: 10, 924 | hideYAxis: true, 925 | hideXAxis: true, 926 | }, 927 | ); 928 | ``` 929 | 930 | Expected Output: 931 | 932 | ```bash 933 | ┏━━━━━━━━━━━━━━┓ 934 | ┃ ┃ 935 | ┃ ┃ 936 | ┃ ┃ 937 | ┃ ┃ 938 | ┃ ┃ 939 | ━━━━━━━━━━━━━━┓ ┏━┓ ┃ ┃ 940 | ┃ ┃ ┃ ┃ ┗━ 941 | ┃ ┃ ┗━┛ 942 | ┗━━━━┛ 943 | ``` 944 | 945 | ### Plot with with large numbers 946 | 947 | Input: 948 | 949 | ```typescript 950 | plot( 951 | [ 952 | [-9000, 2000], 953 | [-8000, -3000], 954 | [-2000, -2000], 955 | [2000, 2000], 956 | [3000, 1500], 957 | [4000, 5000], 958 | [10000, 1400], 959 | [11000, 20000], 960 | [12000, 30000], 961 | ], 962 | { 963 | width: 60, 964 | height: 20, 965 | }, 966 | ); 967 | ``` 968 | 969 | Expected Output: 970 | 971 | ```bash 972 | ▲ 973 | 30k┤ ┏━ 974 | │ ┃ 975 | │ ┃ 976 | │ ┃ 977 | │ ┃ 978 | │ ┃ 979 | 20k┤ ┏━━┛ 980 | │ ┃ 981 | │ ┃ 982 | │ ┃ 983 | │ ┃ 984 | │ ┃ 985 | │ ┃ 986 | │ ┃ 987 | 5k┤ ┏━━━━━━━━━━━━━━━┓ ┃ 988 | │ ┃ ┃ ┃ 989 | 1.4k┤━━┓ ┏━━━━━┛ ┗━━┛ 990 | │ ┃ ┃ 991 | -2k┤ ┃ ┏━━━━━━━━━━┛ 992 | -3k┤ ┗━━━━━━━━━━━━━━━━┛ 993 | └┬──┬────────────────┬──────────┬──┬──┬───────────────┬──┬──┬▶ 994 | -8k 2k 4k 11k 995 | -9k -2k 3k 10k 12k 996 | ``` 997 | 998 | ### Plot with custom line format 999 | 1000 | Input: 1001 | 1002 | ```typescript 1003 | plot( 1004 | [ 1005 | [1, 0], 1006 | [2, 20], 1007 | [3, 29], 1008 | [4, 10], 1009 | [5, 3], 1010 | [6, 40], 1011 | [7, 0], 1012 | [8, 20], 1013 | ], 1014 | { 1015 | height: 10, 1016 | width: 30, 1017 | lineFormatter: ({ y, plotX, plotY, input, index }) => { 1018 | const output = [{ x: plotX, y: plotY, symbol: '█' }]; 1019 | 1020 | if (input[index - 1]?.[1] < y) { 1021 | return [...output, { x: plotX, y: plotY - 1, symbol: '▲' }]; 1022 | } 1023 | 1024 | return [...output, { x: plotX, y: plotY + 1, symbol: '▼' }]; 1025 | }, 1026 | }, 1027 | ); 1028 | ``` 1029 | 1030 | Expected Output: 1031 | 1032 | ```bash 1033 | ▲ ▲ 1034 | 40┤ █ 1035 | │ ▲ 1036 | 29┤ █ 1037 | │ ▲ ▲ 1038 | 20┤ █ █ 1039 | │ 1040 | │ 1041 | 10┤ █ 1042 | 3┤ ▼ █ 1043 | 0┤█ ▼ █ 1044 | └┬───┬───┬───┬────┬───┬───┬───┬▶ 1045 | 1 2 3 4 5 6 7 8 1046 | ``` 1047 | 1048 | ### Bar chart 1049 | 1050 | Input: 1051 | 1052 | ```typescript 1053 | plot( 1054 | [ 1055 | [0, 3], 1056 | [1, 2], 1057 | [2, 3], 1058 | [3, 4], 1059 | [4, -2], 1060 | [5, -5], 1061 | [6, 2], 1062 | [7, 0], 1063 | ], 1064 | { 1065 | title: 'bar chart with axis', 1066 | mode: 'bar', 1067 | showTickLabel: true, 1068 | width: 40, 1069 | axisCenter: [0, 0], 1070 | }, 1071 | ); 1072 | ``` 1073 | 1074 | Expected Output: 1075 | 1076 | ```bash 1077 | bar chart with axis 1078 | ▲ █ 1079 | 4┤ █ █ 1080 | 3┤ █ █ █ █ 1081 | 2┤ █ █ █ █ 1082 | 1┤ █ █ █ █ █ 1083 | 0┤─────┬────┬─────┬────┬─────┬────┬─────┬─▶ 1084 | -1┤ 1 2 3 4 5 6 7 1085 | -2┤ █ 1086 | -3┤ █ 1087 | -4┤ █ 1088 | -5┤ 1089 | │ 1090 | ``` 1091 | 1092 | ### Horizontal Bar chart 1093 | 1094 | Input: 1095 | 1096 | ```typescript 1097 | plot( 1098 | [ 1099 | [0, 3], 1100 | [1, 2], 1101 | [2, 3], 1102 | [3, 4], 1103 | [4, -2], 1104 | [5, -5], 1105 | [6, 2], 1106 | [7, 0], 1107 | ], 1108 | { 1109 | mode: 'horizontalBar', 1110 | showTickLabel: true, 1111 | width: 40, 1112 | height: 20, 1113 | axisCenter: [3, 1], 1114 | }, 1115 | ); 1116 | ``` 1117 | 1118 | Expected Output: 1119 | 1120 | ```bash 1121 | ▲ 1122 | 4┤ 1123 | │ 1124 | ████████████████3┤ 1125 | │ 1126 | ██████████2┤████████████████ 1127 | │ 1128 | ┬─────┬────┬────1┤────┬─────┬────┬─────┬─▶ 1129 | 0 1 2 3 4 5 6 7 1130 | 0┤██████████████████████ 1131 | │ 1132 | -1┤ 1133 | │ 1134 | -2┤ 1135 | │█████ 1136 | -3┤ 1137 | │ 1138 | -4┤ 1139 | │ 1140 | │ 1141 | -5┤███████████ 1142 | │ 1143 | ``` 1144 | -------------------------------------------------------------------------------- /__snapshots__/00_simple_example.txt: -------------------------------------------------------------------------------- 1 | 2 | simple example 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/01_small_numbers.txt: -------------------------------------------------------------------------------- 1 | 2 | small numbers 3 | ▲ 4 | │ ┏━ 5 | │ ┃ 6 | │ ┃ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┏━━━┓ ┏━━━┛ 11 | │ ┃ ┗━━┓ ┃ 12 | │━━━┛ ┃ ┃ 13 | │ ┗━━━┛ 14 | └┬───┬───┬──┬───┬───┬▶ 15 | 0.001 0.003 0.005 16 | 0.002 0.004 0.006 17 | -------------------------------------------------------------------------------- /__snapshots__/02_Tick_Label.txt: -------------------------------------------------------------------------------- 1 | 2 | Tick Label 3 | ▲ 4 | 116┤ 5 | 111┤ 6 | 107┤ 7 | 102┤ 8 | 98┤ 9 | 93┤ 10 | 89┤ 11 | 84┤ 12 | 80┤ 13 | 76┤ 14 | 71┤ 15 | 67┤ 16 | 62┤ 17 | 58┤ 18 | 53┤ 19 | 49┤ 20 | 44┤ 21 | 40┤ 22 | 36┤ 23 | 31┤ 24 | 27┤ 25 | 22┤ 26 | 18┤ 27 | 13┤ 28 | 9┤ ┏━━━━━━━━━━━━ 29 | 4┤ ┏━━━━━━━━━━┓ ┏━━━━━━━━━━┛ 30 | 0┤━━━━┛ ┗━━━━━━━━━━┛ 31 | └┬────┬─────┬────┬──────────┬──────────┬─────┬────┬▶ 32 | 1 2 3 4 6 8 9 10 33 | -------------------------------------------------------------------------------- /__snapshots__/03_Border.txt: -------------------------------------------------------------------------------- 1 | 2 | ███████████████████████████ 3 | █Border █ 4 | █ ▲ █ 5 | █ 11┤ ┏━ █ 6 | █ 9┤ ┏━━━┛ █ 7 | █ │ ┃ █ 8 | █ │ ┃ █ 9 | █ 4┤ ┏━┓ ┃ █ 10 | █y 2┤━━━┛ ┃ ┏━┓ ┃ █ 11 | █ 1┤ ┗━┓ ┃ ┃ ┃ █ 12 | █ -1┤ ┗━━┛ ┗━┛ █ 13 | █ └┬─┬─┬─┬─┬──┬─┬─┬─┬─┬▶█ 14 | █ 1 2 3 4 5 6 7 8 910 █ 15 | █ x █ 16 | ███████████████████████████ 17 | -------------------------------------------------------------------------------- /__snapshots__/04_Simple_chart.txt: -------------------------------------------------------------------------------- 1 | 2 | Simple chart 3 | ▲ 4 | │ 5 | │ 6 | │ 7 | │ 8 | │ 9 | └──────────▶ 10 | -------------------------------------------------------------------------------- /__snapshots__/05_bar_chart.txt: -------------------------------------------------------------------------------- 1 | 2 | bar chart 3 | ▲ █ 4 | 4┤ █ 5 | │ █ 6 | │ █ █ 7 | 3┤ █ █ 8 | │ █ █ 9 | │█ █ █ 10 | 2┤█ █ █ 11 | │█ █ █ 12 | │█ █ █ █ 13 | 1┤█ █ █ █ 14 | └┬──┬──┬──┬▶ 15 | 1 2 3 4 16 | -------------------------------------------------------------------------------- /__snapshots__/06_horizontal_bar_chart.txt: -------------------------------------------------------------------------------- 1 | 2 | horizontal bar chart 3 | ▲ 4 | 4┤███████████████ 5 | │ 6 | │ 7 | 3┤███████████ 8 | │ 9 | │ 10 | 2┤ 11 | │ 12 | │ 13 | 1┤███████████████████ 14 | └┬──────────┬───┬───┬▶ 15 | -1 2 3 4 16 | -------------------------------------------------------------------------------- /__snapshots__/07_area.txt: -------------------------------------------------------------------------------- 1 | 2 | area 3 | ▲ 4 | 4┤ ███████ 5 | │ ███████ 6 | │ ███████ 7 | 3┤ ██████████████ 8 | │ ██████████████ 9 | │ ██████████████ 10 | 2┤███████████████████ 11 | │███████████████████ 12 | │███████████████████ 13 | 1┤████████████████████ 14 | └┬─────┬──────┬─────┬▶ 15 | 1 2 3 4 16 | -------------------------------------------------------------------------------- /__snapshots__/08_labels.txt: -------------------------------------------------------------------------------- 1 | 2 | labels 3 | ▲ 4 | 4┤ ┏━━━━━┓ 5 | │ ┃ ┃ 6 | │ ┃ ┃ 7 | 3┤ ┏━━━━━━┛ ┃ 8 | │ ┃ ┃ 9 | │ ┃ ┃ 10 | y2┤━━━━━┛ ┃ 11 | │ ┃ 12 | │ ┃ 13 | 1┤ ┗━ 14 | └┬─────┬──────┬─────┬▶ 15 | 1 2 3 4 16 | x 17 | -------------------------------------------------------------------------------- /__snapshots__/09_legend.txt: -------------------------------------------------------------------------------- 1 | 2 | legend 3 | ▲ 4 | 4┤ ┏━━━━━┓ 5 | 3┤ ┏━━━━━━┏━━━━━┓ 6 | │ ┃ ┃ ┃ 7 | 2┤━━━━━┛ ┃ ┃ 8 | 1┤ ┃ ┃━ 9 | 0┤ ┃ ┗━ 10 | y │ ┃ 11 | │ ┃ 12 | -2┤━━━━━┓ ┃ 13 | -3┤ ┗━━━━━━┛ 14 | └┬─────┬──────┬─────┬▶ 15 | 1 2 3 4 16 | x 17 | █ first 18 | █ second 19 | -------------------------------------------------------------------------------- /__snapshots__/10_multiline.txt: -------------------------------------------------------------------------------- 1 | 2 | multiline 3 | ▲ 4 | 4┤ ┏━━━━━━━━━━━┓ 5 | 3┤ ┏━━━━━━━━━━━━┏━━━━━━━━━━━┓ ┏━ 6 | 2┤━━━━━━━━━━━┛ ┃ ┃ ┃ 7 | 1┤ ┃ ┃━ ┃ 8 | 0┤ ┃ ┗━━━━━━━━━━━┛ 9 | │ ┃ 10 | -2┤━━━━━━━━━━━┓ ┃ 11 | -3┤ ┗━━━━━━━━━━━━┛ 12 | │ ┃ 13 | │ ┃ 14 | -6┤━━━━━━━━━━━┛ 15 | └┬───────────┬────────────┬───────────┬───────────┬▶ 16 | 1 2 3 4 5 17 | -------------------------------------------------------------------------------- /__snapshots__/11_multiline_points.txt: -------------------------------------------------------------------------------- 1 | 2 | multiline points 3 | ▲ 4 | 4┤ ● 5 | 3┤ ● ● ● 6 | 2┤● 7 | 1┤ ● 8 | 0┤ ● 9 | │ 10 | -2┤● 11 | -3┤ ● 12 | │ 13 | │ 14 | -6┤● 15 | └┬───────────┬────────────┬───────────┬───────────┬▶ 16 | 1 2 3 4 5 17 | -------------------------------------------------------------------------------- /__snapshots__/12_yRange.txt: -------------------------------------------------------------------------------- 1 | 2 | yRange 3 | ▲ 4 | 3┤ ┏━ 5 | │ ┃ 6 | │ ┃ 7 | │ ┃ 8 | 2┤━━━━━━━━━━━━━━━━━━┛ 9 | │ 10 | │ 11 | │ 12 | │ 13 | │ 14 | └┬──────────────────┬▶ 15 | 1 2 16 | -------------------------------------------------------------------------------- /__snapshots__/13_yRange.txt: -------------------------------------------------------------------------------- 1 | 2 | yRange 3 | ▲ 4 | │ 5 | │ 6 | 4┤ ┏━━━━━┓ 7 | │ ┃ ┃ 8 | 3┤ ┏━━━━━━┛ ┃ 9 | 2┤━━━━━┛ ┃ 10 | │ ┃ 11 | 1┤ ┗━ 12 | │ 13 | │ 14 | └┬─────┬──────┬─────┬▶ 15 | 1 2 3 4 16 | -------------------------------------------------------------------------------- /__snapshots__/14_showTickLabel.txt: -------------------------------------------------------------------------------- 1 | 2 | showTickLabel 3 | ▲ 4 | 10┤ ┏━ 5 | 9┤ ┃ 6 | 8┤ ┃ 7 | 7┤ ┃ 8 | 6┤ ┃ 9 | 5┤ ┏━━━┛ 10 | 4┤ ┏━━┓ ┃ 11 | 3┤ ┏━━━┛ ┃ ┃ 12 | 2┤━━━┛ ┃ ┃ 13 | 1┤ ┗━━━┛ 14 | └┬───┬───┬──┬───┬───┬▶ 15 | 1 2 3 4 5 6 16 | -------------------------------------------------------------------------------- /__snapshots__/15_hideXAxis.txt: -------------------------------------------------------------------------------- 1 | 2 | hideXAxis 3 | ▲ 4 | 10┤ ┏━ 5 | │ ┃ 6 | │ ┃ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | 5┤ ┏━━━┛ 11 | │ ┃ 12 | 3┤ ┏━━━━━━━━━━┛ 13 | 2┤━━━┛ 14 | │ 15 | -------------------------------------------------------------------------------- /__snapshots__/16_hideYAxis.txt: -------------------------------------------------------------------------------- 1 | 2 | hideYAxis 3 | ┏━ 4 | ┃ 5 | ┃ 6 | ┃ 7 | ┃ 8 | ┃ 9 | ┏━━━┛ 10 | ┃ 11 | ┏━━━━━━━━━━┛ 12 | ━━━┛ 13 | ─┬───┬──────────┬───┬▶ 14 | 1 2 5 6 15 | -------------------------------------------------------------------------------- /__snapshots__/17_axisCenter.txt: -------------------------------------------------------------------------------- 1 | 2 | axisCenter 3 | ▲ 4 | 5┤ ┏━━┓ 5 | 4┤ ┃ ┃ 6 | 3┤ ┃ ┃ 7 | 2┤ ┏━━━━━━━┛ ┃ 8 | ━━│━━━━┛ ┃ 9 | 1┤ ┃ 10 | ┬─0┼─┬──┬───────┬──┬─▶ 11 | -1-1┤ 1 2 5 6 12 | │ ┃ 13 | -2┤ ┗━ 14 | │ 15 | -------------------------------------------------------------------------------- /__snapshots__/18_lineFormatter.txt: -------------------------------------------------------------------------------- 1 | 2 | lineFormatter 3 | ▲ 4 | 5┤█████████████████ 5 | │ 6 | │ 7 | 3┤█████████ 8 | 2┤██████ 9 | │ 10 | │ 11 | │ 12 | │ 13 | -2┤████████████████████ 14 | └┬────┬──┬───────┬──┬▶ 15 | -1 1 2 5 6 16 | -------------------------------------------------------------------------------- /__snapshots__/19_lineFormatter.txt: -------------------------------------------------------------------------------- 1 | 2 | lineFormatter 3 | ▲ 4 | 5┤ █ 5 | │ █ 6 | │ █ 7 | 3┤ █ █ 8 | 2┤█ █ █ █ 9 | │█ █ █ █ 10 | │█ █ █ █ 11 | │█ █ █ █ 12 | │█ █ █ █ 13 | -2┤█ █ █ █ █ 14 | └┬────┬──┬───────┬──┬▶ 15 | -1 1 2 5 6 16 | -------------------------------------------------------------------------------- /__snapshots__/20_symbols.txt: -------------------------------------------------------------------------------- 1 | 2 | AAAAAAAAAAAAAAAAAAAAAAAAAA 3 | Asymbols█████████████████A 4 | A██▲BBBBBBBBBBBBBBBBBBBBBA 5 | A█5┤BBBBBBBBBBBBBBB┏━━┓BBA 6 | A██│BBBBBBBBBBBBBBB┃BB┃BBA 7 | A██│BBBBBBBBBBBBBBB┃BB┃BBA 8 | A█3┤BBBBBBB┏━━━━━━━┛BB┃BBA 9 | A█2┤━━━━━━━┛BBBBBBBBBB┃BBA 10 | A██│BBBBBBBBBBBBBBBBBB┃BBA 11 | A██│BBBBBBBBBBBBBBBBBB┃BBA 12 | A██│BBBBBBBBBBBBBBBBBB┃BBA 13 | A██│BBBBBBBBBBBBBBBBBB┃BBA 14 | A-2┤BBBBBBBBBBBBBBBBBB┗━BA 15 | A██└┬────┬──┬───────┬──┬▶A 16 | A██-1████1██2███████5██6█A 17 | AAAAAAAAAAAAAAAAAAAAAAAAAA 18 | -------------------------------------------------------------------------------- /__snapshots__/21_colors.txt: -------------------------------------------------------------------------------- 1 | 2 | colors 3 | ▲ 4 | 4┤ ┏━━━━━┓ 5 | │ ┃ ┃ 6 | │ ┃ ┃ 7 | 3┤ ┏━━━━━━┛ ┃ 8 | │ ┃ ┃ 9 | │ ┃ ┃ 10 | 2┤━━━━━┛ ┃ 11 | │ ┃ 12 | │ ┃ 13 | 1┤ ┗━ 14 | └┬─────┬──────┬─────┬▶ 15 | 1 2 3 4 16 | -------------------------------------------------------------------------------- /__snapshots__/22_custom_y_axis.txt: -------------------------------------------------------------------------------- 1 | 2 | custom y axis 3 | ▲ 4 | │ ┏━ 5 | │ ┏━━━┛ 6 | │ ┃ 7 | │ ┃ 8 | 6┤ ┃ 9 | 4┤ ┏━┓ ┃ 10 | │ ┏━┛ ┃ ┏━┓ ┃ 11 | 2┤━┛ ┗━┓ ┃ ┃ ┃ 12 | 0┤ ┃ ┃ ┃ ┃ 13 | │ ┗━━┛ ┗━┛ 14 | └──┬───┬────┬────────▶ 15 | 2 4 6 16 | -------------------------------------------------------------------------------- /__snapshots__/23_custom_ticks_and_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | custom ticks and axis center 3 | ▲ 4 | │ ┏━ 5 | │ ┏━━━┛ 6 | │ ┃ 7 | │ ┃ 8 | 6┤ ┃ 9 | 4┤━┓ ┃ 10 | ──┬─┼─┬────┬─────────▶ 11 | ━22┤ 4━┓ 6 ┃ ┃ 12 | 0┤ ┃ ┃ ┃ ┃ 13 | │ ┗━━┛ ┗━┛ 14 | │ 15 | -------------------------------------------------------------------------------- /__snapshots__/24_custom_ticks_and_axis_center_hide_x_axis.txt: -------------------------------------------------------------------------------- 1 | 2 | custom ticks and axis center, hide x axis 3 | ▲ 4 | │ ┏━ 5 | │ ┏━━━┛ 6 | │ ┃ 7 | │ ┃ 8 | 6┤ ┃ 9 | 4┤━┓ ┃ 10 | ┏━│ ┃ ┏━┓ ┃ 11 | ━┛2┤ ┗━┓ ┃ ┃ ┃ 12 | 0┤ ┃ ┃ ┃ ┃ 13 | │ ┗━━┛ ┗━┛ 14 | │ 15 | -------------------------------------------------------------------------------- /__snapshots__/25_custom_ticks_and_axis_center_hide_y_axis.txt: -------------------------------------------------------------------------------- 1 | 2 | custom ticks and axis center, hide y axis 3 | ┏━ 4 | ┏━━━┛ 5 | ┃ 6 | ┃ 7 | ┃ 8 | ┏━┓ ┃ 9 | ──┬─└─┬────┬─────────▶ 10 | ━2 4━┓ 6 ┃ ┃ 11 | ┃ ┃ ┃ ┃ 12 | ┗━━┛ ┗━┛ 13 | -------------------------------------------------------------------------------- /__snapshots__/26_custom_ticks_and_axis_center_hide_y_axis_show_tick_label.txt: -------------------------------------------------------------------------------- 1 | 2 | custom ticks and axis center, hide y axis, show tick label 3 | ▲ 4 | 10┤ ┏━ 5 | 9┤ ┏━━━┛ 6 | │ ┃ 7 | 6┤ ┃ 8 | 5┤ ┃ 9 | 4┤━┓ ┃ 10 | ┏3┤ ┃ ┏━┓ ┃ 11 | ━┛1┤ ┗━┓ ┃ ┃ ┃ 12 | 0┤ ┃ ┃ ┃ ┃ 13 | -1┤ ┗━━┛ ┗━┛ 14 | │ 15 | -------------------------------------------------------------------------------- /__snapshots__/27_colors_with_legend.txt: -------------------------------------------------------------------------------- 1 | 2 | colors with legend 3 | ▲ ┃ 4 | 4┤ ┃ ┏━━━━━┏━ 5 | │ ┃ ┃ ┃ 6 | 3┤━━━━━┓┃━━━━━┛ ┃ 7 | │ ┃┃ ┃ 8 | 2┤━━━━━━━━━━━━━━━━━━━━━ 9 | │ ┃┃ ┃ 10 | │ ┃┃ ┃ 11 | 1┤ ┗┃━━━━━┓ ┃━ 12 | │ ┃ ┃ ┃ 13 | 0┤ ┃ ┗━━━━━┛ 14 | └┬─────┬──────┬─────┬▶ 15 | 1 2 3 4 16 | █ first 17 | █ second 18 | -------------------------------------------------------------------------------- /__snapshots__/28_thresholds.txt: -------------------------------------------------------------------------------- 1 | 2 | thresholds 3 | ▲ ┃ 4 | 9┤ ┃ ┏━ 5 | │ ┃ ┃ 6 | │ ┃ ┃ 7 | │ ┃ ┃ 8 | │━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9 | 4┤ ┏━━━━━━━━━━┓ ┃ ┃ 10 | 3┤ ┃ ┃ ┃ ┏━━━━┓ ┃ 11 | 2┤━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12 | 1┤━━━━━┛ ┃┃ ┃ ┃ ┃ 13 | │ ┃┃ ┃ ┃ ┃ 14 | -1┤ ┗┃━━━━┛ ┗━━━━━┛ 15 | └┬─────┬────┬─────┬────┬─────┬────┬─────┬▶ 16 | 1 2 3 4 5 6 7 8 17 | -------------------------------------------------------------------------------- /__snapshots__/29_thresholds.txt: -------------------------------------------------------------------------------- 1 | 2 | thresholds 3 | ▲ Y 4 | 9┤ Y ┏━ 5 | │ Y ┃ 6 | │ Y ┃ 7 | │ Y ┃ 8 | │XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 9 | 4┤ ┏━━━━━━━━━━┓ Y ┃ 10 | 3┤ ┃ ┃ Y ┏━━━━┓ ┃ 11 | 2┤XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 12 | 1┤━━━━━┛ ┃Y ┃ ┃ ┃ 13 | │ ┃Y ┃ ┃ ┃ 14 | -1┤ ┗Y━━━━┛ ┗━━━━━┛ 15 | └┬─────┬────┬─────┬────┬─────┬────┬─────┬▶ 16 | 1 2 3 4 5 6 7 8 17 | -------------------------------------------------------------------------------- /__snapshots__/30_with_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | with axis center 3 | ▲ 4 | 4┤ ┏━━━━┓ 5 | 3┤━━━━━┓ ┏━━━━━┛ ┃ 6 | 2├─────┬────┬─────┬────┬─────┬────┬─────┬─▶ 7 | 10 1 2 3 4 5 6 7 8 | 0┤ ┃ ┃ ┗━ 9 | -1┤ ┃ ┃ 10 | -2┤ ┗━━━━━┓ ┃ 11 | -3┤ ┃ ┃ 12 | -4┤ ┃ ┃ 13 | -5┤ ┗━━━━┛ 14 | │ 15 | -------------------------------------------------------------------------------- /__snapshots__/31_bar_chart_with_colors.txt: -------------------------------------------------------------------------------- 1 | 2 | bar chart with colors 3 | ▲ █ 4 | 4┤ █ █ 5 | 3┤ █ █ █ █ 6 | 2┤ █ █ █ █ 7 | 1┤ █ █ █ █ █ 8 | 0├─────┬────┬─────┬────┬─────┬────┬─────┬─▶ 9 | -10 1 2 3 4 5 6 7 10 | -2┤ █ 11 | -3┤ █ 12 | -4┤ █ 13 | -5┤ 14 | │ 15 | -------------------------------------------------------------------------------- /__snapshots__/32_horizontal_bar_chart_with_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | horizontal bar chart with axis center 3 | ▲ 4 | 4┤ 5 | │ 6 | ████████████████3┤ 7 | │ 8 | ██████████2┤████████████████ 9 | │ 10 | ┬─────┬────┬────1┼────┬─────┬────┬─────┬─▶ 11 | 0 1 2 3 4 5 6 7 12 | 0┤██████████████████████ 13 | │ 14 | -1┤ 15 | │ 16 | -2┤ 17 | │█████ 18 | -3┤ 19 | │ 20 | -4┤ 21 | │ 22 | │ 23 | -5┤███████████ 24 | │ 25 | -------------------------------------------------------------------------------- /__snapshots__/33_horizontal_bar_chart.txt: -------------------------------------------------------------------------------- 1 | 2 | horizontal bar chart 3 | ▲ 4 | 26┤███████████████████ 5 | 23┤ 6 | 20┤ 7 | 17┤██████████ 8 | 15┤ 9 | 12┤ 10 | 9┤ 11 | 6┤ 12 | 3┤ 13 | 0┤ 14 | └┬─────────┬────────┬▶ 15 | 1 2 3 16 | -------------------------------------------------------------------------------- /__snapshots__/34_bar_chart.txt: -------------------------------------------------------------------------------- 1 | 2 | bar chart 3 | ▲ █ 4 | 26┤ █ 5 | 23┤ █ 6 | 20┤ █ █ 7 | 17┤ █ █ 8 | 15┤ █ █ 9 | 12┤ █ █ 10 | 9┤ █ █ 11 | 6┤ █ █ 12 | 3┤█ █ █ 13 | 0┤█ █ █ 14 | └┬─────────┬────────┬▶ 15 | 1 2 3 16 | -------------------------------------------------------------------------------- /__snapshots__/35_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | axis center 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏━┛ 6 | 9┤ ┏━┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | ┬─┬─┼─┬─┬──┬─┬─┬─┬─┬─▶ 11 | 1 243━4 5 6 7 8 910 12 | ┏3┤ ┃ ┏━┓ ┃ 13 | ━┛2┤ ┃ ┃ ┃ ┃ 14 | 1┤ ┗━┓ ┃ ┃ ┃ 15 | │ ┃ ┃ ┃ ┃ 16 | -1┤ ┗━━┛ ┗━┛ 17 | │ 18 | -------------------------------------------------------------------------------- /__snapshots__/36_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | axis center 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | ┬┬┼┬┬─┬┬┬┬┬─▶ 11 | 14345 67810 12 | 3┤┃ ┏┓┃ 13 | 2┤┃ ┃┃┃ 14 | 1┤┗┓ ┃┃┃ 15 | │ ┃ ┃┃┃ 16 | -1┤ ┗━┛┗┛ 17 | │ 18 | -------------------------------------------------------------------------------- /__snapshots__/37_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | axis center 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤ ┏┛┃ ┏┓┃ 13 | 2┤ ┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | │ 18 | │ 19 | │ 20 | │ 21 | │ 22 | │ 23 | │ 24 | │ 25 | ├┬┬┬┬┬┬┬┬┬┬─▶ 26 | │1234567810 27 | -------------------------------------------------------------------------------- /__snapshots__/38_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | axis center 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤ ┏┛┃ ┏┓┃ 13 | 2┤ ┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | ├┬┬┬┬┬┬┬┬┬┬─▶ 16 | -1┤1234567810 17 | │ 18 | -------------------------------------------------------------------------------- /__snapshots__/39_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | axis center 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤ ┏┛┃ ┏┓┃ 13 | 2┤ ┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | ├┬┬┬┬┬┬┬┬┬┬─▶ 16 | -1┤1234567810 17 | │ 18 | -------------------------------------------------------------------------------- /__snapshots__/40_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | axis center 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤ ┏┛┃ ┏┓┃ 13 | 2┤ ┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | ├┬┬┬┬┬┬┬┬┬┬─▶ 16 | -1┤1234567810 17 | │ 18 | -------------------------------------------------------------------------------- /__snapshots__/40_draws_two_complicated_graphs_with_moved_axis.txt: -------------------------------------------------------------------------------- 1 | 2 | draws two complicated graphs with moved axis 3 | ▲ 4 | 8┤ ┏━ 5 | │ ┃ 6 | │ ┃ 7 | │ ┃ 8 | │ ┃ 9 | 4┤ ┏━━━━━━━━━━━━━━━━━━━━━━━━┛ 10 | 3┤ ┏━━━━━┛ 11 | │ ┃ 12 | 1┤ ┏━━━━━━━━━━━┛ 13 | ┬────────────────────────┬─────┬─────┬─────┬─────0┬─────┬───────────┬─────┬────────────────────────┬─▶ 14 | -8 -4 -3 -2 -1 0 1 3 4 8 15 | ┏━━━━-1┤ 16 | ┏━━━━━┛ -2┤ 17 | ┏━━━━━┛ -3┤ 18 | ┏━━━━━┛ -4┤ 19 | ┃ │ 20 | ┃ │ 21 | ┃ │ 22 | ┃ │ 23 | ━━━━━━━━━━━━━━━━━━━━━━━━┛ -8┤ 24 | │ 25 | -------------------------------------------------------------------------------- /__snapshots__/41_Small_and_big_values.txt: -------------------------------------------------------------------------------- 1 | 2 | Small and big values 3 | ▲ 4 | 80┤ ┏━ 5 | │ ┃ 6 | │ ┃ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | │ ┃ 12 | 8┤━━┓ ┃ 13 | 4┤ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 14 | └┬──┬───────────────────────────────────────────────────────┬▶ 15 | -8 -4 80 16 | -------------------------------------------------------------------------------- /__snapshots__/41_showTickLabel_true.txt: -------------------------------------------------------------------------------- 1 | 2 | showTickLabel = true 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | 8┤ ┃ 8 | 7┤ ┃ 9 | 6┤ ┃ 10 | 5┤ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | 0┤ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/42_showTickLabel_false.txt: -------------------------------------------------------------------------------- 1 | 2 | showTickLabel = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/42_showTickLabel_true.txt: -------------------------------------------------------------------------------- 1 | 2 | showTickLabel = true 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | 8┤ ┃ 8 | 7┤ ┃ 9 | 6┤ ┃ 10 | 5┤ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | 0┤ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/43_hideXAxis_true.txt: -------------------------------------------------------------------------------- 1 | 2 | hideXAxis = true 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | │ 18 | -------------------------------------------------------------------------------- /__snapshots__/43_showTickLabel_false.txt: -------------------------------------------------------------------------------- 1 | 2 | showTickLabel = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/44_hideXAxis_false.txt: -------------------------------------------------------------------------------- 1 | 2 | hideXAxis = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/44_hideXAxis_true.txt: -------------------------------------------------------------------------------- 1 | 2 | hideXAxis = true 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | │ 18 | -------------------------------------------------------------------------------- /__snapshots__/45_hideXAxisTicks_true.txt: -------------------------------------------------------------------------------- 1 | 2 | hideXAxisTicks = true 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └──────────▶ 18 | -------------------------------------------------------------------------------- /__snapshots__/45_hideXAxis_false.txt: -------------------------------------------------------------------------------- 1 | 2 | hideXAxis = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/46_hideXAxisTicks_false.txt: -------------------------------------------------------------------------------- 1 | 2 | hideXAxisTicks = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/46_hideXAxisTicks_true.txt: -------------------------------------------------------------------------------- 1 | 2 | hideXAxisTicks = true 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └──────────▶ 18 | -------------------------------------------------------------------------------- /__snapshots__/47_hideXAxisTicks_false.txt: -------------------------------------------------------------------------------- 1 | 2 | hideXAxisTicks = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/47_hideYAxis_true.txt: -------------------------------------------------------------------------------- 1 | 2 | hideYAxis = true 3 | ┏━ 4 | ┏┛ 5 | ┏┛ 6 | ┃ 7 | ┃ 8 | ┃ 9 | ┃ 10 | ┏┓ ┃ 11 | ┏┛┃ ┏┓┃ 12 | ┛ ┃ ┃┃┃ 13 | ┗┓┃┃┃ 14 | ┃┃┃┃ 15 | ┗┛┗┛ 16 | ─┬┬┬┬┬┬┬┬┬┬▶ 17 | 123456789 18 | 10 19 | -------------------------------------------------------------------------------- /__snapshots__/48_hideYAxis_false.txt: -------------------------------------------------------------------------------- 1 | 2 | hideYAxis = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/48_hideYAxis_true.txt: -------------------------------------------------------------------------------- 1 | 2 | hideYAxis = true 3 | ┏━ 4 | ┏┛ 5 | ┏┛ 6 | ┃ 7 | ┃ 8 | ┃ 9 | ┃ 10 | ┏┓ ┃ 11 | ┏┛┃ ┏┓┃ 12 | ┛ ┃ ┃┃┃ 13 | ┗┓┃┃┃ 14 | ┃┃┃┃ 15 | ┗┛┗┛ 16 | ─┬┬┬┬┬┬┬┬┬┬▶ 17 | 123456789 18 | 10 19 | -------------------------------------------------------------------------------- /__snapshots__/49_hideYAxisTicks_true.txt: -------------------------------------------------------------------------------- 1 | 2 | hideYAxisTicks = true 3 | ▲ 4 | │ ┏━ 5 | │ ┏┛ 6 | │ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | │ ┏┓ ┃ 12 | │┏┛┃ ┏┓┃ 13 | │┛ ┃ ┃┃┃ 14 | │ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | │ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/49_hideYAxis_false.txt: -------------------------------------------------------------------------------- 1 | 2 | hideYAxis = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/50_hideYAxisTicks_false.txt: -------------------------------------------------------------------------------- 1 | 2 | hideYAxisTicks = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/50_hideYAxisTicks_true.txt: -------------------------------------------------------------------------------- 1 | 2 | hideYAxisTicks = true 3 | ▲ 4 | │ ┏━ 5 | │ ┏┛ 6 | │ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | │ ┏┓ ┃ 12 | │┏┛┃ ┏┓┃ 13 | │┛ ┃ ┃┃┃ 14 | │ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | │ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/51_fillArea_true.txt: -------------------------------------------------------------------------------- 1 | 2 | fillArea = true 3 | ▲ 4 | 11┤ ██ 5 | 10┤ ███ 6 | 9┤ ████ 7 | │ ████ 8 | │ ████ 9 | │ ████ 10 | │ ████ 11 | 4┤ ██ ████ 12 | 3┤███ ██████ 13 | 2┤███ ██████ 14 | 1┤██████████ 15 | │██████████ 16 | -1┤██████████ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/51_hideYAxisTicks_false.txt: -------------------------------------------------------------------------------- 1 | 2 | hideYAxisTicks = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/52_fillArea_false.txt: -------------------------------------------------------------------------------- 1 | 2 | fillArea = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/52_fillArea_true.txt: -------------------------------------------------------------------------------- 1 | 2 | fillArea = true 3 | ▲ 4 | 11┤ ██ 5 | 10┤ ███ 6 | 9┤ ████ 7 | │ ████ 8 | │ ████ 9 | │ ████ 10 | │ ████ 11 | 4┤ ██ ████ 12 | 3┤███ ██████ 13 | 2┤███ ██████ 14 | 1┤██████████ 15 | │██████████ 16 | -1┤██████████ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/53_fillArea_false.txt: -------------------------------------------------------------------------------- 1 | 2 | fillArea = false 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/53_width_10.txt: -------------------------------------------------------------------------------- 1 | 2 | width = 10 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/54_width_10.txt: -------------------------------------------------------------------------------- 1 | 2 | width = 10 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/54_width_50.txt: -------------------------------------------------------------------------------- 1 | 2 | width = 50 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏━━━━┛ 6 | 9┤ ┏━━━━━┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏━━━━┓ ┃ 12 | 3┤ ┏━━━━━┛ ┃ ┏━━━━━┓ ┃ 13 | 2┤━━━━┛ ┃ ┃ ┃ ┃ 14 | 1┤ ┗━━━━━┓ ┃ ┃ ┃ 15 | │ ┃ ┃ ┃ ┃ 16 | -1┤ ┗━━━━┛ ┗━━━━┛ 17 | └┬────┬─────┬────┬─────┬────┬─────┬────┬─────┬────┬▶ 18 | 1 2 3 4 5 6 7 8 9 10 19 | -------------------------------------------------------------------------------- /__snapshots__/55_height_10.txt: -------------------------------------------------------------------------------- 1 | 2 | height = 10 3 | ▲ 4 | 11┤ ┏━ 5 | 9┤ ┏━┛ 6 | │ ┃ 7 | │ ┃ 8 | │ ┃ 9 | 4┤ ┏┓ ┃ 10 | 3┤┏┛┃ ┏┓┃ 11 | 2┤┛ ┗┓┃┃┃ 12 | │ ┃┃┃┃ 13 | -1┤ ┗┛┗┛ 14 | └┬┬┬┬┬┬┬┬┬┬▶ 15 | 123456789 16 | 10 17 | -------------------------------------------------------------------------------- /__snapshots__/55_width_50.txt: -------------------------------------------------------------------------------- 1 | 2 | width = 50 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏━━━━┛ 6 | 9┤ ┏━━━━━┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏━━━━┓ ┃ 12 | 3┤ ┏━━━━━┛ ┃ ┏━━━━━┓ ┃ 13 | 2┤━━━━┛ ┃ ┃ ┃ ┃ 14 | 1┤ ┗━━━━━┓ ┃ ┃ ┃ 15 | │ ┃ ┃ ┃ ┃ 16 | -1┤ ┗━━━━┛ ┗━━━━┛ 17 | └┬────┬─────┬────┬─────┬────┬─────┬────┬─────┬────┬▶ 18 | 1 2 3 4 5 6 7 8 9 10 19 | -------------------------------------------------------------------------------- /__snapshots__/56_height_10.txt: -------------------------------------------------------------------------------- 1 | 2 | height = 10 3 | ▲ 4 | 11┤ ┏━ 5 | 9┤ ┏━┛ 6 | │ ┃ 7 | │ ┃ 8 | │ ┃ 9 | 4┤ ┏┓ ┃ 10 | 3┤┏┛┃ ┏┓┃ 11 | 2┤┛ ┗┓┃┃┃ 12 | │ ┃┃┃┃ 13 | -1┤ ┗┛┗┛ 14 | └┬┬┬┬┬┬┬┬┬┬▶ 15 | 123456789 16 | 10 17 | -------------------------------------------------------------------------------- /__snapshots__/56_height_50.txt: -------------------------------------------------------------------------------- 1 | 2 | height = 50 3 | ▲ 4 | 11┤ ┏━ 5 | │ ┃ 6 | │ ┃ 7 | │ ┃ 8 | 10┤ ┏┛ 9 | │ ┃ 10 | │ ┃ 11 | │ ┃ 12 | 9┤ ┏┛ 13 | │ ┃ 14 | │ ┃ 15 | │ ┃ 16 | │ ┃ 17 | │ ┃ 18 | │ ┃ 19 | │ ┃ 20 | │ ┃ 21 | │ ┃ 22 | │ ┃ 23 | │ ┃ 24 | │ ┃ 25 | │ ┃ 26 | │ ┃ 27 | │ ┃ 28 | │ ┃ 29 | │ ┃ 30 | │ ┃ 31 | │ ┃ 32 | │ ┃ 33 | 4┤ ┏┓ ┃ 34 | │ ┃┃ ┃ 35 | │ ┃┃ ┃ 36 | │ ┃┃ ┃ 37 | 3┤┏┛┃ ┏┓┃ 38 | │┃ ┃ ┃┃┃ 39 | │┃ ┃ ┃┃┃ 40 | │┃ ┃ ┃┃┃ 41 | 2┤┛ ┃ ┃┃┃ 42 | │ ┃ ┃┃┃ 43 | │ ┃ ┃┃┃ 44 | │ ┃ ┃┃┃ 45 | 1┤ ┗┓┃┃┃ 46 | │ ┃┃┃┃ 47 | │ ┃┃┃┃ 48 | │ ┃┃┃┃ 49 | │ ┃┃┃┃ 50 | │ ┃┃┃┃ 51 | │ ┃┃┃┃ 52 | │ ┃┃┃┃ 53 | -1┤ ┗┛┗┛ 54 | └┬┬┬┬┬┬┬┬┬┬▶ 55 | 123456789 56 | 10 57 | -------------------------------------------------------------------------------- /__snapshots__/57_height_50.txt: -------------------------------------------------------------------------------- 1 | 2 | height = 50 3 | ▲ 4 | 11┤ ┏━ 5 | │ ┃ 6 | │ ┃ 7 | │ ┃ 8 | 10┤ ┏┛ 9 | │ ┃ 10 | │ ┃ 11 | │ ┃ 12 | 9┤ ┏┛ 13 | │ ┃ 14 | │ ┃ 15 | │ ┃ 16 | │ ┃ 17 | │ ┃ 18 | │ ┃ 19 | │ ┃ 20 | │ ┃ 21 | │ ┃ 22 | │ ┃ 23 | │ ┃ 24 | │ ┃ 25 | │ ┃ 26 | │ ┃ 27 | │ ┃ 28 | │ ┃ 29 | │ ┃ 30 | │ ┃ 31 | │ ┃ 32 | │ ┃ 33 | 4┤ ┏┓ ┃ 34 | │ ┃┃ ┃ 35 | │ ┃┃ ┃ 36 | │ ┃┃ ┃ 37 | 3┤┏┛┃ ┏┓┃ 38 | │┃ ┃ ┃┃┃ 39 | │┃ ┃ ┃┃┃ 40 | │┃ ┃ ┃┃┃ 41 | 2┤┛ ┃ ┃┃┃ 42 | │ ┃ ┃┃┃ 43 | │ ┃ ┃┃┃ 44 | │ ┃ ┃┃┃ 45 | 1┤ ┗┓┃┃┃ 46 | │ ┃┃┃┃ 47 | │ ┃┃┃┃ 48 | │ ┃┃┃┃ 49 | │ ┃┃┃┃ 50 | │ ┃┃┃┃ 51 | │ ┃┃┃┃ 52 | │ ┃┃┃┃ 53 | -1┤ ┗┛┗┛ 54 | └┬┬┬┬┬┬┬┬┬┬▶ 55 | 123456789 56 | 10 57 | -------------------------------------------------------------------------------- /__snapshots__/57_xLabel_label_.txt: -------------------------------------------------------------------------------- 1 | 2 | xLabel = "label" 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | label 21 | -------------------------------------------------------------------------------- /__snapshots__/58_xLabel_label_.txt: -------------------------------------------------------------------------------- 1 | 2 | xLabel = "label" 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤┏┛┃ ┏┓┃ 13 | 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | label 21 | -------------------------------------------------------------------------------- /__snapshots__/58_yLabel_label_.txt: -------------------------------------------------------------------------------- 1 | 2 | yLabel = "label" 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | l │ ┃ 10 | a │ ┃ 11 | b 4┤ ┏┓ ┃ 12 | e 3┤┏┛┃ ┏┓┃ 13 | l 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/59_yLabel_label_.txt: -------------------------------------------------------------------------------- 1 | 2 | yLabel = "label" 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┏┛ 7 | │ ┃ 8 | │ ┃ 9 | l │ ┃ 10 | a │ ┃ 11 | b 4┤ ┏┓ ┃ 12 | e 3┤┏┛┃ ┏┓┃ 13 | l 2┤┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | └┬┬┬┬┬┬┬┬┬┬▶ 18 | 123456789 19 | 10 20 | -------------------------------------------------------------------------------- /__snapshots__/59_yRange_0_10_.txt: -------------------------------------------------------------------------------- 1 | 2 | yRange = [0, 10] 3 | ▲ 4 | 10┤ ┏━ 5 | 9┤ ┏┛ 6 | │ ┃ 7 | │ ┃ 8 | │ ┃ 9 | 4┤ ┓ ┃ 10 | 3┤┏┃ ┏┛ 11 | 2┤┛┃ ┃ 12 | 1┤ ┗━┛ 13 | │ 14 | └┬┬┬─┬┬┬▶ 15 | 123 689 16 | 4 17 | -------------------------------------------------------------------------------- /__snapshots__/60_yRange_-5_5_.txt: -------------------------------------------------------------------------------- 1 | 2 | yRange = [-5, 5] 3 | ▲ 4 | 4┤ ┏┓ 5 | 2┤━┛┃ ┏┓ 6 | 1┤ ┗┓┃┃ 7 | -1┤ ┗┛┗━ 8 | │ 9 | │ 10 | └┬┬┬┬┬┬┬▶ 11 | 1234567 12 | -------------------------------------------------------------------------------- /__snapshots__/60_yRange_0_10_.txt: -------------------------------------------------------------------------------- 1 | 2 | yRange = [0, 10] 3 | ▲ 4 | 10┤ ┏━ 5 | 9┤ ┏┛ 6 | │ ┃ 7 | │ ┃ 8 | │ ┃ 9 | 4┤ ┓ ┃ 10 | 3┤┏┃ ┏┛ 11 | 2┤┛┃ ┃ 12 | 1┤ ┗━┛ 13 | │ 14 | └┬┬┬─┬┬┬▶ 15 | 123 689 16 | -------------------------------------------------------------------------------- /__snapshots__/61_axisCenter_0_10_.txt: -------------------------------------------------------------------------------- 1 | 2 | axisCenter = [0, 10] 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤┬┬┬┬┬┬┬┬┬┬─▶ 6 | 9┤1234567810 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤ ┏┛┃ ┏┓┃ 13 | 2┤ ┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | │ 18 | -------------------------------------------------------------------------------- /__snapshots__/61_yRange_-5_5_.txt: -------------------------------------------------------------------------------- 1 | 2 | yRange = [-5, 5] 3 | ▲ 4 | 4┤ ┏┓ 5 | 2┤━┛┃ ┏┓ 6 | 1┤ ┗┓┃┃ 7 | -1┤ ┗┛┗━ 8 | │ 9 | │ 10 | └┬┬┬┬┬┬┬▶ 11 | 1234567 12 | -------------------------------------------------------------------------------- /__snapshots__/62_axisCenter_-5_5_.txt: -------------------------------------------------------------------------------- 1 | 2 | axisCenter = [-5, 5] 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │───┬┬┬┬┬┬┬─▶ 11 | 4┤ 1346710 12 | 3┤ ┛┃┏┓┃ 13 | 2┤ ┛┃┃┃┃ 14 | 1┤ ┗┃┃┃ 15 | │ ┃┃┃ 16 | -1┤ ┛┗┛ 17 | │ 18 | -------------------------------------------------------------------------------- /__snapshots__/62_axisCenter_0_10_.txt: -------------------------------------------------------------------------------- 1 | 2 | axisCenter = [0, 10] 3 | ▲ 4 | 11┤ ┏━ 5 | 10├┬┬┬┬┬┬┬┬┬┬─▶ 6 | 9┤1234567810 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | 4┤ ┏┓ ┃ 12 | 3┤ ┏┛┃ ┏┓┃ 13 | 2┤ ┛ ┃ ┃┃┃ 14 | 1┤ ┗┓┃┃┃ 15 | │ ┃┃┃┃ 16 | -1┤ ┗┛┗┛ 17 | │ 18 | -------------------------------------------------------------------------------- /__snapshots__/63_axisCenter_-5_5_.txt: -------------------------------------------------------------------------------- 1 | 2 | axisCenter = [-5, 5] 3 | ▲ 4 | 11┤ ┏━ 5 | 10┤ ┏┛ 6 | 9┤ ┛ 7 | │ ┃ 8 | │ ┃ 9 | │ ┃ 10 | ├───┬┬┬┬┬┬┬─▶ 11 | 4┤ 1346710 12 | 3┤ ┛┃┏┓┃ 13 | 2┤ ┛┃┃┃┃ 14 | 1┤ ┗┃┃┃ 15 | │ ┃┃┃ 16 | -1┤ ┛┗┛ 17 | │ 18 | -------------------------------------------------------------------------------- /__snapshots__/64_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | axis center 3 | ▲ 4 | 11┤ ┏━ 5 | │ ┃ 6 | 10┤ ┏━┛ 7 | 9┤ ┏┛ 8 | │ ┃ 9 | │ ┃ 10 | │ ┃ 11 | │ ┃ 12 | │ ┃ 13 | ├─────────────────────────────────┬─┬─┬┬─┬┬─┬─┬┬─┬─▶ 14 | │ 1 2 34 56 7 8910 15 | 4┤ ┏┓ ┃ 16 | │ ┃┃ ┃ 17 | 3┤ ┏━┛┃ ┏━┓ ┃ 18 | 2┤ ━┛ ┃ ┃ ┃ ┃ 19 | │ ┃ ┃ ┃ ┃ 20 | 1┤ ┗━┓┃ ┃ ┃ 21 | │ ┃┃ ┃ ┃ 22 | │ ┃┃ ┃ ┃ 23 | -1┤ ┗┛ ┗━┛ 24 | │ 25 | -------------------------------------------------------------------------------- /__snapshots__/65_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | axis center 3 | ▲ 4 | ┏━ 11┤ 5 | ┃ │ 6 | ┏━┛ 10┤ 7 | ┏━━┛ 9┤ 8 | ┃ │ 9 | ┃ │ 10 | ┃ │ 11 | ┃ │ 12 | ┃ │ 13 | ┬──┬─┬──┬─┬──┬─┬──┬──┬─┬─────────────────────────┼─▶ 14 | 1 2 3 4 5 6 7 8 910 │ 15 | ┏━━┓ ┃ 4┤ 16 | ┃ ┃ ┃ │ 17 | ┏━┛ ┃ ┏━┓ ┃ 3┤ 18 | ━━┛ ┃ ┃ ┃ ┃ 2┤ 19 | ┃ ┃ ┃ ┃ │ 20 | ┗━┓ ┃ ┃ ┃ 1┤ 21 | ┃ ┃ ┃ ┃ │ 22 | ┃ ┃ ┃ ┃ │ 23 | ┗━━┛ ┗━━┛ -1┤ 24 | │ 25 | -------------------------------------------------------------------------------- /__snapshots__/66_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | axis center 3 | ▲ 4 | ├────┬────┬────┬────┬────┬───┬────┬────┬────┬────┬─▶ 5 | │ 1 2 3 4 5 6 7 8 9 10 6 | │ 7 | │ 8 | │ 9 | │ 10 | │ 11 | │ 12 | 11┤ ┏━ 13 | 10┤ ┏━━━━┛ 14 | 9┤ ┏━━━━┛ 15 | │ ┃ 16 | │ ┃ 17 | │ ┃ 18 | 4┤ ┏━━━━┓ ┃ 19 | 3┤ ┏━━━━┛ ┃ ┏━━━━┓ ┃ 20 | 2┤ ━━━━┛ ┃ ┃ ┃ ┃ 21 | 1┤ ┗━━━━┓ ┃ ┃ ┃ 22 | │ ┃ ┃ ┃ ┃ 23 | -1┤ ┗━━━┛ ┗━━━━┛ 24 | │ 25 | -------------------------------------------------------------------------------- /__snapshots__/67_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | axis center 3 | ▲ 4 | 11┤ ┏━ 5 | 9┤ ┏━━━━━━━━━┛ 6 | │ ┃ 7 | │ ┃ 8 | 4┤ ┏━━━━┓ ┃ 9 | 3┤ ┏━━━━┛ ┃ ┏━━━━┓ ┃ 10 | 2┤ ━━━━┛ ┗━━━━┓ ┃ ┃ ┃ 11 | -1┤ ┗━━━┛ ┗━━━━┛ 12 | │ 13 | │ 14 | │ 15 | │ 16 | │ 17 | │ 18 | │ 19 | │ 20 | │ 21 | │ 22 | │ 23 | ├────┬────┬────┬────┬────┬───┬────┬────┬────┬────┬─▶ 24 | │ 1 2 3 4 5 6 7 8 9 10 25 | -------------------------------------------------------------------------------- /__snapshots__/68_axis_center.txt: -------------------------------------------------------------------------------- 1 | 2 | axis center 3 | ▲ 4 | 11┤ ┏━ 5 | 9┤ ┏━━━━━━━━━┛ 6 | │ ┃ 7 | │ ┃ 8 | 4┤ ┏━━━━┓ ┃ 9 | 3┤ ┏━━━━┛ ┃ ┏━━━━┓ ┃ 10 | 2┤ ━━━━┛ ┗━━━━┓ ┃ ┃ ┃ 11 | -1┤ ┗━━━┛ ┗━━━━┛ 12 | │ 13 | │ 14 | │ 15 | │ 16 | │ 17 | │ 18 | │ 19 | │ 20 | │ 21 | │ 22 | │ 23 | │ 24 | │ 25 | -------------------------------------------------------------------------------- /__snapshots__/69_raws_two_complicated_graphs_with_moved_axis.txt: -------------------------------------------------------------------------------- 1 | 2 | raws two complicated graphs with moved axis 3 | ▲ 4 | 8┤ ┏━ 5 | │ ┃ 6 | │ ┃ 7 | │ ┃ 8 | │ ┃ 9 | 4┤ ┏━━━━━━━━━┛ 10 | 3┤ ┏━┛ 11 | 2┤ ┏━━┛ 12 | │ ┃ 13 | ┬─────────┬─┬──┬─┬─0┼───┬──┬─┬─────────┬─▶ 14 | -8 -4-3 -2-1 0 2 3 4 8 15 | ┏-1┤ 16 | ┏━┛-2┤ 17 | ┏━━┛ -3┤ 18 | ┏━┛ -4┤ 19 | ┃ │ 20 | ┃ │ 21 | ┃ │ 22 | ┃ │ 23 | ━━━━━━━━━┛ -8┤ 24 | │ 25 | -------------------------------------------------------------------------------- /__snapshots__/70_hide_axis.txt: -------------------------------------------------------------------------------- 1 | 2 | hide axis 3 | ┏━━━━━━━━━━━━━━┓ 4 | ┃ ┃ 5 | ┃ ┃ 6 | ┃ ┃ 7 | ┃ ┃ 8 | ┃ ┃ 9 | ━━━━━━━━━━━━━━┓ ┏━┓ ┃ ┃ 10 | ┃ ┃ ┃ ┃ ┗━ 11 | ┃ ┃ ┗━┛ 12 | ┗━━━━┛ 13 | -------------------------------------------------------------------------------- /__snapshots__/71_bar_chart_with_axis.txt: -------------------------------------------------------------------------------- 1 | 2 | bar chart with axis 3 | ▲ █ 4 | 4┤ █ █ 5 | 3┤ █ █ █ █ 6 | 2┤ █ █ █ █ 7 | 1┤ █ █ █ █ █ 8 | 0├─────┬────┬─────┬────┬─────┬────┬─────┬─▶ 9 | -10 1 2 3 4 5 6 7 10 | -2┤ █ 11 | -3┤ █ 12 | -4┤ █ 13 | -5┤ 14 | │ 15 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@typescript-eslint/eslint-plugin'; 2 | import parser from '@typescript-eslint/parser'; 3 | import importPlugin from 'eslint-plugin-import'; 4 | import prettierConfig from 'eslint-config-prettier'; 5 | import jsdoc from 'eslint-plugin-jsdoc'; 6 | 7 | export default [ 8 | { 9 | ignores: ['node_modules/**', 'dist/**', 'coverage/**'], 10 | }, 11 | prettierConfig, 12 | { 13 | files: ['**/*.ts'], 14 | languageOptions: { 15 | parser, 16 | ecmaVersion: 2021, 17 | sourceType: 'module', 18 | }, 19 | plugins: { 20 | '@typescript-eslint': typescript, 21 | import: importPlugin, 22 | jsdoc, 23 | }, 24 | rules: { 25 | 'import/extensions': 'off', 26 | 'no-param-reassign': 'off', 27 | 'no-unused-vars': 'off', 28 | '@typescript-eslint/no-unused-vars': [ 29 | 'warn', 30 | { 31 | vars: 'all', 32 | args: 'after-used', 33 | argsIgnorePattern: '^_', 34 | varsIgnorePattern: '^_', 35 | caughtErrors: 'none', 36 | }, 37 | ], 38 | 39 | 'jsdoc/require-jsdoc': [ 40 | 'warn', 41 | { 42 | require: { 43 | FunctionDeclaration: true, 44 | MethodDefinition: true, 45 | ClassDeclaration: true, 46 | ArrowFunctionExpression: false, 47 | FunctionExpression: false, 48 | }, 49 | }, 50 | ], 51 | 'jsdoc/require-param': 'warn', 52 | 'jsdoc/require-param-type': 'off', 53 | 'jsdoc/require-returns': 'warn', 54 | 'jsdoc/require-returns-type': 'off', 55 | }, 56 | }, 57 | { 58 | files: ['**/*.js'], 59 | languageOptions: { 60 | ecmaVersion: 2021, 61 | sourceType: 'module', 62 | }, 63 | plugins: { 64 | import: importPlugin, 65 | }, 66 | rules: { 67 | 'import/extensions': 'off', 68 | 'no-unused-vars': [ 69 | 'warn', 70 | { 71 | vars: 'all', 72 | args: 'after-used', 73 | argsIgnorePattern: '^_', 74 | varsIgnorePattern: '^_', 75 | caughtErrors: 'none', 76 | }, 77 | ], 78 | }, 79 | }, 80 | ]; 81 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | roots: ['/src'], 3 | testEnvironment: 'node', 4 | extensionsToTreatAsEsm: ['.ts'], 5 | collectCoverage: true, 6 | coverageReporters: ['text', 'cobertura'], 7 | transform: { 8 | '^.+\\.ts$': [ 9 | 'ts-jest', 10 | { 11 | useESM: true, 12 | tsconfig: 'tsconfig.json', 13 | }, 14 | ], 15 | }, 16 | moduleNameMapper: { 17 | '^(\\.{1,2}/.*)\\.js$': '$1', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-ascii-chart", 3 | "version": "5.3.0", 4 | "description": "Simple ascii chart generator", 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | "import": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.js" 13 | }, 14 | "require": { 15 | "types": "./dist/index.d.cts", 16 | "require": "./dist/index.cjs" 17 | } 18 | }, 19 | "scripts": { 20 | "lint": "eslint .", 21 | "lint:fix": "eslint . --fix", 22 | "test": "jest --coverage", 23 | "test:watch": "jest --watch", 24 | "check-exports": "npx --yes @arethetypeswrong/cli --pack .", 25 | "build": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap", 26 | "build:watch": "tsup src/index.ts --format cjs,esm --dts --clean --sourcemap --watch", 27 | "examples": "tsup src/scripts/show-examples.ts --watch --onSuccess \"clear && node dist/show-examples.cjs\"", 28 | "snapshots:generate": "tsup src/scripts/generate-snapshots.ts --onSuccess \"node dist/generate-snapshots.cjs\"", 29 | "prepare": "npm run build && npm test && npm run lint" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "^29.5.12", 33 | "@types/node": "^22.15.17", 34 | "@typescript-eslint/eslint-plugin": "^8.32.0", 35 | "@typescript-eslint/parser": "^8.32.0", 36 | "eslint": "^9.26.0", 37 | "eslint-config-prettier": "^10.1.5", 38 | "eslint-plugin-import": "^2.28.1", 39 | "eslint-plugin-jsdoc": "^50.6.14", 40 | "jest": "^29.7.0", 41 | "prettier": "^3.5.3", 42 | "ts-jest": "^29.3.2", 43 | "tsup": "^8.4.0", 44 | "typescript": "^5.8.3" 45 | }, 46 | "keywords": [ 47 | "ascii", 48 | "js", 49 | "ts", 50 | "chart", 51 | "line chart", 52 | "bar chart", 53 | "plot" 54 | ], 55 | "author": "gtktsc", 56 | "license": "MIT", 57 | "repository": { 58 | "type": "git", 59 | "url": "git+https://github.com/gtktsc/ascii-chart.git" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/gtktsc/ascii-chart/issues" 63 | }, 64 | "homepage": "https://github.com/gtktsc/ascii-chart#readme", 65 | "files": [ 66 | "dist/**/*" 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /src/__tests__/services/coords.test.ts: -------------------------------------------------------------------------------- 1 | import { Point, SingleLine } from '../../types/index'; 2 | import { 3 | toCoordinates, 4 | fromPlot, 5 | scaler, 6 | getExtrema, 7 | getPlotCoords, 8 | toSorted, 9 | toEmpty, 10 | normalize, 11 | padOrTrim, 12 | } from '../../services/coords'; 13 | 14 | describe('normalize', () => { 15 | it('should return empty array for undefined input', () => { 16 | expect(normalize(undefined)).toEqual([]); 17 | }); 18 | 19 | it('should wrap string input in an array', () => { 20 | expect(normalize('hello')).toEqual(['hello']); 21 | }); 22 | 23 | it('should return array as-is if input is already an array', () => { 24 | expect(normalize(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']); 25 | }); 26 | }); 27 | 28 | describe('padOrTrim', () => { 29 | it('should return the same array if already correct length', () => { 30 | const input = ['a', 'b', 'c']; 31 | expect(padOrTrim(input, 3)).toEqual(['a', 'b', 'c']); 32 | }); 33 | 34 | it('should trim the array if it is too long', () => { 35 | const input = ['a', 'b', 'c', 'd']; 36 | expect(padOrTrim(input, 2)).toEqual(['a', 'b']); 37 | }); 38 | 39 | it('should pad the array with empty strings if too short', () => { 40 | const input = ['a']; 41 | expect(padOrTrim(input, 3)).toEqual(['a', '', '']); 42 | }); 43 | 44 | it('should return an array of empty strings if original is empty', () => { 45 | expect(padOrTrim([], 3)).toEqual(['', '', '']); 46 | }); 47 | 48 | it('should return an empty array if target length is 0', () => { 49 | expect(padOrTrim(['a', 'b'], 0)).toEqual([]); 50 | }); 51 | }); 52 | 53 | describe('toCoordinates', () => { 54 | const plotWidth = 10; 55 | const plotHeight = 10; 56 | 57 | it('should scale point to coordinates within the plot dimensions', () => { 58 | const rangeX = [0, 100]; 59 | const rangeY = [0, 100]; 60 | const point: Point = [50, 50]; 61 | 62 | const [x, y] = toCoordinates(point, plotWidth, plotHeight, rangeX, rangeY); 63 | expect(x).toBeCloseTo(5); // midpoint of rangeX scaled to plot width 64 | expect(y).toBeCloseTo(5); // midpoint of rangeY scaled to plot height 65 | }); 66 | 67 | it('should handle a point at the origin', () => { 68 | const rangeX = [0, 100]; 69 | const rangeY = [0, 100]; 70 | const point: Point = [0, 0]; 71 | 72 | const [x, y] = toCoordinates(point, plotWidth, plotHeight, rangeX, rangeY); 73 | expect(x).toBeCloseTo(0); 74 | expect(y).toBeCloseTo(0); 75 | }); 76 | 77 | it('should handle a point at the maximum range', () => { 78 | const rangeX = [0, 100]; 79 | const rangeY = [0, 100]; 80 | const point: Point = [100, 100]; 81 | 82 | const [x, y] = toCoordinates(point, plotWidth, plotHeight, rangeX, rangeY); 83 | expect(x).toBeCloseTo(plotWidth - 1); 84 | expect(y).toBeCloseTo(plotHeight - 1); 85 | }); 86 | 87 | it('should correctly map a point with negative coordinates', () => { 88 | const rangeX = [-50, 50]; 89 | const rangeY = [-50, 50]; 90 | const point: Point = [0, 0]; 91 | 92 | const [x, y] = toCoordinates(point, plotWidth, plotHeight, rangeX, rangeY); 93 | expect(x).toBeCloseTo(5); // midpoint of plot width 94 | expect(y).toBeCloseTo(5); // midpoint of plot height 95 | }); 96 | 97 | it('should map fractional points accurately', () => { 98 | const rangeX = [0, 1]; 99 | const rangeY = [0, 1]; 100 | const point: Point = [0.5, 0.5]; 101 | 102 | const [x, y] = toCoordinates(point, plotWidth, plotHeight, rangeX, rangeY); 103 | expect(x).toBeCloseTo(5); 104 | expect(y).toBeCloseTo(5); 105 | }); 106 | }); 107 | 108 | describe('fromPlot', () => { 109 | const plotWidth = 10; 110 | const plotHeight = 10; 111 | const reverseCoords = fromPlot(plotWidth, plotHeight); 112 | 113 | it('should correctly convert scaled coordinates at (0, 0)', () => { 114 | const [x, y] = reverseCoords(0, 0); 115 | expect(x).toBe(0); 116 | expect(y).toBe(9); 117 | }); 118 | 119 | it('should correctly convert scaled coordinates at maximum width and height', () => { 120 | const [x, y] = reverseCoords(10, 10); 121 | expect(x).toBe(10); 122 | expect(y).toBe(0); 123 | }); 124 | 125 | it('should correctly convert scaled coordinates at midpoint', () => { 126 | const [x, y] = reverseCoords(5, 5); 127 | expect(x).toBe(5); 128 | expect(y).toBe(5); 129 | }); 130 | 131 | it('should handle non-integer scaled coordinates', () => { 132 | const [x, y] = reverseCoords(7.5, 2.5); 133 | expect(x).toBe(8); 134 | expect(y).toBe(7); 135 | }); 136 | }); 137 | 138 | describe('getPlotCoords', () => { 139 | describe.each([ 140 | [ 141 | 'returns proper data', 142 | [ 143 | [0, 0], 144 | [1, 1], 145 | ], 146 | 2, 147 | 2, 148 | [ 149 | [0, 0], 150 | [1, 1], 151 | ], 152 | ], 153 | [ 154 | 'scales data', 155 | [ 156 | [0, 0], 157 | [1, 1], 158 | ], 159 | 4, 160 | 4, 161 | [ 162 | [0, 0], 163 | [3, 3], 164 | ], 165 | ], 166 | [ 167 | 'returns proper range', 168 | [ 169 | [0, 0], 170 | [1, 1], 171 | [2, 2], 172 | ], 173 | 6, 174 | 6, 175 | [ 176 | [0, 0], 177 | [2.5, 2.5], 178 | [5, 5], 179 | ], 180 | ], 181 | ])('', (variant, coords, width, height, output) => { 182 | it(variant, () => { 183 | expect(getPlotCoords(coords as SingleLine, width, height)).toStrictEqual(output); 184 | }); 185 | }); 186 | }); 187 | 188 | describe('scaler', () => { 189 | describe.each([ 190 | ['picks right range', [0, 1], [1, 1], 0, 1], 191 | ['picks right range with negative values', [-1, 1], [0, 100], -1, 0], 192 | ['picks right domain and range', [-1, 1], [-100, 100], -1, -100], 193 | ['picks right domain and range with negatives', [0, 10], [-100, 0], 10, 0], 194 | ['picks right domain and range from zero', [-1, 0], [-100, 100], 0, 100], 195 | ['picks right values from the range', [-1, 1], [-100, 100], 0.5, 50], 196 | ['picks right negative values', [0, 1], [-100, 0], 0.5, -50], 197 | ['picks fractional range', [0, 1], [0, 3], 0.5, 1.5], 198 | ])('', (variant, domain, range, input, output) => { 199 | it(variant, () => { 200 | expect(scaler(domain as number[], range as number[])(input)).toBe(output); 201 | }); 202 | }); 203 | }); 204 | 205 | describe('getExtrema', () => { 206 | describe.each([ 207 | [ 208 | 'gets max value', 209 | [ 210 | [0, 1], 211 | [1, 1], 212 | [4, 1], 213 | [2, 1], 214 | ], 215 | 'max', 216 | 0, 217 | 4, 218 | ], 219 | [ 220 | 'gets max value with negative values', 221 | [ 222 | [-1, 1], 223 | [1.3, 1], 224 | [4, 1], 225 | [0, 1], 226 | ], 227 | 'max', 228 | 0, 229 | 4, 230 | ], 231 | [ 232 | 'gets min value', 233 | [ 234 | [-1, 1], 235 | [1.3, 1], 236 | [4, 1], 237 | [0, 1], 238 | ], 239 | 'min', 240 | 0, 241 | -1, 242 | ], 243 | [ 244 | 'gets min value from second row when all values are equal', 245 | [ 246 | [-1, 1], 247 | [1.3, 1], 248 | [1, 1], 249 | [0, 1], 250 | ], 251 | 'min', 252 | 1, 253 | 1, 254 | ], 255 | [ 256 | 'gets max value from second row', 257 | [ 258 | [-1, 1], 259 | [1.3, 10], 260 | [1, -10], 261 | [0, 100], 262 | ], 263 | 'max', 264 | 1, 265 | 100, 266 | ], 267 | ])('', (variant, arr, type, position, output) => { 268 | it(variant, () => { 269 | expect(getExtrema(arr as SingleLine, type as 'min' | 'max', position)).toBe(output); 270 | }); 271 | }); 272 | }); 273 | 274 | describe('toSorted', () => { 275 | describe.each([ 276 | [ 277 | 'picks keeps the same range', 278 | [ 279 | [0, 1], 280 | [1, 1], 281 | ], 282 | [ 283 | [0, 1], 284 | [1, 1], 285 | ], 286 | ], 287 | [ 288 | 'inverts range', 289 | [ 290 | [1, 1], 291 | [0, 1], 292 | ], 293 | [ 294 | [0, 1], 295 | [1, 1], 296 | ], 297 | ], 298 | [ 299 | 'keep same values in place', 300 | [ 301 | [0, 1], 302 | [1, 1], 303 | [1, 2], 304 | [2, 2], 305 | ], 306 | [ 307 | [0, 1], 308 | [1, 1], 309 | [1, 2], 310 | [2, 2], 311 | ], 312 | ], 313 | ])('', (variant, arr, output) => { 314 | it(variant, () => { 315 | expect(toSorted(arr as SingleLine)).toStrictEqual(output); 316 | }); 317 | }); 318 | }); 319 | describe('toEmpty', () => { 320 | it('should return an empty array of the specified size', () => { 321 | const size = 5; 322 | const result = toEmpty(size, ''); 323 | expect(result).toHaveLength(size); 324 | expect(result.every((item) => item === '')).toBe(true); 325 | }); 326 | 327 | it('should return an array filled with the specified empty string', () => { 328 | const size = 3; 329 | const emptyString = 'X'; 330 | const result = toEmpty(size, emptyString); 331 | expect(result).toHaveLength(size); 332 | expect(result.every((item) => item === emptyString)).toBe(true); 333 | }); 334 | 335 | it('should return an empty array when size is 0', () => { 336 | const size = 0; 337 | const result = toEmpty(size); 338 | expect(result).toHaveLength(size); 339 | }); 340 | 341 | it('should return an empty array when size is negative', () => { 342 | const size = -5; 343 | const result = toEmpty(size); 344 | expect(result).toHaveLength(0); 345 | }); 346 | }); 347 | -------------------------------------------------------------------------------- /src/__tests__/services/defaults.test.ts: -------------------------------------------------------------------------------- 1 | import { getSymbols, getLabelShift, getInput, getChartSize, getLegendData } from '../../services/defaults'; 2 | import { AXIS, EMPTY, POINT, THRESHOLDS } from '../../constants'; 3 | import { Coordinates, MultiLine, Symbols } from '../../types'; 4 | 5 | describe('Chart Helper Functions', () => { 6 | describe('getLegendData', () => { 7 | const input = [ 8 | [ 9 | [1, 2], 10 | [3, 4], 11 | ], 12 | [ 13 | [5, 6], 14 | [7, 8], 15 | ], 16 | ] as MultiLine; 17 | 18 | const points = [ 19 | { x: 1, y: 2 }, 20 | { x: 3, y: 4 }, 21 | ]; 22 | const thresholds = [{ x: 5 }, { y: 6 }]; 23 | 24 | it('normalizes string inputs to arrays', () => { 25 | const result = getLegendData({ 26 | input, 27 | points, 28 | thresholds, 29 | dataSeries: 'S', 30 | pointsSeries: 'P', 31 | thresholdsSeries: 'T', 32 | }); 33 | 34 | expect(result.series).toEqual(['S', '']); 35 | expect(result.points).toEqual(['P', '']); 36 | expect(result.thresholds).toEqual(['T', '']); 37 | }); 38 | 39 | it('pads short legend arrays with empty strings', () => { 40 | const result = getLegendData({ 41 | input, 42 | points, 43 | thresholds, 44 | dataSeries: ['A'], 45 | pointsSeries: ['X'], 46 | thresholdsSeries: ['T1'], 47 | }); 48 | 49 | expect(result.series).toEqual(['A', '']); 50 | expect(result.points).toEqual(['X', '']); 51 | expect(result.thresholds).toEqual(['T1', '']); 52 | }); 53 | 54 | it('trims long legend arrays', () => { 55 | const result = getLegendData({ 56 | input, 57 | points, 58 | thresholds, 59 | dataSeries: ['S1', 'S2', 'S3'], 60 | pointsSeries: ['P1', 'P2', 'P3'], 61 | thresholdsSeries: ['T1', 'T2', 'T3'], 62 | }); 63 | 64 | expect(result.series).toEqual(['S1', 'S2']); 65 | expect(result.points).toEqual(['P1', 'P2']); 66 | expect(result.thresholds).toEqual(['T1', 'T2']); 67 | }); 68 | 69 | it('returns empty arrays if series are not provided', () => { 70 | const result = getLegendData({ 71 | input, 72 | points, 73 | thresholds, 74 | }); 75 | 76 | expect(result.series).toEqual([]); 77 | expect(result.points).toEqual([]); 78 | expect(result.thresholds).toEqual([]); 79 | }); 80 | 81 | it('returns empty arrays when input data is missing', () => { 82 | const result = getLegendData({ 83 | input, 84 | dataSeries: undefined, 85 | pointsSeries: undefined, 86 | thresholdsSeries: undefined, 87 | }); 88 | 89 | expect(result).toEqual({ 90 | series: [], 91 | points: [], 92 | thresholds: [], 93 | }); 94 | }); 95 | }); 96 | 97 | describe('getSymbols', () => { 98 | it('should return default symbols when none are provided', () => { 99 | const symbols = getSymbols({}); 100 | expect(symbols).toEqual({ 101 | axisSymbols: AXIS, 102 | emptySymbol: EMPTY, 103 | backgroundSymbol: EMPTY, 104 | borderSymbol: undefined, 105 | thresholdSymbols: { 106 | x: THRESHOLDS.x, 107 | y: THRESHOLDS.y, 108 | }, 109 | pointSymbol: POINT, 110 | }); 111 | }); 112 | 113 | it('should override default symbols when provided', () => { 114 | const customSymbols: Symbols = { 115 | axis: { x: 'X', y: 'Y' }, 116 | empty: '-', 117 | background: '=', 118 | border: '#', 119 | thresholds: { 120 | x: 'X', 121 | y: 'Y', 122 | }, 123 | point: 'o', 124 | }; 125 | 126 | const symbols = getSymbols({ symbols: customSymbols }); 127 | expect(symbols).toEqual({ 128 | axisSymbols: { ...AXIS, ...customSymbols.axis }, 129 | emptySymbol: customSymbols.empty, 130 | backgroundSymbol: customSymbols.background, 131 | borderSymbol: customSymbols.border, 132 | thresholdSymbols: customSymbols.thresholds, 133 | pointSymbol: customSymbols.point, 134 | }); 135 | }); 136 | }); 137 | 138 | describe('getChartSize', () => { 139 | it('should return default sizes when width and height are not provided', () => { 140 | const input: MultiLine = [ 141 | [ 142 | [1, 2], 143 | [2, 4], 144 | [3, 6], 145 | ], 146 | ]; 147 | const size = getChartSize({ input }); 148 | expect(size).toEqual({ 149 | minX: 1, 150 | minY: 2, 151 | plotWidth: 3, // length of rangeX 152 | plotHeight: 5, // maxY - minY + 1 153 | expansionX: [1, 3], 154 | expansionY: [2, 6], 155 | }); 156 | }); 157 | 158 | it('should use provided width and height', () => { 159 | const input: MultiLine = [ 160 | [ 161 | [1, 2], 162 | [2, 4], 163 | [3, 6], 164 | ], 165 | ]; 166 | 167 | const size = getChartSize({ input, width: 10, height: 10 }); 168 | expect(size).toEqual({ 169 | minX: 1, 170 | minY: 2, 171 | plotWidth: 10, 172 | plotHeight: 10, 173 | expansionX: [1, 3], 174 | expansionY: [2, 6], 175 | }); 176 | }); 177 | 178 | it('should adjust for small values without height', () => { 179 | const input: MultiLine = [ 180 | [ 181 | [1, 2], 182 | [2, 4], 183 | ], 184 | ]; 185 | const size = getChartSize({ input }); 186 | expect(size).toEqual({ 187 | minX: 1, 188 | minY: 2, 189 | plotWidth: 2, // length of rangeX 190 | plotHeight: 3, // length of rangeY since it's less than 3 without provided height 191 | expansionX: [1, 2], 192 | expansionY: [2, 4], 193 | }); 194 | }); 195 | 196 | it('should handle a mix of positive and negative values', () => { 197 | const input: MultiLine = [ 198 | [ 199 | [-3, -2], 200 | [-2, 4], 201 | [0, 0], 202 | [3, -1], 203 | ], 204 | ]; 205 | const size = getChartSize({ input }); 206 | expect(size).toEqual({ 207 | minX: -3, 208 | minY: -2, 209 | plotWidth: 4, // length of rangeX 210 | plotHeight: 7, // maxY - minY + 1 211 | expansionX: [-3, 3], 212 | expansionY: [-2, 4], 213 | }); 214 | }); 215 | }); 216 | 217 | describe('getLabelShift', () => { 218 | it('should calculate label shifts correctly', () => { 219 | const input: MultiLine = [ 220 | [ 221 | [1, 2], 222 | [3, 4], 223 | [5, 6], 224 | ], 225 | ]; 226 | const transformLabel = (value: number) => value.toString(); 227 | const result = getLabelShift({ 228 | showTickLabel: false, 229 | input, 230 | transformLabel, 231 | expansionX: [1, 5], 232 | expansionY: [2, 6], 233 | minX: 1, 234 | }); 235 | 236 | expect(result.xShift).toBe(1); 237 | expect(result.yShift).toBe(1); 238 | }); 239 | }); 240 | 241 | describe('getInput', () => { 242 | it('should convert singleline input to multiline', () => { 243 | const input: Coordinates = [ 244 | [1, 2], 245 | [3, 4], 246 | ]; 247 | const result = getInput({ rawInput: input }); 248 | expect(result).toEqual([input]); 249 | }); 250 | 251 | it('should keep multiline input unchanged', () => { 252 | const input: MultiLine = [ 253 | [ 254 | [1, 2], 255 | [3, 4], 256 | ], 257 | [ 258 | [5, 6], 259 | [7, 8], 260 | ], 261 | ]; 262 | const result = getInput({ rawInput: input }); 263 | expect(result).toEqual(input); 264 | }); 265 | }); 266 | }); 267 | -------------------------------------------------------------------------------- /src/__tests__/services/draw.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | drawXAxisEnd, 3 | drawYAxisEnd, 4 | drawAxis, 5 | drawGraph, 6 | drawChart, 7 | drawCustomLine, 8 | drawLine, 9 | drawShift, 10 | drawPosition, 11 | } from '../../services/draw'; 12 | import { AXIS, CHART } from '../../constants'; 13 | import { GraphMode, MultiLine, Point } from '../../types'; 14 | 15 | describe('Drawing functions', () => { 16 | describe('drawPosition', () => { 17 | it('should correctly draw a symbol at the specified position in the graph', () => { 18 | const graph = [ 19 | [' ', ' ', ' '], 20 | [' ', ' ', ' '], 21 | [' ', ' ', ' '], 22 | ]; 23 | drawPosition({ graph, scaledX: 1, scaledY: 1, symbol: 'X' }); 24 | expect(graph[1][1]).toEqual('X'); 25 | }); 26 | 27 | it('should handle out-of-bounds Y position in debug mode', () => { 28 | const graph = [ 29 | [' ', ' ', ' '], 30 | [' ', ' ', ' '], 31 | [' ', ' ', ' '], 32 | ]; 33 | const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); 34 | drawPosition({ graph, scaledX: 1, scaledY: 3, symbol: 'X', debugMode: true }); 35 | expect(consoleSpy).toHaveBeenCalledWith( 36 | 'Drawing at [1, 3]', 37 | 'Error: out of bounds Y', 38 | expect.objectContaining({ 39 | graph, 40 | scaledX: 1, 41 | scaledY: 3, 42 | }), 43 | ); 44 | consoleSpy.mockRestore(); 45 | }); 46 | 47 | it('should handle out-of-bounds X position in debug mode', () => { 48 | const graph = [ 49 | [' ', ' ', ' '], 50 | [' ', ' ', ' '], 51 | [' ', ' ', ' '], 52 | ]; 53 | const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); 54 | drawPosition({ graph, scaledX: 4, scaledY: 1, symbol: 'X', debugMode: true }); 55 | expect(consoleSpy).toHaveBeenCalledWith( 56 | 'Drawing at [4, 1]', 57 | 'Error: out of bounds X', 58 | expect.objectContaining({ 59 | graph, 60 | scaledX: 4, 61 | scaledY: 1, 62 | }), 63 | ); 64 | consoleSpy.mockRestore(); 65 | }); 66 | 67 | it('should not log any errors if debugMode is off and out-of-bounds error occurs', () => { 68 | const graph = [ 69 | [' ', ' ', ' '], 70 | [' ', ' ', ' '], 71 | [' ', ' ', ' '], 72 | ]; 73 | const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); 74 | drawPosition({ graph, scaledX: 4, scaledY: 1, symbol: 'X' }); 75 | expect(consoleSpy).not.toHaveBeenCalled(); 76 | consoleSpy.mockRestore(); 77 | }); 78 | }); 79 | 80 | describe('drawXAxisEnd', () => { 81 | it('should draw the X-axis end correctly', () => { 82 | const graph = [ 83 | [' ', ' ', ' ', ' ', ' '], 84 | [' ', ' ', ' ', ' ', ' '], 85 | [' ', ' ', ' ', ' ', ' '], 86 | ]; 87 | const args = { 88 | hasPlaceToRender: true, 89 | yPos: 1, 90 | graph, 91 | yShift: 1, 92 | i: 0, 93 | plotHeight: 3, 94 | scaledX: 1, 95 | shift: 0, 96 | signShift: 0, 97 | axisSymbols: AXIS, 98 | pointXShift: ['1'], 99 | }; 100 | drawXAxisEnd(args); 101 | expect(graph[0][4]).toEqual('1'); 102 | }); 103 | it('should draw Y axis end', () => { 104 | const graph = [ 105 | [' ', ' ', ' '], 106 | [' ', ' ', ' '], 107 | [' ', 'A', ' '], 108 | ]; 109 | const params = { 110 | graph, 111 | scaledY: 1, 112 | yShift: 0, 113 | axis: { x: 0, y: 0 }, 114 | pointY: 1, 115 | plotHeight: 3, 116 | transformLabel: (value: number) => value.toString(), 117 | axisSymbols: { y: 'Y', ns: 'A', we: 'B' }, 118 | expansionX: [0], 119 | expansionY: [0, 1, 2], 120 | coordsGetter: () => [0, 0] as Point, 121 | plotGetter: () => [0, 0] as Point, 122 | }; 123 | 124 | drawYAxisEnd(params); 125 | 126 | expect(graph[2][1]).toBe('Y'); 127 | }); 128 | }); 129 | 130 | describe('drawYAxisEnd', () => { 131 | it('should draw tick labels for each step when showTickLabel is true', () => { 132 | const graph = [ 133 | [' ', ' ', ' ', ' ', ' '], 134 | [' ', ' ', ' ', ' ', ' '], 135 | [' ', ' ', ' ', ' ', ' '], 136 | [' ', ' ', ' ', ' ', ' '], 137 | ]; 138 | const args = { 139 | graph, 140 | scaledY: 1, 141 | yShift: 1, 142 | axis: { x: 1, y: 1 }, 143 | pointY: 2, 144 | plotHeight: 4, 145 | transformLabel: (value: number) => value.toString(), 146 | axisSymbols: { y: 'Y' }, 147 | expansionX: [0], 148 | expansionY: [0, 1, 2, 3], 149 | showTickLabel: true, 150 | }; 151 | drawYAxisEnd(args); 152 | 153 | // Expect Y-axis labels to be drawn starting from [1][2], not [0][2] 154 | expect(graph[1][2]).toEqual('3'); // Top of the axis (Y value 3) 155 | expect(graph[2][2]).toEqual('2'); // Mid Y value (Y value 2) 156 | expect(graph[3][2]).toEqual('1'); // Near bottom (Y value 1) 157 | // The bottom Y value '0' might not be drawn, depending on the graph size 158 | }); 159 | 160 | it('should draw the Y-axis end correctly', () => { 161 | const graph = [ 162 | [' ', ' ', ' ', ' '], 163 | [' ', ' ', ' ', ' '], 164 | [' ', ' ', ' ', AXIS.ns], 165 | ]; 166 | const args = { 167 | graph, 168 | scaledY: 1, 169 | yShift: 1, 170 | axis: { x: 1, y: 1 }, 171 | pointY: 2, 172 | plotHeight: 2, 173 | transformLabel: (value: number) => value.toString(), 174 | axisSymbols: AXIS, 175 | expansionX: [], 176 | expansionY: [], 177 | coordsGetter: () => [0, 0] as Point, 178 | plotGetter: () => [0, 0] as Point, 179 | }; 180 | drawYAxisEnd(args); 181 | 182 | expect(graph[2][3]).toEqual(AXIS.y); 183 | expect(graph[2][2]).toEqual('2'); 184 | }); 185 | }); 186 | 187 | describe('drawAxis', () => { 188 | it('should draw the main axis', () => { 189 | const graph = [ 190 | [' ', ' ', ' '], 191 | [' ', ' ', ' '], 192 | [' ', ' ', ' '], 193 | ]; 194 | const args = { 195 | graph, 196 | axis: { x: 1, y: 1 }, 197 | axisSymbols: AXIS, 198 | }; 199 | drawAxis(args); 200 | expect(graph[0][1]).toEqual(AXIS.n); 201 | expect(graph[1][1]).toEqual(AXIS.ns); 202 | expect(graph[2][1]).toEqual(AXIS.nse); 203 | }); 204 | }); 205 | 206 | describe('drawGraph', () => { 207 | it('should draw an empty graph correctly', () => { 208 | const result = drawGraph({ 209 | plotWidth: 3, 210 | plotHeight: 2, 211 | emptySymbol: ' ', 212 | }); 213 | expect(result).toEqual([ 214 | [' ', ' ', ' ', ' ', ' '], 215 | [' ', ' ', ' ', ' ', ' '], 216 | [' ', ' ', ' ', ' ', ' '], 217 | [' ', ' ', ' ', ' ', ' '], 218 | ]); 219 | }); 220 | }); 221 | 222 | describe('drawChart', () => { 223 | it('should return the graph as a string', () => { 224 | const graph = [ 225 | ['a', 'b', 'c'], 226 | ['d', 'e', 'f'], 227 | ['g', 'h', 'i'], 228 | ]; 229 | const result = drawChart({ graph }); 230 | expect(result).toEqual('\nabc\ndef\nghi\n'); 231 | }); 232 | }); 233 | 234 | describe('drawCustomLine', () => { 235 | it('should draw a custom line', () => { 236 | const graph = [ 237 | [' ', ' ', ' '], 238 | [' ', ' ', ' '], 239 | [' ', ' ', ' '], 240 | ]; 241 | const args = { 242 | sortedCoords: [[1, 1]] as Point[], 243 | scaledX: 1, 244 | scaledY: 1, 245 | input: [[1, 1]] as unknown as MultiLine, 246 | index: 0, 247 | minY: 0, 248 | minX: 0, 249 | expansionX: [0], 250 | expansionY: [0], 251 | lineFormatter: () => ({ x: 1, y: 1, symbol: 'X' }), 252 | toPlotCoordinates: () => [1, 1] as Point, 253 | graph, 254 | }; 255 | drawCustomLine(args); 256 | expect(graph[1][1]).toEqual('X'); 257 | }); 258 | }); 259 | 260 | describe('drawLine', () => { 261 | it('should draw a line', () => { 262 | const graph = [ 263 | [' ', ' ', ' ', ' '], 264 | [' ', ' ', ' ', ' '], 265 | [' ', ' ', ' ', ' '], 266 | [' ', ' ', ' ', ' '], 267 | ]; 268 | const args = { 269 | index: 1, 270 | arr: [ 271 | [0, 0], 272 | [1, 1], 273 | ] as Point[], 274 | graph, 275 | mode: 'line' as GraphMode, 276 | scaledX: 1, 277 | axis: { x: 0, y: 5 }, 278 | axisCenter: undefined, 279 | scaledY: 1, 280 | plotHeight: 3, 281 | emptySymbol: ' ', 282 | chartSymbols: CHART, 283 | }; 284 | drawLine(args); 285 | expect(graph[3][1]).toEqual('┛'); 286 | expect(graph[2][2]).toEqual('━'); 287 | expect(graph[3][2]).toEqual(' '); 288 | }); 289 | }); 290 | 291 | describe('drawShift', () => { 292 | it('should shift the graph', () => { 293 | const graph = [ 294 | [' ', ' ', ' '], 295 | [' ', ' ', ' '], 296 | ]; 297 | const result = drawShift({ 298 | graph, 299 | plotWidth: 2, 300 | emptySymbol: ' ', 301 | scaledCoords: [ 302 | [0, 0], 303 | [1, 1], 304 | ], 305 | xShift: 1, 306 | yShift: 1, 307 | }); 308 | expect(result.hasToBeMoved).toBe(false); 309 | }); 310 | }); 311 | }); 312 | -------------------------------------------------------------------------------- /src/__tests__/services/overrides.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setTitle, 3 | addXLable, 4 | addYLabel, 5 | addLegend, 6 | addBorder, 7 | addBackgroundSymbol, 8 | addThresholds, 9 | setFillArea, 10 | removeEmptyLines, 11 | getTransformLabel, 12 | addPoints, 13 | } from '../../services/overrides'; 14 | import { CHART, EMPTY, THRESHOLDS, POINT } from '../../constants'; 15 | import { Formatter, FormatterHelpers, Graph, Legend, Threshold, GraphPoint } from '../../types'; 16 | 17 | describe('Graph Utility Functions', () => { 18 | let graph: Graph = []; 19 | let defaultGraph: Graph = []; 20 | const backgroundSymbol = EMPTY; 21 | const plotWidth = 10; 22 | const plotHeight = 8; 23 | const yShift = 2; 24 | const thresholdSymbols = { 25 | x: THRESHOLDS.x, 26 | y: THRESHOLDS.y, 27 | }; 28 | const expansionX = [0, plotWidth]; 29 | const expansionY = [0, plotHeight]; 30 | 31 | beforeEach(() => { 32 | defaultGraph = Array.from({ length: plotHeight }, () => 33 | Array(plotWidth).fill(backgroundSymbol), 34 | ); 35 | graph = Array.from({ length: plotHeight }, () => Array(plotWidth).fill(backgroundSymbol)); 36 | }); 37 | 38 | describe('addPoints', () => { 39 | const pointSymbol = POINT; 40 | 41 | it('should add a single point to the graph', () => { 42 | const points: GraphPoint[] = [{ x: 5, y: 5 }]; 43 | addPoints({ 44 | graph, 45 | points, 46 | plotWidth, 47 | plotHeight, 48 | expansionX, 49 | expansionY, 50 | pointSymbol, 51 | }); 52 | 53 | const flattened = graph.flat().join(''); 54 | expect(flattened).toContain(pointSymbol); 55 | }); 56 | 57 | it('should add multiple points to the graph', () => { 58 | const points: GraphPoint[] = [ 59 | { x: 1, y: 1 }, 60 | { x: 3, y: 3 }, 61 | { x: 8, y: 8 }, 62 | ]; 63 | 64 | addPoints({ 65 | graph, 66 | points, 67 | plotWidth, 68 | plotHeight, 69 | expansionX, 70 | expansionY, 71 | pointSymbol: 'A', 72 | }); 73 | 74 | expect(graph[1][8]).toBe('A'); 75 | expect(graph[5][4]).toBe('A'); 76 | expect(graph[7][2]).toBe('A'); 77 | }); 78 | 79 | it('should not fail for empty points array', () => { 80 | expect(() => 81 | addPoints({ 82 | graph, 83 | points: [], 84 | plotWidth, 85 | plotHeight, 86 | expansionX, 87 | expansionY, 88 | pointSymbol, 89 | }), 90 | ).not.toThrow(); 91 | }); 92 | 93 | it('should respect graph boundaries', () => { 94 | const points: GraphPoint[] = [{ x: 100, y: 100 }]; 95 | addPoints({ 96 | graph, 97 | points, 98 | plotWidth, 99 | plotHeight, 100 | expansionX, 101 | expansionY, 102 | pointSymbol, 103 | }); 104 | 105 | const flattened = graph.flat().join(''); 106 | expect(flattened).not.toContain(pointSymbol); // point is outside 107 | }); 108 | }); 109 | 110 | describe('setTitle', () => { 111 | it('should set the title correctly', () => { 112 | const title = 'TestTitle'; 113 | 114 | setTitle({ title, graph, backgroundSymbol, plotWidth, yShift }); 115 | 116 | expect(graph[0].join('')).toContain(title); 117 | }); 118 | }); 119 | 120 | describe('addXLable', () => { 121 | it('should add the xLabel correctly', () => { 122 | const xLabel = 'XLabel'; 123 | 124 | addXLable({ xLabel, graph, backgroundSymbol, plotWidth, yShift }); 125 | 126 | expect(graph[graph.length - 1].join('')).toContain(xLabel); 127 | }); 128 | }); 129 | describe('addYLabel', () => { 130 | it('should add the yLabel correctly', () => { 131 | const yLabel = 'YLabel'; 132 | addYLabel({ yLabel, graph, backgroundSymbol }); 133 | 134 | const firstCol = graph.map((row) => row[0]).join(''); 135 | expect(firstCol).toContain(yLabel); 136 | }); 137 | }); 138 | 139 | describe('addLegend', () => { 140 | it('should add the legend correctly', () => { 141 | const legend = { position: 'top', series: ['A', 'B'] } as Legend; 142 | 143 | addLegend({ 144 | pointSymbol: 'A', 145 | legend, 146 | graph, 147 | backgroundSymbol, 148 | input: [[]], 149 | }); 150 | 151 | expect(graph[0].join('')).toContain('A'); 152 | expect(graph[1].join('')).toContain('B'); 153 | }); 154 | const baseOptions = { 155 | pointSymbol: '•', 156 | backgroundSymbol: '.', 157 | input: [[]], 158 | }; 159 | 160 | it('adds top legend correctly with series only', () => { 161 | const legend: Legend = { position: 'top', series: ['S1', 'S2'] }; 162 | const graph: Graph = Array(3) 163 | .fill(null) 164 | .map(() => Array(10).fill('.')); 165 | 166 | addLegend({ ...baseOptions, legend, graph }); 167 | 168 | expect(graph[0].join('')).toContain('S1'); 169 | expect(graph[1].join('')).toContain('S2'); 170 | }); 171 | 172 | it('adds bottom legend with points and thresholds', () => { 173 | const legend: Legend = { 174 | position: 'bottom', 175 | series: ['S'], 176 | points: ['P1'], 177 | thresholds: ['T1'], 178 | }; 179 | const graph: Graph = Array(3) 180 | .fill(null) 181 | .map(() => Array(10).fill('.')); 182 | const thresholds = [{ x: 1, color: 'ansiBlue' }] as Threshold[]; 183 | const points = [{ x: 2, y: 1, color: 'ansiRed' }] as GraphPoint[]; 184 | 185 | addLegend({ ...baseOptions, legend, graph, thresholds, points }); 186 | 187 | const bottom = graph 188 | .slice(-3) 189 | .map((row) => row.join('')) 190 | .join('\n'); 191 | expect(bottom).toMatch(/S/); 192 | expect(bottom).toMatch(/T1/); 193 | expect(bottom).toMatch(/P1/); 194 | }); 195 | 196 | it('adds left legend with spacers between types', () => { 197 | const legend: Legend = { 198 | position: 'left', 199 | series: ['S'], 200 | thresholds: ['T'], 201 | points: ['P'], 202 | }; 203 | const graph: Graph = Array(10) 204 | .fill(null) 205 | .map(() => Array(10).fill('.')); 206 | 207 | addLegend({ ...baseOptions, legend, graph }); 208 | 209 | const left = graph.map((row) => row.slice(0, 6).join('')).join('\n'); 210 | expect(left).toMatch(/S/); 211 | expect(left).toMatch(/T/); 212 | expect(left).toMatch(/P/); 213 | expect(left).toMatch(/\n\.+\n/); // spacer 214 | }); 215 | 216 | it('adds right legend and does not overwrite chart content', () => { 217 | const legend: Legend = { 218 | position: 'right', 219 | series: ['R1', 'R2'], 220 | }; 221 | const graph: Graph = Array(5) 222 | .fill(null) 223 | .map(() => Array(10).fill('.')); 224 | graph[2][5] = 'X'; // Simulate chart content 225 | 226 | addLegend({ ...baseOptions, legend, graph }); 227 | 228 | expect(graph[2][5]).toBe('X'); // Unchanged 229 | const right = graph.map((row) => row.slice(-5).join('')).join('\n'); 230 | expect(right).toMatch(/R1/); 231 | expect(right).toMatch(/R2/); 232 | }); 233 | }); 234 | 235 | describe('addBorder', () => { 236 | it('should add the border correctly', () => { 237 | const borderSymbol = '#'; 238 | 239 | addBorder({ graph, borderSymbol, backgroundSymbol }); 240 | 241 | expect(graph[0][0]).toBe(borderSymbol); 242 | expect(graph[0][graph[0].length - 1]).toBe(borderSymbol); 243 | expect(graph[graph.length - 1][0]).toBe(borderSymbol); 244 | expect(graph[graph.length - 1][graph[0].length - 1]).toBe(borderSymbol); 245 | }); 246 | }); 247 | 248 | describe('addBackgroundSymbol', () => { 249 | it('should replace empty symbols with background symbols', () => { 250 | const emptySymbol = ' '; 251 | graph[1][1] = emptySymbol; 252 | 253 | addBackgroundSymbol({ graph, backgroundSymbol, emptySymbol }); 254 | 255 | expect(graph[1][1]).toBe(backgroundSymbol); 256 | }); 257 | }); 258 | 259 | describe('addThresholds', () => { 260 | it('should add thresholds correctly', () => { 261 | const thresholds = [{ x: 2, color: 'ansiRed' }] as Threshold[]; 262 | const axis = { x: 0, y: 0 }; 263 | const plotHeight = 10; 264 | const expansionX = [0, plotWidth]; 265 | const expansionY = [0, plotHeight]; 266 | 267 | addThresholds({ 268 | thresholdSymbols, 269 | graph, 270 | thresholds, 271 | axis, 272 | plotWidth, 273 | plotHeight, 274 | expansionX, 275 | expansionY, 276 | }); 277 | expect(graph[2][3]).toContain(CHART.ns); 278 | }); 279 | it('should add thresholds correctly - y', () => { 280 | const thresholds = [{ y: 5 }] as Threshold[]; 281 | const axis = { x: 0, y: 0 }; 282 | const plotHeight = 10; 283 | const expansionX = [0, plotWidth]; 284 | const expansionY = [0, plotHeight]; 285 | 286 | addThresholds({ 287 | thresholdSymbols, 288 | graph, 289 | thresholds, 290 | axis, 291 | plotWidth, 292 | plotHeight, 293 | expansionX, 294 | expansionY, 295 | }); 296 | 297 | expect(graph[5]).toEqual(Array(plotWidth).fill(CHART.we)); 298 | }); 299 | it('should add color', () => { 300 | const thresholds = [{ x: 1, y: 2, color: 'ansiBlue' }] as Threshold[]; 301 | const axis = { x: 0, y: 0 }; 302 | const plotHeight = 10; 303 | const expansionX = [0, plotWidth]; 304 | const expansionY = [0, plotHeight]; 305 | 306 | addThresholds({ 307 | thresholdSymbols, 308 | graph, 309 | thresholds, 310 | axis, 311 | plotWidth, 312 | plotHeight, 313 | expansionX, 314 | expansionY, 315 | }); 316 | 317 | expect(graph[2].join('')).toContain('\u001b[34m'); 318 | }); 319 | it('should add two thresholds ', () => { 320 | const thresholds = [ 321 | { x: 2, color: 'ansiBlue' }, 322 | { x: 3, color: 'ansiRed' }, 323 | ] as Threshold[]; 324 | 325 | const axis = { x: 0, y: 0 }; 326 | const plotHeight = 10; 327 | const expansionX = [0, plotWidth]; 328 | const expansionY = [0, plotHeight]; 329 | 330 | addThresholds({ 331 | thresholdSymbols, 332 | graph, 333 | thresholds, 334 | axis, 335 | plotWidth, 336 | plotHeight, 337 | expansionX, 338 | expansionY, 339 | }); 340 | expect(graph[0][3]).toContain('\u001b[34m'); 341 | expect(graph[0][4]).toContain('\u001b[31m'); 342 | }); 343 | 344 | it('should not add color if not set', () => { 345 | const thresholds = [{ x: 1, y: 2 }] as Threshold[]; 346 | const axis = { x: 0, y: 0 }; 347 | const plotHeight = 10; 348 | const expansionX = [0, plotWidth]; 349 | const expansionY = [0, plotHeight]; 350 | 351 | addThresholds({ 352 | thresholdSymbols, 353 | graph, 354 | thresholds, 355 | axis, 356 | plotWidth, 357 | plotHeight, 358 | expansionX, 359 | expansionY, 360 | }); 361 | 362 | expect(graph[2].join('')).not.toContain('\u001b[0m'); 363 | }); 364 | it('should not add color if not set', () => { 365 | const thresholds = [{ x: undefined }] as Threshold[]; 366 | const axis = { x: 0, y: 0 }; 367 | const plotHeight = 10; 368 | const expansionX = [0, plotWidth]; 369 | const expansionY = [0, plotHeight]; 370 | 371 | addThresholds({ 372 | thresholdSymbols, 373 | graph, 374 | thresholds, 375 | axis, 376 | plotWidth, 377 | plotHeight, 378 | expansionX, 379 | expansionY, 380 | }); 381 | 382 | expect(graph).toEqual(defaultGraph); 383 | }); 384 | 385 | it('should not add thresholds if its outside - x', () => { 386 | const thresholds = [{ x: 100, color: 'ansiRed' }] as Threshold[]; 387 | const axis = { x: 0, y: 0 }; 388 | const plotHeight = 10; 389 | const expansionX = [0, plotWidth]; 390 | const expansionY = [0, plotHeight]; 391 | 392 | addThresholds({ 393 | thresholdSymbols, 394 | graph, 395 | thresholds, 396 | axis, 397 | plotWidth, 398 | plotHeight, 399 | expansionX, 400 | expansionY, 401 | }); 402 | expect(graph.map((line) => line.join('')).join('')).not.toContain(CHART.ns); 403 | }); 404 | it('should not add thresholds if its outside - y', () => { 405 | const thresholds = [{ y: 100, color: 'ansiRed' }] as Threshold[]; 406 | const axis = { x: 0, y: 0 }; 407 | const plotHeight = 10; 408 | const expansionX = [0, plotWidth]; 409 | const expansionY = [0, plotHeight]; 410 | 411 | addThresholds({ 412 | thresholdSymbols, 413 | graph, 414 | thresholds, 415 | axis, 416 | plotWidth, 417 | plotHeight, 418 | expansionX, 419 | expansionY, 420 | }); 421 | expect(graph.map((line) => line.join('')).join('')).not.toContain(CHART.we); 422 | }); 423 | }); 424 | 425 | describe('setFillArea', () => { 426 | it('should set fill area correctly', () => { 427 | const chartSymbols = { nse: CHART.nse, wsn: CHART.wsn, we: CHART.we, area: 'A' }; 428 | graph[1][1] = CHART.nse; 429 | 430 | setFillArea({ graph, chartSymbols }); 431 | 432 | for (let y = 2; y < graph.length; y++) { 433 | expect(graph[y][1]).toBe('A'); 434 | } 435 | }); 436 | 437 | it.only('should use fill area correctly', () => { 438 | const chartSymbols = { nse: CHART.nse, wsn: CHART.wsn, we: CHART.we }; 439 | graph[1][1] = CHART.nse; 440 | setFillArea({ graph, chartSymbols }); 441 | 442 | for (let y = 2; y < graph.length; y++) { 443 | expect(graph[y][1]).toBe(CHART.area); 444 | } 445 | }); 446 | }); 447 | 448 | describe('removeEmptyLines', () => { 449 | it('should remove empty lines correctly', () => { 450 | const currentGraph = [ 451 | ['a', 'b', 'c'], 452 | ['d', 'e', 'f'], 453 | ['g', 'h', 'i'], 454 | [...Array(plotWidth).fill(backgroundSymbol)], 455 | ]; 456 | 457 | removeEmptyLines({ graph: currentGraph, backgroundSymbol }); 458 | 459 | expect(currentGraph.length).toBe(3); 460 | expect(currentGraph).toEqual([ 461 | ['a', 'b', 'c'], 462 | ['d', 'e', 'f'], 463 | ['g', 'h', 'i'], 464 | ]); 465 | }); 466 | }); 467 | 468 | describe('getTransformLabel', () => { 469 | it('should return a formatter function', () => { 470 | const formatter = getTransformLabel({}); 471 | 472 | expect(formatter).toBeInstanceOf(Function); 473 | }); 474 | 475 | it('should format labels correctly with a custom formatter', () => { 476 | const customFormatter: Formatter = (value) => `Custom: ${value}`; 477 | const formatter = getTransformLabel({ formatter: customFormatter }); 478 | 479 | expect(formatter(1, {} as FormatterHelpers)).toBe('Custom: 1'); 480 | }); 481 | }); 482 | }); 483 | -------------------------------------------------------------------------------- /src/__tests__/services/settings.test.ts: -------------------------------------------------------------------------------- 1 | import { getAnsiColor, getChartSymbols } from '../../services/settings'; 2 | import { Color } from '../../types'; 3 | import { CHART } from '../../constants'; 4 | 5 | describe('getAnsiColor', () => { 6 | describe.each([ 7 | ['ansiBlack', '\u001b[30m'], 8 | ['ansiRed', '\u001b[31m'], 9 | ['ansiGreen', '\u001b[32m'], 10 | ['ansiYellow', '\u001b[33m'], 11 | ['ansiBlue', '\u001b[34m'], 12 | ['ansiMagenta', '\u001b[35m'], 13 | ['ansiCyan', '\u001b[36m'], 14 | ['ansiWhite', '\u001b[37m'], 15 | ['default', '\u001b[37m'], 16 | ])('', (input, output) => { 17 | it(input, () => { 18 | const formatted = getAnsiColor(input as Color); 19 | expect(formatted).toBe(output); 20 | }); 21 | }); 22 | }); 23 | 24 | describe('getChartSymbols', () => { 25 | describe.each([ 26 | ['ansiBlack', '\u001b[30m', 0], 27 | ['ansiRed', '\u001b[31m', 0], 28 | ['ansiGreen', '\u001b[32m', 0], 29 | ['ansiYellow', '\u001b[33m', 0], 30 | ['ansiBlue', '\u001b[34m', 0], 31 | ['ansiMagenta', '\u001b[35m', 0], 32 | ['ansiCyan', '\u001b[36m', 0], 33 | ['ansiWhite', '\u001b[37m', 0], 34 | ['default', '\u001b[37m', 0], 35 | [['ansiBlack', 'ansiRed'], '\u001b[30m', 0], 36 | [['ansiBlack', 'ansiRed'], '\u001b[31m', 1], 37 | ])('applies color based on input and series index', (input, output, series) => { 38 | it(`applies color ${input.toString()} to series index ${series}`, () => { 39 | const formatted = getChartSymbols(input as Color, series, undefined, [[]], false); 40 | expect(formatted.we).toBe(`${output}━\u001b[0m`); 41 | expect(formatted.wns).toBe(`${output}┓\u001b[0m`); 42 | expect(formatted.ns).toBe(`${output}┃\u001b[0m`); 43 | expect(formatted.nse).toBe(`${output}┗\u001b[0m`); 44 | expect(formatted.wsn).toBe(`${output}┛\u001b[0m`); 45 | expect(formatted.sne).toBe(`${output}┏\u001b[0m`); 46 | expect(formatted.area).toBe(`${output}█\u001b[0m`); 47 | }); 48 | }); 49 | 50 | it('applies fill area symbol when fillArea is true', () => { 51 | const formatted = getChartSymbols('ansiBlue', 0, undefined, [[]], true); 52 | Object.keys(CHART).forEach((key) => { 53 | expect(formatted[key as keyof typeof CHART]).toBe(formatted.area); 54 | }); 55 | }); 56 | 57 | it('uses custom chart symbols when provided', () => { 58 | const customSymbols = { ...CHART, we: '-' }; 59 | const formatted = getChartSymbols('ansiRed', 0, customSymbols, [[]], false); 60 | expect(formatted.we).toBe(`\u001b[31m-\u001b[0m`); 61 | }); 62 | 63 | it('handles color getter function for color input', () => { 64 | const colorGetter = (series: number) => (series === 0 ? 'ansiGreen' : 'ansiYellow'); 65 | const formatted = getChartSymbols(colorGetter, 1, undefined, [[]], false); 66 | expect(formatted.we).toBe(`\u001b[33m━\u001b[0m`); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/__tests__/snapshot.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { plot } from '../index'; 4 | import { examples } from '../examples'; 5 | 6 | const SNAPSHOT_DIR = path.resolve(__dirname, '../../__snapshots__'); 7 | 8 | describe('graph snapshots', () => { 9 | examples.forEach(([data, settings], index) => { 10 | const name = settings.title || `example_${index}`; 11 | const fileName = 12 | `${index.toString().padStart(2, '0')}_${name}`.replace(/[^\w.-]+/g, '_') + '.txt'; 13 | const filePath = path.join(SNAPSHOT_DIR, fileName); 14 | 15 | it(`matches snapshot: ${name}`, () => { 16 | const actual = plot(data, settings); 17 | const expected = fs.readFileSync(filePath, 'utf8'); 18 | expect(actual).toBe(expected); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Symbols for drawing the axes on the graph. 3 | */ 4 | export const AXIS = { 5 | n: '▲', // Symbol for the top end of the Y-axis 6 | ns: '│', // Vertical line for the Y-axis 7 | y: '┤', // Right tick mark on the Y-axis 8 | nse: '└', // Bottom corner for the Y-axis meeting the X-axis 9 | x: '┬', // Top tick mark on the X-axis 10 | we: '─', // Horizontal line for the X-axis 11 | e: '▶', // Arrow symbol for the end of the X-axis 12 | intersectionXY: '┼', // Intersection of the X and Y axes 13 | intersectionX: '┴', // Bottom tick mark on the X-axis 14 | intersectionY: '├', // Left tick mark on the Y-axis 15 | }; 16 | 17 | /** 18 | * Symbols for rendering chart elements, including lines and areas. 19 | */ 20 | export const CHART = { 21 | we: '━', // Bold horizontal line in the chart 22 | wns: '┓', // Top-right corner for vertical-to-horizontal connection 23 | ns: '┃', // Bold vertical line in the chart 24 | nse: '┗', // Bottom-left corner for vertical-to-horizontal connection 25 | wsn: '┛', // Bottom-right corner for vertical-to-horizontal connection 26 | sne: '┏', // Top-left corner for vertical-to-horizontal connection 27 | area: '█', // Filled area symbol for chart representation 28 | }; 29 | 30 | /** 31 | * Symbol representing an empty space on the graph. 32 | */ 33 | export const EMPTY = ' '; 34 | 35 | /** 36 | * Symbols for drawing thresholds on the graph. 37 | */ 38 | export const THRESHOLDS = { 39 | x: '━', // Symbol for horizontal threshold line 40 | y: '┃', // Symbol for vertical threshold line 41 | }; 42 | 43 | /** 44 | * Symbol for the point of the graph. 45 | */ 46 | export const POINT = '●'; 47 | -------------------------------------------------------------------------------- /src/examples.ts: -------------------------------------------------------------------------------- 1 | import { Coordinates, Settings, SingleLine } from './types'; 2 | 3 | type Example = [Coordinates, Settings & { only?: boolean; title: string }]; 4 | 5 | const create = ( 6 | data: Coordinates, 7 | settings: Settings & { only?: boolean; title: string }, 8 | ): Example => [data, settings]; 9 | 10 | const settingsSchema = { 11 | showTickLabel: 'boolean', 12 | hideXAxis: 'boolean', 13 | hideXAxisTicks: 'boolean', 14 | hideYAxis: 'boolean', 15 | hideYAxisTicks: 'boolean', 16 | fillArea: 'boolean', 17 | debugMode: 'boolean', 18 | width: 'number', 19 | height: 'number', 20 | title: 'string', 21 | xLabel: 'string', 22 | yLabel: 'string', 23 | yRange: 'tuple:number:number', 24 | axisCenter: 'tuple:number:number', 25 | } as const; 26 | 27 | const typedSettings = (settings: T): T => { 28 | return settings; 29 | }; 30 | 31 | const generateAllSettingsExamples = ( 32 | data: Coordinates, 33 | ): [Coordinates, Settings & { title: string }][] => { 34 | const examples: [Coordinates, Settings & { title: string }][] = []; 35 | 36 | for (const [key, type] of Object.entries(settingsSchema)) { 37 | if (key === 'debugMode') continue; 38 | 39 | if (type === 'boolean') { 40 | for (const value of [true, false]) { 41 | examples.push( 42 | create( 43 | data, 44 | typedSettings({ 45 | title: `${key} = ${value}`, 46 | [key]: value, 47 | } as const), 48 | ), 49 | ); 50 | } 51 | } else if (type === 'number') { 52 | for (const value of [10, 50]) { 53 | examples.push( 54 | create( 55 | data, 56 | typedSettings({ 57 | title: `${key} = ${value}`, 58 | [key]: value, 59 | } as const), 60 | ), 61 | ); 62 | } 63 | } else if (type === 'tuple:number:number') { 64 | for (const value of [ 65 | [0, 10], 66 | [-5, 5], 67 | ] as [number, number][]) { 68 | examples.push( 69 | create( 70 | data, 71 | typedSettings({ 72 | title: `${key} = [${value.join(', ')}]`, 73 | [key]: value, 74 | } as const), 75 | ), 76 | ); 77 | } 78 | } else if (type === 'string') { 79 | if (key !== 'title') { 80 | examples.push( 81 | create( 82 | data, 83 | typedSettings({ 84 | title: `${key} = "label"`, 85 | [key]: 'label', 86 | } as const), 87 | ), 88 | ); 89 | } 90 | } 91 | } 92 | 93 | return examples; 94 | }; 95 | 96 | const datasets: Record = { 97 | default: [ 98 | [1, 2], 99 | [2, 3], 100 | [3, 4], 101 | [4, 1], 102 | [5, -1], 103 | [6, 3], 104 | [7, -1], 105 | [8, 9], 106 | [9, 10], 107 | [10, 11], 108 | ], 109 | small: [ 110 | [0.001, 0.001], 111 | [0.002, 0.004], 112 | [0.003, 0.002], 113 | [0.004, -0.001], 114 | [0.005, 0.004], 115 | [0.006, 0.014], 116 | ], 117 | short: [ 118 | [1, 2], 119 | [2, 3], 120 | [3, 4], 121 | [4, 1], 122 | ], 123 | negativeMixed: [ 124 | [-1, 2], 125 | [1, 2], 126 | [2, 3], 127 | [5, 5], 128 | [6, -2], 129 | ], 130 | simpleHigh: [ 131 | [1, 10], 132 | [2, 20], 133 | [3, 30], 134 | [4, 40], 135 | [5, 50], 136 | ], 137 | limited: [ 138 | [1, 2], 139 | [2, 3], 140 | [3, 4], 141 | [4, 8], 142 | ], 143 | tickTest: [ 144 | [1, 2], 145 | [2, 3], 146 | [5, 5], 147 | [6, 10], 148 | ], 149 | thresholds: [ 150 | [1, 1], 151 | [2, 4], 152 | [3, 4], 153 | [4, 2], 154 | [5, -1], 155 | [6, 3], 156 | [7, -1], 157 | [8, 9], 158 | ], 159 | }; 160 | 161 | export const examples: Example[] = [ 162 | create(datasets.default, { title: 'simple example' }), 163 | create(datasets.small, { 164 | title: 'small numbers', 165 | hideYAxisTicks: true, 166 | width: 20, 167 | height: 10, 168 | }), 169 | create(datasets.default, { 170 | title: 'Tick Label', 171 | hideYAxisTicks: true, 172 | showTickLabel: true, 173 | yRange: [0, 120], 174 | width: 50, 175 | height: 27, 176 | }), 177 | create(datasets.default, { 178 | title: 'Border', 179 | symbols: { border: '█' }, 180 | xLabel: 'x', 181 | yLabel: 'y', 182 | width: 20, 183 | height: 8, 184 | }), 185 | create(datasets.simpleHigh, { title: 'Simple chart', width: 10, height: 5, yRange: [60, 70] }), 186 | create(datasets.short, { title: 'bar chart', width: 10, mode: 'bar', height: 10 }), 187 | create( 188 | [ 189 | [-1, 2], 190 | [2, 3], 191 | [3, 4], 192 | [4, 1], 193 | ], 194 | { title: 'horizontal bar chart', width: 20, mode: 'horizontalBar', height: 10 }, 195 | ), 196 | create(datasets.short, { title: 'area', width: 20, fillArea: true, height: 10 }), 197 | create(datasets.short, { title: 'labels', width: 20, xLabel: 'x', yLabel: 'y', height: 10 }), 198 | create( 199 | [ 200 | datasets.short, 201 | [ 202 | [1, -2], 203 | [2, -3], 204 | [3, 3], 205 | [4, 0], 206 | ], 207 | ], 208 | { 209 | title: 'legend', 210 | width: 20, 211 | legend: { position: 'bottom', series: ['first', 'second'] }, 212 | xLabel: 'x', 213 | yLabel: 'y', 214 | height: 10, 215 | }, 216 | ), 217 | create( 218 | [ 219 | datasets.short, 220 | [ 221 | [1, -2], 222 | [2, -3], 223 | [3, 3], 224 | [4, 0], 225 | ], 226 | [ 227 | [1, -6], 228 | [2, -3], 229 | [3, 3], 230 | [4, 0], 231 | ], 232 | [ 233 | [1, -2], 234 | [2, -3], 235 | [3, 3], 236 | [4, 0], 237 | [5, 3], 238 | ], 239 | ], 240 | { 241 | title: 'multiline', 242 | width: 50, 243 | }, 244 | ), 245 | create( 246 | [ 247 | datasets.short, 248 | [ 249 | [1, -2], 250 | [2, -3], 251 | [3, 3], 252 | [4, 0], 253 | ], 254 | [ 255 | [1, -6], 256 | [2, -3], 257 | [3, 3], 258 | [4, 0], 259 | ], 260 | [ 261 | [1, -2], 262 | [2, -3], 263 | [3, 3], 264 | [4, 0], 265 | [5, 3], 266 | ], 267 | ], 268 | { 269 | title: 'multiline points', 270 | width: 50, 271 | mode: 'point', 272 | }, 273 | ), 274 | create(datasets.limited, { title: 'yRange', width: 20, height: 10, yRange: [1, 3] }), 275 | create(datasets.short, { title: 'yRange', width: 20, height: 10, yRange: [0, 5] }), 276 | create([...datasets.short, [5, 5], [6, 10]], { 277 | title: 'showTickLabel', 278 | width: 20, 279 | height: 10, 280 | showTickLabel: true, 281 | }), 282 | create(datasets.tickTest, { title: 'hideXAxis', width: 20, height: 10, hideXAxis: true }), 283 | create(datasets.tickTest, { title: 'hideYAxis', width: 20, height: 10, hideYAxis: true }), 284 | create(datasets.negativeMixed, { 285 | title: 'axisCenter', 286 | width: 20, 287 | height: 10, 288 | axisCenter: [0, 0], 289 | showTickLabel: true, 290 | }), 291 | create(datasets.negativeMixed, { 292 | title: 'lineFormatter', 293 | width: 20, 294 | height: 10, 295 | lineFormatter: (props) => { 296 | const output = [{ x: props.plotX, y: props.plotY, symbol: '█' }]; 297 | const [minX] = props.toPlotCoordinates(props.minX, props.minY); 298 | let i = minX; 299 | while (i <= props.plotX) output.push({ x: i++, y: props.plotY, symbol: '█' }); 300 | return output; 301 | }, 302 | }), 303 | create(datasets.negativeMixed, { 304 | title: 'lineFormatter', 305 | width: 20, 306 | height: 10, 307 | lineFormatter: (props) => { 308 | const output = [{ x: props.plotX, y: props.plotY, symbol: '█' }]; 309 | const [, maxY] = props.toPlotCoordinates(props.x, props.minY); 310 | let i = props.plotY; 311 | while (i <= maxY + 1) output.push({ x: props.plotX, y: i++, symbol: '█' }); 312 | return output; 313 | }, 314 | }), 315 | create(datasets.negativeMixed, { 316 | title: 'symbols', 317 | width: 20, 318 | height: 10, 319 | symbols: { 320 | background: '█', 321 | border: 'A', 322 | empty: 'B', 323 | }, 324 | }), 325 | create(datasets.short, { title: 'colors', width: 20, color: 'ansiGreen', height: 10 }), 326 | create(datasets.default, { 327 | title: 'custom y axis', 328 | width: 20, 329 | height: 10, 330 | customYAxisTicks: [-30, -2, 0, 2, 4, 6, 30], 331 | customXAxisTicks: [-30, 0, 2, 4, 6, 30], 332 | }), 333 | create(datasets.default, { 334 | title: 'custom ticks and axis center', 335 | 336 | width: 20, 337 | height: 10, 338 | axisCenter: [3, 3], 339 | customYAxisTicks: [-30, -2, 0, 2, 4, 6, 30], 340 | customXAxisTicks: [-30, 0, 2, 4, 6, 30], 341 | }), 342 | create(datasets.default, { 343 | title: 'custom ticks and axis center, hide x axis', 344 | 345 | width: 20, 346 | height: 10, 347 | hideXAxis: true, 348 | axisCenter: [3, 3], 349 | customYAxisTicks: [-30, -2, 0, 2, 4, 6, 30], 350 | customXAxisTicks: [-30, 0, 2, 4, 6, 30], 351 | }), 352 | create(datasets.default, { 353 | title: 'custom ticks and axis center, hide y axis', 354 | width: 20, 355 | height: 10, 356 | hideYAxis: true, 357 | axisCenter: [3, 3], 358 | customYAxisTicks: [-30, -2, 0, 2, 4, 6, 30], 359 | customXAxisTicks: [-30, 0, 2, 4, 6, 30], 360 | }), 361 | create(datasets.default, { 362 | title: 'custom ticks and axis center, hide y axis, show tick label', 363 | width: 20, 364 | height: 10, 365 | hideXAxis: true, 366 | axisCenter: [3, 3], 367 | showTickLabel: true, 368 | customXAxisTicks: [-30, 0, 2, 4, 6, 30], 369 | }), 370 | create( 371 | [ 372 | datasets.short, 373 | [ 374 | [1, 3], 375 | [2, 1], 376 | [3, 0], 377 | [4, 4], 378 | ], 379 | ], 380 | { 381 | title: 'colors with legend', 382 | width: 20, 383 | height: 10, 384 | thresholds: [{ x: 2, y: 2, color: 'ansiBlue' }], 385 | color: ['ansiGreen', 'ansiMagenta'], 386 | legend: { position: 'bottom', series: ['first', 'second'] }, 387 | }, 388 | ), 389 | create(datasets.thresholds, { 390 | width: 40, 391 | title: 'thresholds', 392 | thresholds: [ 393 | { y: 5, x: 5, color: 'ansiBlue' }, 394 | { y: 2, color: 'ansiGreen' }, 395 | ], 396 | }), 397 | create(datasets.thresholds, { 398 | width: 40, 399 | title: 'thresholds', 400 | symbols: { 401 | thresholds: { 402 | x: 'X', 403 | y: 'Y', 404 | }, 405 | }, 406 | thresholds: [ 407 | { y: 5, x: 5, color: 'ansiBlue' }, 408 | { y: 2, color: 'ansiGreen' }, 409 | ], 410 | }), 411 | create( 412 | [ 413 | [0, 3], 414 | [1, 2], 415 | [2, 3], 416 | [3, 4], 417 | [4, -2], 418 | [5, -5], 419 | [6, 2], 420 | [7, 0], 421 | ], 422 | { 423 | title: 'with axis center', 424 | color: 'ansiGreen', 425 | showTickLabel: true, 426 | width: 40, 427 | axisCenter: [0, 2], 428 | }, 429 | ), 430 | create( 431 | [ 432 | [0, 3], 433 | [1, 2], 434 | [2, 3], 435 | [3, 4], 436 | [4, -2], 437 | [5, -5], 438 | [6, 2], 439 | [7, 0], 440 | ], 441 | { 442 | title: 'bar chart with colors', 443 | color: 'ansiGreen', 444 | mode: 'bar', 445 | showTickLabel: true, 446 | width: 40, 447 | axisCenter: [0, 0], 448 | }, 449 | ), 450 | create( 451 | [ 452 | [0, 3], 453 | [1, 2], 454 | [2, 3], 455 | [3, 4], 456 | [4, -2], 457 | [5, -5], 458 | [6, 2], 459 | [7, 0], 460 | ], 461 | { 462 | title: 'horizontal bar chart with axis center', 463 | mode: 'horizontalBar', 464 | 465 | showTickLabel: true, 466 | width: 40, 467 | height: 20, 468 | axisCenter: [3, 1], 469 | }, 470 | ), 471 | create( 472 | [ 473 | [1, 0], 474 | [2, 20], 475 | [3, 29], 476 | ], 477 | { 478 | height: 10, 479 | mode: 'horizontalBar', 480 | width: 20, 481 | showTickLabel: true, 482 | title: 'horizontal bar chart', 483 | }, 484 | ), 485 | create( 486 | [ 487 | [1, 0], 488 | [2, 20], 489 | [3, 29], 490 | ], 491 | { 492 | height: 10, 493 | mode: 'bar', 494 | width: 20, 495 | showTickLabel: true, 496 | title: 'bar chart', 497 | }, 498 | ), 499 | 500 | create(datasets.default, { 501 | width: 20, 502 | axisCenter: [3, 5], 503 | title: 'axis center', 504 | }), 505 | 506 | create(datasets.default, { 507 | axisCenter: [3, 5], 508 | title: 'axis center', 509 | }), 510 | create(datasets.default, { 511 | axisCenter: [0, -10], 512 | title: 'axis center', 513 | }), 514 | create(datasets.default, { 515 | axisCenter: [0, 0], 516 | title: 'axis center', 517 | }), 518 | create(datasets.default, { 519 | axisCenter: [0, 0], 520 | title: 'axis center', 521 | }), 522 | create(datasets.default, { 523 | axisCenter: [0, 0], 524 | title: 'axis center', 525 | }), 526 | create( 527 | [ 528 | [-8, 8], 529 | [-4, 4], 530 | [-3, 3], 531 | [80, 80], 532 | ], 533 | { title: 'Small and big values', width: 60, height: 10 }, 534 | ), 535 | ...generateAllSettingsExamples(datasets.default), 536 | create(datasets.default, { 537 | axisCenter: [-20, 5], 538 | width: 50, 539 | height: 20, 540 | title: 'axis center', 541 | }), 542 | create(datasets.default, { 543 | axisCenter: [20, 5], 544 | width: 50, 545 | height: 20, 546 | title: 'axis center', 547 | }), 548 | create(datasets.default, { 549 | axisCenter: [0, 20], 550 | width: 50, 551 | height: 20, 552 | title: 'axis center', 553 | }), 554 | create(datasets.default, { 555 | axisCenter: [0, -20], 556 | width: 50, 557 | height: 20, 558 | title: 'axis center', 559 | }), 560 | create(datasets.default, { 561 | axisCenter: [0, -20], 562 | hideXAxis: true, 563 | width: 50, 564 | height: 20, 565 | title: 'axis center', 566 | }), 567 | create( 568 | [ 569 | [ 570 | [-8, -8], 571 | [-4, -4], 572 | [-3, -3], 573 | [-2, -2], 574 | [-1, -1], 575 | [0, 0], 576 | [2, 2], 577 | [3, 3], 578 | [4, 4], 579 | [8, 8], 580 | ], 581 | ], 582 | { 583 | title: 'raws two complicated graphs with moved axis', 584 | width: 40, 585 | height: 20, 586 | axisCenter: [0, 0], 587 | }, 588 | ), 589 | create( 590 | [ 591 | [-5, 2], 592 | [2, -3], 593 | [13, 0.1], 594 | [4, 2], 595 | [5, -2], 596 | [6, 12], 597 | ], 598 | { 599 | title: 'hide axis', 600 | width: 40, 601 | height: 10, 602 | hideYAxis: true, 603 | hideXAxis: true, 604 | }, 605 | ), 606 | create( 607 | [ 608 | [0, 3], 609 | [1, 2], 610 | [2, 3], 611 | [3, 4], 612 | [4, -2], 613 | [5, -5], 614 | [6, 2], 615 | [7, 0], 616 | ], 617 | { 618 | title: 'bar chart with axis', 619 | mode: 'bar', 620 | showTickLabel: true, 621 | width: 40, 622 | axisCenter: [0, 0], 623 | }, 624 | ), 625 | ]; 626 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { plot } from './services/plot'; 2 | 3 | export * from './types'; 4 | export * from './constants'; 5 | 6 | export default plot; 7 | export { plot }; 8 | -------------------------------------------------------------------------------- /src/scripts/generate-snapshots.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { plot } from '../services/plot'; 4 | import { examples } from '../examples'; 5 | 6 | const SNAPSHOT_DIR = path.resolve(__dirname, '../__snapshots__'); 7 | 8 | if (!fs.existsSync(SNAPSHOT_DIR)) { 9 | fs.mkdirSync(SNAPSHOT_DIR); 10 | } 11 | 12 | examples.forEach(([data, settings], index) => { 13 | const output = plot(data, settings); 14 | const fileName = 15 | `${index.toString().padStart(2, '0')}_${settings.title || 'example'}`.replace( 16 | /[^\w.-]+/g, 17 | '_', 18 | ) + '.txt'; 19 | 20 | const filePath = path.join(SNAPSHOT_DIR, fileName); 21 | fs.writeFileSync(filePath, output); 22 | }); 23 | -------------------------------------------------------------------------------- /src/scripts/show-examples.ts: -------------------------------------------------------------------------------- 1 | import { plot } from '../services/plot'; 2 | import { examples } from '../examples'; 3 | 4 | const hasFilter = examples.some(([, { only }]) => only); 5 | 6 | console.clear(); 7 | examples 8 | .filter(([, settings]) => (hasFilter ? settings.only : true)) 9 | .forEach(([data, options]) => { 10 | console.log(plot(data, options)); 11 | }); 12 | -------------------------------------------------------------------------------- /src/services/coords.ts: -------------------------------------------------------------------------------- 1 | import { SingleLine, Point, MultiLine } from '../types/index'; 2 | import { EMPTY } from '../constants/index'; 3 | 4 | /** 5 | * Normalizes the input value to an array of strings. 6 | * @param value - The value to normalize, which can be a string, an array of strings, or undefined. 7 | * @returns {string[]} - An array of strings. If the input is undefined, an empty array is returned. 8 | */ 9 | export const normalize = (value?: string[] | string): string[] => 10 | value === undefined ? [] : Array.isArray(value) ? value : [value]; 11 | 12 | /** 13 | * Pads or trims an array of labels to a specified target length. 14 | * @param {string[]} labels - The array of labels to pad or trim. 15 | * @param {number} targetLength - The target length for the array. 16 | * @returns {string[]} - The padded or trimmed array of labels. 17 | */ 18 | export const padOrTrim = (labels: string[], targetLength: number): string[] => { 19 | if (labels.length > targetLength) return labels.slice(0, targetLength); 20 | if (labels.length < targetLength) 21 | return [...labels, ...Array(targetLength - labels.length).fill('')]; 22 | return labels; 23 | }; 24 | 25 | /** 26 | * Creates an array filled with a specified string. 27 | * @param {number} size - The size of the array. 28 | * @param {string} empty - The value to fill the array with (default: EMPTY). 29 | * @returns {string[]} - An array filled with the specified string. 30 | */ 31 | export const toEmpty = (size: number, empty: string = EMPTY): string[] => 32 | Array(size >= 0 ? size : 0).fill(empty); 33 | 34 | /** 35 | * Converts a number or string to an array of its characters. 36 | * @param {number | string} input - The input to convert. 37 | * @returns {string[]} - An array of characters. 38 | */ 39 | export const toArray = (input: number | string): string[] => { 40 | return input.toString().split(''); 41 | }; 42 | 43 | /** 44 | * Removes duplicate values from an array. 45 | * @param {number[]} array - The array of numbers. 46 | * @returns {number[]} - An array containing only unique values. 47 | */ 48 | export const toUnique = (array: number[]): number[] => [...new Set(array)]; 49 | 50 | /** 51 | * Calculates the distance between two integer coordinates by rounding to the nearest integers. 52 | * @param {number} x - The x-coordinate of the first point. 53 | * @param {number} y - The y-coordinate of the second point. 54 | * @returns {number} - The absolute distance between the rounded points. 55 | */ 56 | export const distance = (x: number, y: number): number => Math.abs(Math.round(x) - Math.round(y)); 57 | 58 | /** 59 | * Flattens a multi-line array of points into a single array of points. 60 | * @param {MultiLine} array - The multi-line array. 61 | * @returns {Point[]} - A flat array of points. 62 | */ 63 | export const toFlat = (array: MultiLine): Point[] => ([] as Point[]).concat(...array); 64 | 65 | /** 66 | * Converts a multi-line array into arrays of unique x and y values. 67 | * @param {MultiLine} array - The multi-line array. 68 | * @returns {[number[], number[]]} - Arrays of unique x and y values. 69 | */ 70 | export const toArrays = (array: MultiLine): [number[], number[]] => { 71 | const rangeX: number[] = []; 72 | const rangeY: number[] = []; 73 | 74 | toFlat(array).forEach(([x, y]) => { 75 | rangeX.push(x); 76 | rangeY.push(y); 77 | }); 78 | 79 | return [toUnique(rangeX), toUnique(rangeY)]; 80 | }; 81 | 82 | /** 83 | * Sorts a single-line array of points in ascending order based on the x-coordinate. 84 | * @param {SingleLine} array - The single-line array to sort. 85 | * @returns {SingleLine} - The sorted array. 86 | */ 87 | export const toSorted = (array: SingleLine): SingleLine => 88 | array.sort(([x1], [x2]) => { 89 | if (x1 < x2) return -1; 90 | if (x1 > x2) return 1; 91 | return 0; 92 | }); 93 | 94 | /** 95 | * Converts a number or undefined value to a point represented as an array. 96 | * @param {number} [x] - The x-coordinate (default: 0). 97 | * @param {number} [y] - The y-coordinate (default: 0). 98 | * @returns {Point} - The point represented as an array [x, y]. 99 | */ 100 | export const toPoint = (x?: number, y?: number): Point => [x ?? 0, y ?? 0]; 101 | 102 | /** 103 | * Returns a function that converts a coordinate (x, y) to scaled plot coordinates. 104 | * @param {number} plotWidth - The width of the plot. 105 | * @param {number} plotHeight - The height of the plot. 106 | * @returns {function} - A function that takes (x, y) and returns scaled plot coordinates [scaledX, scaledY]. 107 | */ 108 | export const toPlot = 109 | (plotWidth: number, plotHeight: number) => 110 | (x: number, y: number): Point => [ 111 | Math.round((x / plotWidth) * plotWidth), 112 | plotHeight - 1 - Math.round((y / plotHeight) * plotHeight), 113 | ]; 114 | 115 | /** 116 | * Returns a function that converts scaled plot coordinates (scaledX, scaledY) back to the original coordinates. 117 | * @param {number} plotWidth - The width of the plot. 118 | * @param {number} plotHeight - The height of the plot. 119 | * @returns {function} - A function that takes (scaledX, scaledY) and returns original coordinates [x, y]. 120 | */ 121 | export const fromPlot = 122 | (plotWidth: number, plotHeight: number) => 123 | (scaledX: number, scaledY: number): [number, number] => { 124 | const x = (scaledX / plotWidth) * plotWidth; 125 | const y = plotHeight - 1 - (scaledY / plotHeight) * (plotHeight - 1); 126 | return [Math.round(x), Math.round(y)]; 127 | }; 128 | 129 | /** 130 | * Finds the maximum or minimum value in a single-line array of points. 131 | * @param {SingleLine} arr - The single-line array to search for extrema. 132 | * @param {'max' | 'min'} type - 'max' to find the maximum value, 'min' for minimum (default is 'max'). 133 | * @param {number} position - The position of the value within each point (default is 1). 134 | * @returns {number} - The maximum or minimum value found in the array. 135 | */ 136 | export const getExtrema = (arr: SingleLine, type: 'max' | 'min' = 'max', position = 1) => 137 | arr.reduce( 138 | (previous, curr) => Math[type](previous, curr[position]), 139 | type === 'max' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY, 140 | ); 141 | 142 | /** 143 | * Finds the maximum value in an array of numbers. 144 | * @param {number[]} arr - The array of numbers. 145 | * @returns {number} - The maximum value in the array. 146 | */ 147 | export const getMax = (arr: number[]) => 148 | arr.reduce((previous, curr) => Math.max(previous, curr), Number.NEGATIVE_INFINITY); 149 | 150 | /** 151 | * Finds the minimum value in an array of numbers. 152 | * @param {number[]} arr - The array of numbers. 153 | * @returns {number} - The minimum value in the array. 154 | */ 155 | export const getMin = (arr: number[]) => 156 | arr.reduce((previous, curr) => Math.min(previous, curr), Number.POSITIVE_INFINITY); 157 | 158 | /** 159 | * Returns a function that scales coordinates to fit within a specified range. 160 | * @param {[number, number]} domain - The original value range (min and max). 161 | * @param {[number, number]} range - The range to scale the values into. 162 | * @returns {(value: number) => number} - A function for scaling coordinates. 163 | */ 164 | export const scaler = ([domainMin, domainMax]: number[], [rangeMin, rangeMax]: number[]) => { 165 | const domainLength = Math.sqrt(Math.abs((domainMax - domainMin) ** 2)) || 1; 166 | const rangeLength = Math.sqrt((rangeMax - rangeMin) ** 2); 167 | 168 | return (domainValue: number) => 169 | rangeMin + (rangeLength * (domainValue - domainMin)) / domainLength; 170 | }; 171 | 172 | /** 173 | * Scales a point's coordinates to fit within the specified plot dimensions. 174 | * @param {Point} point - The point to scale. 175 | * @param {number} plotWidth - The width of the plot. 176 | * @param {number} plotHeight - The height of the plot. 177 | * @param {number[]} rangeX - The range of x values. 178 | * @param {number[]} rangeY - The range of y values. 179 | * @returns {Point} - The scaled point. 180 | */ 181 | export const toCoordinates = ( 182 | point: Point, 183 | plotWidth: number, 184 | plotHeight: number, 185 | rangeX: number[], 186 | rangeY: number[], 187 | ): Point => { 188 | const getXCoord = scaler(rangeX, [0, plotWidth - 1]); 189 | const getYCoord = scaler(rangeY, [0, plotHeight - 1]); 190 | 191 | return [Math.round(getXCoord(point[0])), Math.round(getYCoord(point[1]))]; 192 | }; 193 | 194 | /** 195 | * Scales a list of coordinates to fit within the specified plot dimensions. 196 | * @param {SingleLine} coordinates - The list of coordinates to scale. 197 | * @param {number} plotWidth - The width of the plot. 198 | * @param {number} plotHeight - The height of the plot. 199 | * @param {number[]} [rangeX] - The range of x values (defaults to min and max from coordinates). 200 | * @param {number[]} [rangeY] - The range of y values (defaults to min and max from coordinates). 201 | * @returns {SingleLine} - The scaled list of coordinates. 202 | */ 203 | export const getPlotCoords = ( 204 | coordinates: SingleLine, 205 | plotWidth: number, 206 | plotHeight: number, 207 | rangeX?: number[], 208 | rangeY?: number[], 209 | ): SingleLine => { 210 | const getXCoord = scaler( 211 | rangeX || [getExtrema(coordinates, 'min', 0), getExtrema(coordinates, 'max', 0)], 212 | [0, plotWidth - 1], 213 | ); 214 | const getYCoord = scaler(rangeY || [getExtrema(coordinates, 'min'), getExtrema(coordinates)], [ 215 | 0, 216 | plotHeight - 1, 217 | ]); 218 | 219 | return coordinates.map(([x, y]) => [getXCoord(x), getYCoord(y)]); 220 | }; 221 | 222 | /** 223 | * Computes the axis center point based on specified plot dimensions and ranges. 224 | * @param {MaybePoint} axisCenter - The center point for the axis. 225 | * @param {number} plotWidth - The width of the plot. 226 | * @param {number} plotHeight - The height of the plot. 227 | * @param {number[]} rangeX - The range of x values. 228 | * @param {number[]} rangeY - The range of y values. 229 | * @param {number[]} initialValue - The initial axis values. 230 | * @returns {Point} - The center point of the axis. 231 | */ 232 | export const getAxisCenter = ( 233 | axisCenter: Point | [number | undefined, number | undefined] | undefined, 234 | plotWidth: number, 235 | plotHeight: number, 236 | rangeX: number[], 237 | rangeY: number[], 238 | initialValue: [number, number], 239 | ): { x: number; y: number } => { 240 | const axis = { x: initialValue[0], y: initialValue[1] }; 241 | 242 | if (axisCenter) { 243 | const [x, y] = axisCenter; 244 | 245 | if (typeof x === 'number') { 246 | const xScaler = scaler(rangeX, [0, plotWidth - 1]); 247 | axis.x = Math.round(xScaler(x)); 248 | } 249 | 250 | if (typeof y === 'number') { 251 | const yScaler = scaler(rangeY, [0, plotHeight - 1]); 252 | axis.y = plotHeight - Math.round(yScaler(y)); 253 | } 254 | } 255 | 256 | return axis; 257 | }; 258 | -------------------------------------------------------------------------------- /src/services/defaults.ts: -------------------------------------------------------------------------------- 1 | import { AXIS, EMPTY, POINT, THRESHOLDS } from '../constants'; 2 | import { 3 | Symbols, 4 | MultiLine, 5 | Formatter, 6 | Coordinates, 7 | GraphPoint, 8 | Threshold, 9 | MaybePoint, 10 | } from '../types'; 11 | import { toArrays, getMin, getMax, toArray, padOrTrim, normalize } from './coords'; 12 | 13 | /** 14 | * Merges custom symbols with default axis symbols and defines plot symbols. 15 | * @param {object} options - An object containing optional custom symbols. 16 | * @param {Symbols} options.symbols - Custom symbols for the plot. 17 | * @returns {object} - Object containing the merged axis symbols, and defined symbols for empty, background, and border. 18 | */ 19 | export const getSymbols = ({ symbols }: { symbols?: Symbols }) => { 20 | const emptySymbol = symbols?.empty || EMPTY; 21 | return { 22 | axisSymbols: { ...AXIS, ...symbols?.axis }, 23 | emptySymbol, 24 | backgroundSymbol: symbols?.background || emptySymbol, 25 | borderSymbol: symbols?.border, 26 | thresholdSymbols: { 27 | x: symbols?.thresholds?.x || THRESHOLDS.x, 28 | y: symbols?.thresholds?.y || THRESHOLDS.y, 29 | }, 30 | pointSymbol: symbols?.point || POINT, 31 | }; 32 | }; 33 | 34 | /** 35 | * Determines plot size and range based on provided data and dimensions. 36 | * @param {object} options - An object containing input data and optional dimensions. 37 | * @param {MultiLine} options.input - The multiline array of points. 38 | * @param {number} [options.width] - Optional width of the plot. 39 | * @param {number} [options.height] - Optional height of the plot. 40 | * @param {MaybePoint} [options.axisCenter] - Optional axis center point. 41 | * @param {[number, number]} [options.yRange] - Optional range for the y-axis. 42 | * @returns {object} - Object containing min x value, plot width, plot height, and x and y expansions. 43 | */ 44 | export const getChartSize = ({ 45 | input, 46 | width, 47 | height, 48 | yRange, 49 | axisCenter, 50 | }: { 51 | input: MultiLine; 52 | width?: number; 53 | height?: number; 54 | axisCenter?: MaybePoint; 55 | yRange?: [number, number]; 56 | }) => { 57 | const [inputRangeX, inputRangeY] = toArrays(input); 58 | 59 | const rangeX = [...inputRangeX, axisCenter?.[0]].filter((v) => typeof v === 'number') as number[]; 60 | const rangeY = [...inputRangeY, axisCenter?.[1]].filter((v) => typeof v === 'number') as number[]; 61 | 62 | const minX = getMin(rangeX); 63 | const maxX = getMax(rangeX); 64 | const minY = getMin(rangeY); 65 | const maxY = getMax(rangeY); 66 | 67 | const expansionX = [minX, maxX]; 68 | 69 | const expansionY = yRange || [minY, maxY]; 70 | 71 | // Set default plot dimensions if not provided 72 | const plotWidth = width || rangeX.length; 73 | 74 | let plotHeight = Math.round(height || maxY - minY + 1); 75 | 76 | // Adjust plot height for small value ranges if no height is provided 77 | if (!height && plotHeight < 3) { 78 | plotHeight = rangeY.length; 79 | } 80 | 81 | return { 82 | minX, 83 | minY, 84 | plotWidth, 85 | plotHeight, 86 | expansionX, 87 | expansionY, 88 | }; 89 | }; 90 | 91 | /** 92 | * Calculates shifts for x and y labels, based on the longest label length. 93 | * @param {object} options - The input data and formatting options. 94 | * @param {MultiLine} options.input - The multiline array of points. 95 | * @param {Formatter} options.transformLabel - A function to transform label values. 96 | * @param {number[]} options.expansionX - The x-axis range. 97 | * @param {number[]} options.expansionY - The y-axis range. 98 | * @param {number} options.minX - The minimum x value for label calculation. 99 | * @param {boolean} [options.showTickLabel] - Flag to indicate if tick labels should be shown. 100 | * @returns {object} - Object containing the calculated xShift and yShift. 101 | */ 102 | export const getLabelShift = ({ 103 | input, 104 | transformLabel, 105 | expansionX, 106 | expansionY, 107 | minX, 108 | showTickLabel, 109 | }: { 110 | input: MultiLine; 111 | transformLabel: Formatter; 112 | expansionX: number[]; 113 | expansionY: number[]; 114 | minX: number; 115 | showTickLabel?: boolean; 116 | }) => { 117 | // Helper to compute the length of a formatted label 118 | const getLength = (value: number, axis: 'x' | 'y'): number => { 119 | const formatted = transformLabel(value, { axis, xRange: expansionX, yRange: expansionY }); 120 | return toArray(formatted).length; 121 | }; 122 | 123 | // Combine all points into one array for iteration 124 | const points = input.flat(); 125 | 126 | // Determine the maximum label lengths for x and y 127 | const { x: xShift, y: longestY } = points.reduce( 128 | (acc, [x, y]) => ({ 129 | x: Math.max(acc.x, getLength(x, 'x')), 130 | y: Math.max(acc.y, getLength(y, 'y')), 131 | }), 132 | { x: 0, y: 0 }, 133 | ); 134 | 135 | if (!showTickLabel) { 136 | // For minimal mode, ensure space for the axis symbol and labels 137 | const minXLength = getLength(minX, 'x'); 138 | const baseShift = Math.max(0, minXLength - 2); 139 | return { 140 | xShift, 141 | yShift: Math.max(baseShift, longestY), 142 | }; 143 | } 144 | 145 | // Full mode: add extra padding for tick labels 146 | return { xShift, yShift: longestY + 1 }; 147 | }; 148 | 149 | /** 150 | * Normalizes raw input data into a consistent multi-line format. 151 | * @param {object} options - Contains the raw input data. 152 | * @param {Coordinates} options.rawInput - Input coordinates, either single or multi-line. 153 | * @returns {MultiLine} - The formatted data as a multi-line array of points. 154 | */ 155 | export const getInput = ({ rawInput }: { rawInput: Coordinates }) => { 156 | let input = rawInput; 157 | 158 | // Convert single-line data to a multi-line format if needed 159 | if (typeof input[0]?.[0] === 'number') { 160 | input = [rawInput] as MultiLine; 161 | } 162 | 163 | return input as MultiLine; 164 | }; 165 | 166 | /** 167 | * Generates legend data based on the provided points, thresholds, and series. 168 | * @param {object} options - Contains points, thresholds, and series data. 169 | * @param {Coordinates} options.points - The coordinates of the points. 170 | * @param {THRESHOLDS} options.thresholds - The thresholds for the plot. 171 | * @param {string[]} options.series - The series names for the plot. 172 | * @param {string[]} options.pointsSeries - The series names for the points. 173 | * @param {string[]} options.thresholdsSeries - The series names for the thresholds. 174 | * @param {string[]} options.dataSeries - The series names for the data. 175 | * @param {MultiLine} options.input - The input data for the plot. 176 | * @returns {object} - Object containing the series, points, and thresholds for the legend. 177 | * 178 | */ 179 | export const getLegendData = ({ 180 | input, 181 | thresholds, 182 | points, 183 | pointsSeries, 184 | thresholdsSeries, 185 | dataSeries, 186 | }: { 187 | input: MultiLine; 188 | points?: GraphPoint[]; 189 | thresholds?: Threshold[]; 190 | pointsSeries?: string[] | string; 191 | thresholdsSeries?: string[] | string; 192 | dataSeries?: string[] | string; 193 | }) => { 194 | const legendSeries = dataSeries && input ? padOrTrim(normalize(dataSeries), input.length) : []; 195 | 196 | const legendPoints = 197 | pointsSeries && points ? padOrTrim(normalize(pointsSeries), points.length) : []; 198 | 199 | const legendThresholds = 200 | thresholdsSeries && thresholds ? padOrTrim(normalize(thresholdsSeries), thresholds.length) : []; 201 | 202 | return { 203 | series: legendSeries, 204 | points: legendPoints, 205 | thresholds: legendThresholds, 206 | }; 207 | }; 208 | -------------------------------------------------------------------------------- /src/services/overrides.ts: -------------------------------------------------------------------------------- 1 | import { CHART, THRESHOLDS } from '../constants'; 2 | import { 3 | Colors, 4 | Formatter, 5 | Graph, 6 | Legend, 7 | MultiLine, 8 | Point, 9 | Symbols, 10 | Threshold, 11 | FormatterHelpers, 12 | GraphPoint, 13 | } from '../types'; 14 | import { getPlotCoords, toArray, toEmpty, toPlot } from './coords'; 15 | import { getLegendData } from './defaults'; 16 | import { drawPosition } from './draw'; 17 | import { defaultFormatter, getAnsiColor, getChartSymbols } from './settings'; 18 | 19 | /** 20 | * Adds a title to the graph at the top. 21 | * @param {object} options - Object containing title options. 22 | * @param {string} options.title - The title text. 23 | * @param {Graph} options.graph - The graph array to modify. 24 | * @param {string} options.backgroundSymbol - Background symbol for the graph. 25 | * @param {number} options.plotWidth - Width of the plot. 26 | * @param {number} options.yShift - Vertical shift for positioning. 27 | * @param {boolean} [options.debugMode=false] - If true, logs errors for out-of-bounds access. 28 | */ 29 | export const setTitle = ({ 30 | title, 31 | graph, 32 | backgroundSymbol, 33 | plotWidth, 34 | yShift, 35 | debugMode, 36 | }: { 37 | title: string; 38 | graph: Graph; 39 | backgroundSymbol: string; 40 | plotWidth: number; 41 | yShift: number; 42 | debugMode?: boolean; 43 | }) => { 44 | graph.unshift(toEmpty(plotWidth + yShift + 2, backgroundSymbol)); 45 | Array.from(title).forEach((letter, index) => { 46 | drawPosition({ 47 | debugMode, 48 | graph, 49 | scaledX: index, 50 | scaledY: 0, 51 | symbol: letter, 52 | }); 53 | }); 54 | }; 55 | 56 | /** 57 | * Adds an x-axis label centered at the bottom of the graph. 58 | * @param {object} options - Object containing x-label options. 59 | * @param {string} options.xLabel - The x-axis label text. 60 | * @param {Graph} options.graph - The graph array to modify. 61 | * @param {string} options.backgroundSymbol - Background symbol for the graph. 62 | * @param {number} options.plotWidth - Width of the plot. 63 | * @param {number} options.yShift - Vertical shift for positioning. 64 | * @param {boolean} [options.debugMode=false] - If true, logs errors for out-of-bounds access. 65 | */ 66 | export const addXLable = ({ 67 | graph, 68 | plotWidth, 69 | yShift, 70 | backgroundSymbol, 71 | xLabel, 72 | debugMode, 73 | }: { 74 | xLabel: string; 75 | graph: Graph; 76 | backgroundSymbol: string; 77 | plotWidth: number; 78 | yShift: number; 79 | debugMode?: boolean; 80 | }) => { 81 | const totalWidth = graph[0].length; 82 | const labelLength = toArray(xLabel).length; 83 | const startingPosition = Math.round((totalWidth - labelLength) / 2); 84 | 85 | graph.push(toEmpty(plotWidth + yShift + 2, backgroundSymbol)); 86 | Array.from(xLabel).forEach((letter, index) => { 87 | drawPosition({ 88 | debugMode, 89 | graph, 90 | scaledX: startingPosition + index, 91 | scaledY: graph.length - 1, 92 | symbol: letter, 93 | }); 94 | }); 95 | }; 96 | 97 | /** 98 | * Adds a y-axis label centered on the left side of the graph. 99 | * @param {object} options - Object containing y-label options. 100 | * @param {Graph} options.graph - The graph array to modify. 101 | * @param {string} options.backgroundSymbol - Background symbol for the graph. 102 | * @param {string} options.yLabel - The y-axis label text. 103 | * @param {boolean} [options.debugMode=false] - If true, logs errors for out-of-bounds access. 104 | */ 105 | export const addYLabel = ({ 106 | graph, 107 | backgroundSymbol, 108 | yLabel, 109 | debugMode, 110 | }: { 111 | graph: Graph; 112 | backgroundSymbol: string; 113 | yLabel: string; 114 | debugMode?: boolean; 115 | }) => { 116 | const totalHeight = graph.length; 117 | const labelLength = toArray(yLabel).length; 118 | const startingPosition = Math.round((totalHeight - labelLength) / 2) - 1; 119 | 120 | const label = Array.from(yLabel); 121 | graph.forEach((line, position) => { 122 | line.unshift(backgroundSymbol); 123 | if (position > startingPosition && label[position - startingPosition - 1]) { 124 | drawPosition({ 125 | debugMode, 126 | graph, 127 | scaledX: 0, 128 | scaledY: position, 129 | symbol: label[position - startingPosition - 1], 130 | }); 131 | } 132 | }); 133 | }; 134 | 135 | /** 136 | * Adds a legend to the specified position on the graph (top, bottom, left, or right). 137 | * @param {object} options - Object containing legend options. 138 | * @param {Graph} options.graph - The graph array to modify. 139 | * @param {Legend} options.legend - Configuration for the legend's position and series. 140 | * @param {string} options.backgroundSymbol - Background symbol for the graph. 141 | * @param {MultiLine} options.input - Input data series for the chart. 142 | * @param {string} options.pointSymbol - Symbol used to draw points. 143 | * @param {GraphPoint[]} [options.points] - Points to render, with optional colors. 144 | * @param {Threshold[]} [options.thresholds] - Thresholds for the plot. 145 | * @param {Colors} [options.color] - Color(s) for each series. 146 | * @param {Symbols} [options.symbols] - Custom symbols for the chart. 147 | * @param {boolean} [options.fillArea] - Whether to fill the area below the lines. 148 | * @param {boolean} [options.debugMode=false] - If true, logs errors for out-of-bounds access. 149 | */ 150 | export const addLegend = ({ 151 | graph, 152 | legend, 153 | backgroundSymbol, 154 | color, 155 | symbols, 156 | fillArea, 157 | input, 158 | pointSymbol, 159 | debugMode, 160 | points, 161 | thresholds, 162 | }: { 163 | graph: Graph; 164 | legend: Legend; 165 | backgroundSymbol: string; 166 | input: MultiLine; 167 | color?: Colors; 168 | pointSymbol: string; 169 | symbols?: Symbols; 170 | fillArea?: boolean; 171 | debugMode?: boolean; 172 | points?: GraphPoint[]; 173 | thresholds?: Threshold[]; 174 | }) => { 175 | const { 176 | series: legendSeries, 177 | points: legendPoints, 178 | thresholds: legendThresholds, 179 | } = getLegendData({ 180 | input, 181 | thresholds, 182 | points, 183 | pointsSeries: legend.points, 184 | thresholdsSeries: legend.thresholds, 185 | dataSeries: legend.series, 186 | }); 187 | 188 | const allLabels = [ 189 | ...legendSeries.map((label, i) => ({ 190 | type: 'series' as const, 191 | label, 192 | index: i, 193 | })), 194 | ...(legendThresholds.length > 0 ? [{ type: 'spacer' as const }] : []), 195 | ...legendThresholds.map((label, i) => ({ 196 | type: 'threshold' as const, 197 | label, 198 | index: i, 199 | })), 200 | ...(legendPoints.length > 0 ? [{ type: 'spacer' as const }] : []), 201 | ...legendPoints.map((label, i) => ({ 202 | type: 'point' as const, 203 | label, 204 | index: i, 205 | })), 206 | ]; 207 | 208 | const legendWidth = 209 | 2 + 210 | Math.max( 211 | ...allLabels.map((entry) => { 212 | if (entry.type === 'spacer') return 0; 213 | return toArray(entry.label).length; 214 | }), 215 | ); 216 | 217 | const makePointSymbol = (index: number) => 218 | points && points[index]?.color 219 | ? `${getAnsiColor(points[index].color)}${pointSymbol}\u001b[0m` 220 | : pointSymbol; 221 | 222 | const makeThresholdSymbol = (index: number) => 223 | thresholds && thresholds[index]?.color 224 | ? `${getAnsiColor(thresholds[index].color)}┃\u001b[0m` 225 | : '┃'; 226 | 227 | const makeLabelRow = (entry: (typeof allLabels)[number]) => { 228 | if (entry.type === 'spacer') return Array(legendWidth).fill(backgroundSymbol); 229 | 230 | const { label } = entry; 231 | const labelText = toArray(label); 232 | const pad = legendWidth - labelText.length - 2; 233 | const padArr = Array(pad).fill(backgroundSymbol); 234 | 235 | let symbol: string; 236 | if (entry.type === 'point') symbol = makePointSymbol(entry.index); 237 | else if (entry.type === 'threshold') symbol = makeThresholdSymbol(entry.index); 238 | else symbol = getChartSymbols(color, entry.index, symbols?.chart, input, fillArea).area; 239 | 240 | return [symbol, backgroundSymbol, ...labelText, ...padArr]; 241 | }; 242 | 243 | if (legend.position === 'left') { 244 | const newRows = allLabels.map(makeLabelRow); 245 | for (let i = 0; i < graph.length; i++) { 246 | const label = newRows[i]; 247 | if (label) { 248 | for (let j = legendWidth - 1; j >= 0; j--) { 249 | graph[i].unshift(label[j]); 250 | } 251 | } else { 252 | for (let j = 0; j < legendWidth; j++) { 253 | graph[i].unshift(backgroundSymbol); 254 | } 255 | } 256 | } 257 | } 258 | 259 | if (legend.position === 'right') { 260 | // Pre-pad each row with empty space on the right so chart rendering doesn't overwrite legend 261 | for (let row of graph) { 262 | for (let i = 0; i < legendWidth + 2; i++) { 263 | row.push(backgroundSymbol); 264 | } 265 | } 266 | 267 | const newRows = allLabels.map(makeLabelRow); 268 | for (let i = 0; i < graph.length; i++) { 269 | const label = newRows[i]; 270 | if (label) { 271 | for (let j = 0; j < legendWidth; j++) { 272 | const columnIndex = graph[i].length - legendWidth + j; 273 | graph[i][columnIndex] = label[j]; 274 | } 275 | } 276 | } 277 | } 278 | 279 | if (legend.position === 'top') { 280 | allLabels 281 | .slice() 282 | .reverse() 283 | .forEach((entry) => { 284 | const line = makeLabelRow(entry); 285 | const row = toEmpty(graph[0].length, backgroundSymbol); 286 | graph.unshift(row); 287 | 288 | line.forEach((symbol, i) => { 289 | drawPosition({ 290 | debugMode, 291 | graph, 292 | scaledX: i, 293 | scaledY: 0, 294 | symbol, 295 | }); 296 | }); 297 | }); 298 | } 299 | 300 | if (legend.position === 'bottom' || !legend.position) { 301 | allLabels.forEach((entry) => { 302 | const line = makeLabelRow(entry); 303 | graph.push(toEmpty(graph[0].length, backgroundSymbol)); 304 | const y = graph.length - 1; 305 | line.forEach((symbol, i) => { 306 | drawPosition({ 307 | debugMode, 308 | graph, 309 | scaledX: i, 310 | scaledY: y, 311 | symbol, 312 | }); 313 | }); 314 | }); 315 | } 316 | }; 317 | 318 | /** 319 | * Adds a border around the graph. 320 | * @param {object} options - Object containing border options. 321 | * @param {Graph} options.graph - The graph array to modify. 322 | * @param {string} options.backgroundSymbol - The symbol to use for the background. 323 | * @param {string} options.borderSymbol - The symbol to use for the border. 324 | */ 325 | export const addBorder = ({ 326 | graph, 327 | borderSymbol, 328 | backgroundSymbol, 329 | }: { 330 | graph: Graph; 331 | borderSymbol: string; 332 | backgroundSymbol: string; 333 | }) => { 334 | const maxLength = Math.max(...graph.map((line) => line.length)); 335 | graph.forEach((line) => { 336 | while (line.length < maxLength) line.push(backgroundSymbol); 337 | line.unshift(borderSymbol); 338 | line.push(borderSymbol); 339 | }); 340 | graph.unshift(toEmpty(graph[0].length, borderSymbol)); 341 | graph.push(toEmpty(graph[0].length, borderSymbol)); 342 | }; 343 | 344 | /** 345 | * Fills the background of empty cells in the graph with a specified symbol. 346 | * @param {object} options - Object containing background fill options. 347 | * @param {Graph} options.graph - The graph array to modify. 348 | * @param {string} options.backgroundSymbol - Symbol to fill empty cells with. 349 | * @param {string} options.emptySymbol - Symbol representing empty cells. 350 | * @param {boolean} [options.debugMode=false] - If true, logs errors for out-of-bounds access. 351 | */ 352 | export const addBackgroundSymbol = ({ 353 | graph, 354 | backgroundSymbol, 355 | emptySymbol, 356 | debugMode, 357 | }: { 358 | graph: Graph; 359 | backgroundSymbol: string; 360 | emptySymbol: string; 361 | debugMode?: boolean; 362 | }) => { 363 | graph.forEach((line, curr) => { 364 | for (let index = 0; index < line.length; index += 1) { 365 | if (line[index] === emptySymbol) { 366 | drawPosition({ 367 | debugMode, 368 | graph, 369 | scaledX: index, 370 | scaledY: curr, 371 | symbol: backgroundSymbol, 372 | }); 373 | } else break; 374 | } 375 | }); 376 | }; 377 | 378 | /** 379 | * Adds points to the graph at specified (x, y) coordinates. 380 | * 381 | * @param {object} options - Configuration options. 382 | * @param {Graph} options.graph - The graph array to modify. 383 | * @param {GraphPoint[]} options.points - Points to render, with optional colors. 384 | * @param {number} options.plotWidth - Width of the plot. 385 | * @param {number} options.plotHeight - Height of the plot. 386 | * @param {number[]} options.expansionX - Range of x-values for scaling. 387 | * @param {number[]} options.expansionY - Range of y-values for scaling. 388 | * @param {string} options.pointSymbol - Symbol used to draw the point. 389 | * @param {boolean} [options.debugMode] - Enables debug logging. 390 | */ 391 | export const addPoints = ({ 392 | graph, 393 | points, 394 | plotWidth, 395 | plotHeight, 396 | expansionX, 397 | expansionY, 398 | pointSymbol, 399 | debugMode, 400 | }: { 401 | graph: Graph; 402 | points: GraphPoint[]; 403 | plotWidth: number; 404 | plotHeight: number; 405 | expansionX: number[]; 406 | expansionY: number[]; 407 | pointSymbol: string; 408 | debugMode?: boolean; 409 | }) => { 410 | const mappedPoints = points.map(({ x, y }) => [x, y] as Point); 411 | 412 | getPlotCoords(mappedPoints, plotWidth, plotHeight, expansionX, expansionY).forEach( 413 | ([x, y], pointNumber) => { 414 | const [scaledX, scaledY] = toPlot(plotWidth, plotHeight)(x, y); 415 | 416 | drawPosition({ 417 | debugMode, 418 | graph, 419 | scaledX: scaledX + 1, 420 | scaledY: scaledY + 1, 421 | symbol: points[pointNumber]?.color 422 | ? `${getAnsiColor(points[pointNumber]?.color || 'ansiRed')}${pointSymbol}\u001b[0m` 423 | : pointSymbol, 424 | }); 425 | }, 426 | ); 427 | }; 428 | 429 | /** 430 | * Draws threshold lines on the graph at specified x and/or y coordinates. 431 | * 432 | * @param {object} options - Configuration object for threshold rendering. 433 | * @param {Graph} options.graph - The 2D graph matrix to modify. 434 | * @param {Threshold[]} options.thresholds - List of threshold definitions with x, y, and optional color. 435 | * @param {object} options.axis - Axis configuration defining origin point. 436 | * @param {number} options.axis.x - X-position of the Y-axis on the graph. 437 | * @param {number} options.axis.y - Y-position of the X-axis on the graph. 438 | * @param {number} options.plotWidth - Width of the plot area in characters. 439 | * @param {number} options.plotHeight - Height of the plot area in characters. 440 | * @param {number[]} options.expansionX - Original data range for the X-axis, used for scaling. 441 | * @param {number[]} options.expansionY - Original data range for the Y-axis, used for scaling. 442 | * @param {typeof THRESHOLDS} options.thresholdSymbols - Symbols used to draw horizontal and vertical threshold lines. 443 | * @param {boolean} [options.debugMode=false] - Enables debug logging for invalid coordinates or out-of-bounds access. 444 | */ 445 | export const addThresholds = ({ 446 | graph, 447 | thresholds, 448 | axis, 449 | plotWidth, 450 | plotHeight, 451 | expansionX, 452 | expansionY, 453 | thresholdSymbols, 454 | debugMode, 455 | }: { 456 | graph: Graph; 457 | thresholds: Threshold[]; 458 | axis: { x: number; y: number }; 459 | plotWidth: number; 460 | plotHeight: number; 461 | expansionX: number[]; 462 | expansionY: number[]; 463 | thresholdSymbols: typeof THRESHOLDS; 464 | debugMode?: boolean; 465 | }) => { 466 | const mappedThreshold = thresholds.map(({ x: thresholdX, y: thresholdY }) => { 467 | let { x, y } = axis; 468 | 469 | if (thresholdX) { 470 | x = thresholdX; 471 | } 472 | if (thresholdY) { 473 | y = thresholdY; 474 | } 475 | return [x, y] as Point; 476 | }); 477 | 478 | getPlotCoords(mappedThreshold, plotWidth, plotHeight, expansionX, expansionY).forEach( 479 | ([x, y], thresholdNumber) => { 480 | const [scaledX, scaledY] = toPlot(plotWidth, plotHeight)(x, y); 481 | 482 | if (thresholds[thresholdNumber]?.x && graph[0][scaledX]) { 483 | graph.forEach((_, index) => { 484 | if (graph[index][scaledX]) { 485 | drawPosition({ 486 | debugMode, 487 | graph, 488 | scaledX: scaledX + 1, 489 | scaledY: index, 490 | symbol: thresholds[thresholdNumber]?.color 491 | ? `${getAnsiColor(thresholds[thresholdNumber]?.color || 'ansiRed')}${thresholdSymbols.y}\u001b[0m` 492 | : thresholdSymbols.y, 493 | }); 494 | } 495 | }); 496 | } 497 | if (thresholds[thresholdNumber]?.y && graph[scaledY]) { 498 | graph[scaledY].forEach((_, index) => { 499 | if (graph[scaledY][index]) { 500 | drawPosition({ 501 | debugMode, 502 | graph, 503 | scaledX: index, 504 | scaledY: scaledY + 1, 505 | symbol: thresholds[thresholdNumber]?.color 506 | ? `${getAnsiColor(thresholds[thresholdNumber]?.color || 'ansiRed')}${thresholdSymbols.x}\u001b[0m` 507 | : thresholdSymbols.x, 508 | }); 509 | } 510 | }); 511 | } 512 | }, 513 | ); 514 | }; 515 | 516 | /** 517 | * Fills the area below chart symbols with the specified area symbol. 518 | * @param {object} options - Object containing fill options. 519 | * @param {Graph} options.graph - The graph array to modify. 520 | * @param {Symbols['chart']} options.chartSymbols - Chart symbols to use for filling. 521 | * @param {boolean} [options.debugMode=false] - If true, logs errors for out-of-bounds access. 522 | */ 523 | export const setFillArea = ({ 524 | graph, 525 | chartSymbols, 526 | debugMode, 527 | }: { 528 | graph: Graph; 529 | chartSymbols: Symbols['chart']; 530 | debugMode?: boolean; 531 | }) => { 532 | graph.forEach((xValues, yIndex) => { 533 | xValues.forEach((xSymbol, xIndex) => { 534 | let areaSymbol = chartSymbols?.area || CHART.area; 535 | if ( 536 | xSymbol === chartSymbols?.nse || 537 | xSymbol === chartSymbols?.wsn || 538 | xSymbol === chartSymbols?.we || 539 | xSymbol === areaSymbol 540 | ) { 541 | if (graph[yIndex + 1]?.[xIndex]) { 542 | drawPosition({ 543 | debugMode, 544 | graph, 545 | scaledX: xIndex, 546 | scaledY: yIndex + 1, 547 | symbol: areaSymbol, 548 | }); 549 | } 550 | } 551 | }); 552 | }); 553 | }; 554 | 555 | /** 556 | * Removes any completely empty lines from the graph. 557 | * @param {object} options - Object containing empty line removal options. 558 | * @param {Graph} options.graph - The graph array to modify. 559 | * @param {string} options.backgroundSymbol - Background symbol for identifying empty lines. 560 | */ 561 | export const removeEmptyLines = ({ 562 | graph, 563 | backgroundSymbol, 564 | }: { 565 | graph: Graph; 566 | backgroundSymbol: string; 567 | }) => { 568 | const elementsToRemove: number[] = []; 569 | graph.forEach((line, position) => { 570 | if (line.every((symbol) => symbol === backgroundSymbol)) { 571 | elementsToRemove.push(position); 572 | } 573 | 574 | if (graph.every((currentLine) => currentLine[0] === backgroundSymbol)) { 575 | graph.forEach((currentLine) => currentLine.shift()); 576 | } 577 | }); 578 | 579 | elementsToRemove.reverse().forEach((position) => { 580 | graph.splice(position, 1); 581 | }); 582 | }; 583 | 584 | /** 585 | * Returns a label transformation function using the specified formatter. 586 | * @param {object} options - Object containing formatter options. 587 | * @param {Formatter} [options.formatter] - Formatter function to apply to labels. 588 | * @returns {Formatter} - A formatter function for transforming labels. 589 | */ 590 | export const getTransformLabel = 591 | ({ formatter }: { formatter?: Formatter }) => 592 | (value: number, helpers: FormatterHelpers) => 593 | formatter ? formatter(value, helpers) : defaultFormatter(value, helpers); 594 | -------------------------------------------------------------------------------- /src/services/plot.ts: -------------------------------------------------------------------------------- 1 | import { getPlotCoords, toPlot, toSorted, getAxisCenter } from './coords'; 2 | import { getChartSymbols } from './settings'; 3 | import { SingleLine, Plot } from '../types/index'; 4 | import { 5 | addBackgroundSymbol, 6 | addBorder, 7 | addLegend, 8 | addThresholds, 9 | addXLable, 10 | addYLabel, 11 | setTitle, 12 | setFillArea, 13 | removeEmptyLines, 14 | getTransformLabel, 15 | addPoints, 16 | } from './overrides'; 17 | import { getSymbols, getChartSize, getLabelShift, getInput } from './defaults'; 18 | 19 | import { 20 | drawAxis, 21 | drawGraph, 22 | drawChart, 23 | drawCustomLine, 24 | drawLine, 25 | drawShift, 26 | drawTicks, 27 | drawAxisCenter, 28 | } from './draw'; 29 | 30 | export const plot: Plot = ( 31 | rawInput, 32 | { 33 | color, 34 | width, 35 | height, 36 | yRange, 37 | showTickLabel, 38 | hideXAxisTicks, 39 | hideYAxisTicks, 40 | customXAxisTicks, 41 | customYAxisTicks, 42 | axisCenter, 43 | formatter, 44 | lineFormatter, 45 | symbols, 46 | title, 47 | fillArea, 48 | hideXAxis, 49 | hideYAxis, 50 | xLabel, 51 | yLabel, 52 | legend, 53 | thresholds, 54 | points, 55 | debugMode, 56 | mode = 'line', 57 | } = {}, 58 | ) => { 59 | // Multiline 60 | let input = getInput({ rawInput }); 61 | 62 | // Filter out points that are outside of the yRange (if defined) 63 | if (yRange) { 64 | const [yMin, yMax] = yRange; 65 | input = input.map((line) => line.filter(([, y]) => y >= yMin && y <= yMax)); 66 | } 67 | 68 | // Empty input, return early 69 | if (input.length === 0) { 70 | return ''; 71 | } 72 | 73 | // Proceed with the rest of your function as usual 74 | const transformLabel = getTransformLabel({ formatter }); 75 | 76 | let scaledCoords = [[0, 0]]; 77 | 78 | const { minX, minY, plotWidth, plotHeight, expansionX, expansionY } = getChartSize({ 79 | width, 80 | height, 81 | input, 82 | yRange, 83 | axisCenter, 84 | }); 85 | const { 86 | axisSymbols, 87 | emptySymbol, 88 | backgroundSymbol, 89 | borderSymbol, 90 | thresholdSymbols, 91 | pointSymbol, 92 | } = getSymbols({ symbols }); 93 | 94 | // create placeholder 95 | const graph = drawGraph({ plotWidth, plotHeight, emptySymbol }); 96 | 97 | const axis = getAxisCenter(axisCenter, plotWidth, plotHeight, expansionX, expansionY, [ 98 | 0, 99 | graph.length - 1, 100 | ]); 101 | 102 | // get default chart symbols 103 | input.forEach((coords: SingleLine, series) => { 104 | // override default chart symbols with colored ones 105 | const chartSymbols = getChartSymbols(color, series, symbols?.chart, input, fillArea); 106 | 107 | // sort input by the first value 108 | const sortedCoords = toSorted(coords); 109 | 110 | scaledCoords = getPlotCoords(sortedCoords, plotWidth, plotHeight, expansionX, expansionY).map( 111 | ([x, y], index, arr) => { 112 | const toPlotCoordinates = toPlot(plotWidth, plotHeight); 113 | const [scaledX, scaledY] = toPlotCoordinates(x, y); 114 | if (!lineFormatter) { 115 | drawLine({ 116 | mode, 117 | debugMode, 118 | index, 119 | arr, 120 | graph, 121 | scaledX, 122 | scaledY, 123 | plotHeight, 124 | emptySymbol, 125 | chartSymbols, 126 | axis, 127 | axisCenter, 128 | }); 129 | 130 | // fill empty area under the line if fill area is true 131 | if (fillArea) { 132 | setFillArea({ graph, chartSymbols, debugMode }); 133 | } 134 | } else { 135 | drawCustomLine({ 136 | debugMode, 137 | sortedCoords, 138 | scaledX, 139 | scaledY, 140 | input, 141 | index, 142 | lineFormatter, 143 | graph, 144 | toPlotCoordinates, 145 | expansionX, 146 | expansionY, 147 | minY, 148 | minX, 149 | }); 150 | } 151 | 152 | return [scaledX, scaledY]; 153 | }, 154 | ); 155 | }); 156 | 157 | if (thresholds) { 158 | addThresholds({ 159 | thresholdSymbols, 160 | debugMode, 161 | graph, 162 | thresholds, 163 | axis, 164 | plotWidth, 165 | plotHeight, 166 | expansionX, 167 | expansionY, 168 | }); 169 | } 170 | 171 | if (points) { 172 | addPoints({ 173 | pointSymbol, 174 | debugMode, 175 | graph, 176 | points, 177 | plotWidth, 178 | plotHeight, 179 | expansionX, 180 | expansionY, 181 | }); 182 | } 183 | 184 | // axis 185 | drawAxis({ 186 | debugMode, 187 | graph, 188 | hideXAxis, 189 | hideYAxis, 190 | axisCenter, 191 | axisSymbols, 192 | axis, 193 | }); 194 | 195 | // takes the longest label that needs to be rendered 196 | // on the Y axis and returns it's length 197 | const { xShift, yShift } = getLabelShift({ 198 | input, 199 | transformLabel, 200 | showTickLabel, 201 | expansionX, 202 | expansionY, 203 | minX, 204 | }); 205 | 206 | // shift graph 207 | let { realXShift } = drawShift({ 208 | graph, 209 | plotWidth, 210 | emptySymbol, 211 | scaledCoords, 212 | xShift, 213 | yShift, 214 | }); 215 | 216 | // apply background symbol if override 217 | if (backgroundSymbol) { 218 | addBackgroundSymbol({ debugMode, graph, backgroundSymbol, emptySymbol }); 219 | } 220 | 221 | // axis 222 | drawAxisCenter({ 223 | realXShift, 224 | debugMode, 225 | emptySymbol, 226 | backgroundSymbol, 227 | graph, 228 | axisSymbols, 229 | axis, 230 | }); 231 | 232 | // draw axis ends and ticks 233 | drawTicks({ 234 | input, 235 | graph, 236 | plotWidth, 237 | plotHeight, 238 | axis, 239 | axisCenter, 240 | yShift, 241 | emptySymbol, 242 | debugMode, 243 | hideXAxis, 244 | expansionX, 245 | expansionY, 246 | hideYAxis, 247 | customYAxisTicks, 248 | customXAxisTicks, 249 | hideYAxisTicks, 250 | hideXAxisTicks, 251 | showTickLabel, 252 | axisSymbols, 253 | transformLabel, 254 | }); 255 | 256 | // Remove empty lines 257 | removeEmptyLines({ graph, backgroundSymbol }); 258 | 259 | // Adds x axis label below the graph 260 | if (xLabel) { 261 | addXLable({ 262 | debugMode, 263 | xLabel, 264 | graph, 265 | backgroundSymbol, 266 | plotWidth, 267 | yShift, 268 | }); 269 | } 270 | 271 | // Adds x axis label below the graph 272 | if (yLabel) { 273 | addYLabel({ 274 | debugMode, 275 | yLabel, 276 | graph, 277 | backgroundSymbol, 278 | }); 279 | } 280 | 281 | if (legend) { 282 | addLegend({ 283 | points, 284 | thresholds, 285 | pointSymbol, 286 | debugMode, 287 | input, 288 | graph, 289 | legend, 290 | backgroundSymbol, 291 | color, 292 | symbols, 293 | fillArea, 294 | }); 295 | } 296 | 297 | // Adds title above the graph 298 | if (title) { 299 | setTitle({ 300 | debugMode, 301 | title, 302 | graph, 303 | backgroundSymbol, 304 | plotWidth, 305 | yShift, 306 | }); 307 | } 308 | 309 | if (borderSymbol) { 310 | addBorder({ graph, borderSymbol, backgroundSymbol }); 311 | } 312 | 313 | return drawChart({ graph }); 314 | }; 315 | 316 | export default plot; 317 | -------------------------------------------------------------------------------- /src/services/settings.ts: -------------------------------------------------------------------------------- 1 | import { CHART } from '../constants/index'; 2 | import { Color, ColorGetter, Formatter, MultiLine } from '../types/index'; 3 | 4 | const colorMap: Record = { 5 | ansiBlack: '\u001b[30m', 6 | ansiRed: '\u001b[31m', 7 | ansiGreen: '\u001b[32m', 8 | ansiYellow: '\u001b[33m', 9 | ansiBlue: '\u001b[34m', 10 | ansiMagenta: '\u001b[35m', 11 | ansiCyan: '\u001b[36m', 12 | ansiWhite: '\u001b[37m', 13 | }; 14 | 15 | /** 16 | * Maps a color name to its corresponding ANSI color code. 17 | * @param {Color} color - The color to map. 18 | * @returns {string} - The ANSI escape code for the specified color. 19 | */ 20 | export const getAnsiColor = (color: Color): string => colorMap[color] || colorMap.ansiWhite; 21 | 22 | /** 23 | * Configures and applies colors to chart symbols based on the specified color or series. 24 | * @param {Color | Color[] | ColorGetter | undefined} color - The color setting for the series. 25 | * @param {number} series - The index of the series. 26 | * @param {Partial | undefined} chartSymbols - Custom symbols for the chart, if any. 27 | * @param {MultiLine} input - The dataset used in the chart. 28 | * @param {boolean} [fillArea] - If true, fills the area under the plot with the chart's fill symbol. 29 | * @returns {object} - An object with chart symbols applied in the specified color(s). 30 | */ 31 | export const getChartSymbols = ( 32 | color: Color | Color[] | undefined | ColorGetter, 33 | series: number, 34 | chartSymbols: Partial | void, 35 | input: MultiLine, 36 | fillArea?: boolean, 37 | ) => { 38 | const chart = { ...CHART, ...chartSymbols }; 39 | 40 | // Apply the fill area symbol to all chart symbols if fillArea is true 41 | if (fillArea) { 42 | Object.entries(chart).forEach(([key]) => { 43 | chart[key as keyof typeof chart] = chart.area; 44 | }); 45 | } 46 | 47 | // Determine the color for the current series and apply it to all chart symbols 48 | if (color) { 49 | let currentColor: Color = 'ansiWhite'; 50 | 51 | if (Array.isArray(color)) { 52 | currentColor = color[series]; 53 | } else if (typeof color === 'function') { 54 | currentColor = color(series, input); 55 | } else { 56 | currentColor = color; 57 | } 58 | 59 | Object.entries(chart).forEach(([key, symbol]) => { 60 | chart[key as keyof typeof chart] = `${getAnsiColor(currentColor)}${symbol}\u001b[0m`; 61 | }); 62 | } 63 | 64 | return chart; 65 | }; 66 | 67 | /** 68 | * Formats a number for display, converting values >= 1000 to a "k" notation. 69 | * @param {number} value - The value to format. 70 | * @returns {string | number} - The formatted value as a string with "k" for thousands or as a rounded number. 71 | */ 72 | export const defaultFormatter: Formatter = (value) => { 73 | if (Math.abs(value) >= 1000) { 74 | const rounded = value / 1000; 75 | return rounded % 1 === 0 ? `${rounded}k` : `${rounded.toFixed(3)}k`; 76 | } 77 | return Number(value.toFixed(3)); 78 | }; 79 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { AXIS, CHART, THRESHOLDS } from '../constants'; 2 | 3 | /** 4 | * Represents a point with x and y coordinates. 5 | */ 6 | export type Point = [x: number, y: number]; 7 | 8 | /** 9 | * A point that may contain undefined values or be completely undefined. 10 | */ 11 | export type MaybePoint = Point | undefined | [number | undefined, number | undefined]; 12 | 13 | /** 14 | * A series of connected points representing a single line on a graph. 15 | */ 16 | export type SingleLine = Point[]; 17 | 18 | /** 19 | * A collection of single lines, used for plotting multiple lines on a graph. 20 | */ 21 | export type MultiLine = SingleLine[]; 22 | 23 | /** 24 | * Coordinates, which can be either a single line or multiple lines. 25 | */ 26 | export type Coordinates = SingleLine | MultiLine; 27 | 28 | /** 29 | * Represents ANSI colors for styling output. 30 | */ 31 | export type Color = `ansi${ 32 | | 'Red' 33 | | 'Green' 34 | | 'Black' 35 | | 'Yellow' 36 | | 'Blue' 37 | | 'Magenta' 38 | | 'Cyan' 39 | | 'White'}`; 40 | 41 | /** 42 | * Arguments required for custom line formatting. 43 | */ 44 | export type LineFormatterArgs = { 45 | x: number; // x-coordinate of the point 46 | y: number; // y-coordinate of the point 47 | plotX: number; // x-coordinate on the plot 48 | plotY: number; // y-coordinate on the plot 49 | input: SingleLine; // line input containing points 50 | index: number; // index of the current point 51 | minY: number; // minimum y-value 52 | minX: number; // minimum y-value 53 | expansionX: number[]; // expansion values for x-axis 54 | expansionY: number[]; // expansion values for y-axis 55 | toPlotCoordinates: (x: number, y: number) => Point; // function to convert coordinates to plot coordinates 56 | }; 57 | 58 | /** 59 | * Custom symbol with specified coordinates and symbol character. 60 | */ 61 | export type CustomSymbol = { x: number; y: number; symbol: string }; 62 | 63 | /** 64 | * Helpers for formatting, providing axis and range information. 65 | */ 66 | export type FormatterHelpers = { 67 | axis: 'x' | 'y'; // axis type ('x' or 'y') 68 | xRange: number[]; // range of x-values for scaling 69 | yRange: number[]; // range of y-values for scaling 70 | }; 71 | 72 | /** 73 | * Symbols for customizing chart appearance, including axis, chart, and background symbols. 74 | */ 75 | export type Symbols = { 76 | axis?: Partial; // Custom axis symbols 77 | chart?: Partial; // Custom chart symbols 78 | empty?: string; // Symbol representing empty space 79 | background?: string; // Symbol for background 80 | border?: string; // Symbol for border 81 | thresholds?: Partial; // Custom threshold symbols 82 | point?: string; // Symbol for points 83 | }; 84 | 85 | /** 86 | * Function type for formatting numbers on the chart. 87 | */ 88 | export type Formatter = (value: number, helpers: FormatterHelpers) => number | string; 89 | 90 | /** 91 | * Configuration for the legend display on the chart. 92 | */ 93 | export type Legend = { 94 | position?: 'left' | 'right' | 'top' | 'bottom'; // Legend position 95 | series?: string | string[]; // Series labels in the legend 96 | points?: string | string[]; // Points labels in the legend 97 | thresholds?: string | string[]; // Thresholds labels in the legend 98 | }; 99 | 100 | /** 101 | * Threshold definition with optional x, y coordinates and a color. 102 | */ 103 | export type Threshold = { 104 | x?: number; // x-coordinate threshold 105 | y?: number; // y-coordinate threshold 106 | color?: Color; // Color for threshold line or point 107 | }; 108 | 109 | /** 110 | * Points definition with x, y coordinates and a color. 111 | */ 112 | export type GraphPoint = { 113 | x: number; // x-coordinate 114 | y: number; // y-coordinate 115 | color?: Color; // Color for point 116 | }; 117 | 118 | /** 119 | * Function type for dynamically determining color based on series and coordinates. 120 | */ 121 | export type ColorGetter = (series: number, coordinates: MultiLine) => Color; 122 | 123 | /** 124 | * Color options, which can be a single color, an array of colors, or a color getter function. 125 | */ 126 | export type Colors = Color | Color[] | ColorGetter; 127 | 128 | /** 129 | * A 2D array representing the grid of symbols for the graph display. 130 | */ 131 | export type Graph = string[][]; 132 | 133 | /** 134 | * Graph mode options for rendering the graph. 135 | * 'line' mode connects points with lines. 136 | * 'point' mode displays points without connecting lines. 137 | * line is the default mode. 138 | */ 139 | export type GraphMode = 'line' | 'point' | 'bar' | 'horizontalBar'; 140 | 141 | /** 142 | * Configuration settings for rendering a plot. 143 | */ 144 | export type Settings = { 145 | color?: Colors; // Colors for the plot lines or areas 146 | width?: number; // Width of the plot 147 | height?: number; // Height of the plot 148 | yRange?: [number, number]; // Range of y-axis values 149 | showTickLabel?: boolean; // Option to show tick labels on the axis 150 | hideXAxis?: boolean; // Option to hide the x-axis 151 | hideXAxisTicks?: boolean; // Option to hide the x-axis ticks 152 | hideYAxis?: boolean; // Option to hide the y-axis 153 | hideYAxisTicks?: boolean; // Option to hide the y-axis ticks 154 | customXAxisTicks?: number[]; // Custom values for x-axis ticks 155 | customYAxisTicks?: number[]; // Custom values for y-axis ticks 156 | title?: string; // Title of the plot 157 | xLabel?: string; // Label for the x-axis 158 | yLabel?: string; // Label for the y-axis 159 | thresholds?: Threshold[]; // Array of threshold lines 160 | points?: GraphPoint[]; // Array of points to plot 161 | fillArea?: boolean; // Option to fill the area under lines 162 | legend?: Legend; // Legend settings 163 | axisCenter?: MaybePoint; // Center point for axes alignment 164 | formatter?: Formatter; // Custom formatter for axis values 165 | lineFormatter?: (args: LineFormatterArgs) => CustomSymbol | CustomSymbol[]; // Custom line formatter 166 | symbols?: Symbols; // Custom symbols for chart elements 167 | mode?: GraphMode; // Option to enable debug mode 168 | debugMode?: boolean; // Option to enable debug mode 169 | }; 170 | 171 | /** 172 | * Plot function type that takes coordinates and settings to generate a graph output. 173 | */ 174 | export type Plot = (coordinates: Coordinates, settings?: Settings) => string; 175 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "allowJs": true, 5 | "esModuleInterop": true, 6 | "strict": true, 7 | "moduleResolution": "node", 8 | "downlevelIteration": true, 9 | "target": "esnext", 10 | "declaration": true 11 | }, 12 | "include": ["src"] 13 | } 14 | --------------------------------------------------------------------------------