Skip to main content
Glama
scatter-plot.ts6.53 kB
import { ChartData, ChartResult } from '../types/index.js'; import { colorize } from '../utils/colors.js'; import { ASCII_CHARS, normalize, clamp, createGrid, gridToString, center, padLeft } from '../utils/ascii.js'; export interface ScatterPlotOptions { pointChar?: string; showTrendLine?: boolean; } export function createScatterPlot(data: ChartData, options: ScatterPlotOptions = {}): ChartResult { const { data: values, title, width = 60, height = 15, color = 'white' } = data; const { pointChar = ASCII_CHARS.point, showTrendLine = false } = options; if (values.length === 0) { throw new Error('Data array cannot be empty'); } const chartWidth = width - 10; // Reserve space for y-axis labels const chartHeight = height - 3; // Reserve space for x-axis and title // Create x-values (indices) and y-values (data) const xValues = values.map((_, i) => i); const yValues = values; const minX = Math.min(...xValues); const maxX = Math.max(...xValues); const minY = Math.min(...yValues); const maxY = Math.max(...yValues); // const xRange = maxX - minX || 1; const yRange = maxY - minY || 1; // Create the chart grid const grid = createGrid(width, height); // Draw y-axis labels and grid lines for (let y = 0; y < chartHeight; y++) { const value = maxY - (y / (chartHeight - 1)) * yRange; const label = value.toFixed(1); const labelStr = padLeft(label, 8); // Place y-axis label for (let i = 0; i < Math.min(labelStr.length, 8); i++) { if (8 - i < width) { grid[y][8 - i] = labelStr[i]; } } // Draw y-axis line if (9 < width) { grid[y][9] = y === chartHeight - 1 ? ASCII_CHARS.bottomLeft : y === 0 ? ASCII_CHARS.topLeft : ASCII_CHARS.teeRight; } // Draw horizontal grid lines for (let x = 10; x < width; x++) { if (y === chartHeight - 1) { grid[y][x] = ASCII_CHARS.horizontal; } else if (y % 3 === 0) { grid[y][x] = '·'; // Light grid dots } } } // Draw x-axis labels const labelStep = Math.max(1, Math.floor(xValues.length / 8)); for (let i = 0; i < xValues.length; i += labelStep) { const x = 10 + Math.floor(normalize(xValues[i], minX, maxX) * (chartWidth - 1)); const label = xValues[i].toString(); if (x + label.length <= width && height - 1 >= 0) { for (let j = 0; j < label.length && x + j < width; j++) { grid[height - 1][x + j] = label[j]; } } } // Plot points const plotPoints: { x: number; y: number; originalX: number; originalY: number }[] = []; for (let i = 0; i < values.length; i++) { const normalizedX = normalize(xValues[i], minX, maxX); const normalizedY = normalize(yValues[i], minY, maxY); const plotX = 10 + Math.floor(normalizedX * (chartWidth - 1)); const plotY = Math.floor((1 - normalizedY) * (chartHeight - 1)); const x = clamp(plotX, 10, width - 1); const y = clamp(plotY, 0, chartHeight - 1); plotPoints.push({ x, y, originalX: xValues[i], originalY: yValues[i] }); // Draw the point if (x < width && y < chartHeight) { grid[y][x] = pointChar; } } // Draw trend line if requested if (showTrendLine && values.length > 1) { const trendLine = calculateLinearRegression(plotPoints.map(p => p.originalX), plotPoints.map(p => p.originalY)); drawTrendLine(grid, trendLine, minX, maxX, minY, maxY, chartWidth, chartHeight); } // Convert grid to string and apply coloring let chart = gridToString(grid); if (color !== 'white') { chart = colorize(chart, color); } // Add title if provided if (title) { const titleLine = center(title, width); chart = titleLine + '\n' + chart; } return { chart, title, dimensions: { width, height } }; } interface LinearRegression { slope: number; intercept: number; rSquared: number; } function calculateLinearRegression(xValues: number[], yValues: number[]): LinearRegression { const n = xValues.length; const sumX = xValues.reduce((a, b) => a + b, 0); const sumY = yValues.reduce((a, b) => a + b, 0); const sumXY = xValues.reduce((sum, x, i) => sum + x * yValues[i], 0); const sumXX = xValues.reduce((sum, x) => sum + x * x, 0); // const sumYY = yValues.reduce((sum, y) => sum + y * y, 0); const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX); const intercept = (sumY - slope * sumX) / n; // Calculate R-squared const yMean = sumY / n; const ssRes = yValues.reduce((sum, y, i) => { const predicted = slope * xValues[i] + intercept; return sum + Math.pow(y - predicted, 2); }, 0); const ssTot = yValues.reduce((sum, y) => sum + Math.pow(y - yMean, 2), 0); const rSquared = 1 - (ssRes / ssTot); return { slope, intercept, rSquared }; } function drawTrendLine( grid: string[][], trendLine: LinearRegression, minX: number, maxX: number, minY: number, maxY: number, chartWidth: number, chartHeight: number ): void { const { slope, intercept } = trendLine; // Calculate start and end points of trend line const startY = slope * minX + intercept; const endY = slope * maxX + intercept; // Convert to grid coordinates const startGridX = 10; const endGridX = 10 + chartWidth - 1; const startGridY = Math.floor((1 - normalize(startY, minY, maxY)) * (chartHeight - 1)); const endGridY = Math.floor((1 - normalize(endY, minY, maxY)) * (chartHeight - 1)); // Draw line using Bresenham's algorithm drawLine(grid, startGridX, startGridY, endGridX, endGridY, chartWidth, chartHeight); } function drawLine( grid: string[][], x1: number, y1: number, x2: number, y2: number, maxWidth: number, maxHeight: number ): void { const dx = Math.abs(x2 - x1); const dy = Math.abs(y2 - y1); const sx = x1 < x2 ? 1 : -1; const sy = y1 < y2 ? 1 : -1; let err = dx - dy; let x = x1; let y = y1; // eslint-disable-next-line no-constant-condition while (true) { // Draw line character (only if cell is empty or has grid dots) if (x >= 10 && x < maxWidth + 10 && y >= 0 && y < maxHeight) { if (grid[y][x] === ' ' || grid[y][x] === '·') { grid[y][x] = ASCII_CHARS.horizontal; } } if (x === x2 && y === y2) break; const e2 = 2 * err; if (e2 > -dy) { err -= dy; x += sx; } if (e2 < dx) { err += dx; y += sy; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gianlucamazza/mcp-ascii-charts'

If you have feedback or need assistance with the MCP directory API, please join our Discord server