Skip to content

Instantly share code, notes, and snippets.

@Draco18s
Last active December 29, 2022 01:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Draco18s/dae0a0d37988f058cd4f9428b801411d to your computer and use it in GitHub Desktop.
Save Draco18s/dae0a0d37988f058cd4f9428b801411d to your computer and use it in GitHub Desktop.
/**
Requires color.js https://gist.github.com/Draco18s/df3a75fbf965556c528061b4b014a49b
Graph drawing function intended for Bitburner.
Bar graphs should be drawn first so that their color can be used as a background color maintaing the graph when later line graphs are drawn
StockPosition should be drawn last so that it can use special characters when intersecting with line graphs.
StockPosition should also not take Red or Green as its line color as it uses those to colorize buy and sell entries. Black is also used for position == 0 along with a dashed character.
Maximum width is approximately 120 before linewrapping occurs.
Examples:
Bar and Line
▁ ▁⎼
█ ▁⎼
█⎼
▁███
▁⎼▁███▁
▁⎼ ▁█████▁
▁⎼ ▁███████▁
▁⎼ ▁▅█████████▅▁
▁⎼▃▄▅▇█████████████▇▅▄
Stock position
╒══
═══════════╕ │
╘┈┈┈┈┈┈┈┈═══════════╛
*/
/** @param {NS} ns */
import { Color } from "/functions/color.js";
//import { Lerp } from "/functions/util.js";
//Lerp can be referenced via an import instead
function Lerp(v0, v1, t) {
return v0*(1-t)+v1*t
}
// Windows character combining and font widths sometimes don't work right
// If not on Windows, set this to false and things'll probably look nicer
// If things still don't line up, go to line 218 and adjust with the
// available characters on 219-223.
// Characters on 219 may not be in increasing hight on any given OS.
const WindowsOS = true;
export const DrawType = {
Default: 0,
FullCharacterLine: 1,
SubcharacterLine: 2,
Bar: 3,
StockOptions: 4,
}
export class GraphOptions {
/**
* @type {Number}
* @public
*/
height = 10; //number of lines
/**
* @type {Number}
* @public
*/
width = 115;
/**
* @type {Number}
* @public
*/
fidelity = 1; //lerping between samples
/**
*
*/
shareScale = true;
}
/**
* @class
*/
export class GraphData {
/**
* @type {Number}
* @public
*/
sortOrder;
/**
* @type {Number[]}
* @public
*/
data;
/**
* @type {Color}
* @public
*/
color;
/**
* @type {DrawType}
* @public
*/
drawType = DrawType.FullCharacterLine;
Min() {
return Math.min(...this.data);
}
Max() {
return Math.max(...this.data);
}
}
/**
* @param {GraphData[]} dataStreams
* @param {GraphOptions} options
* @returns {string[][]}
*/
export function DrawGraph(dataStreams, options, NS = null) {
dataStreams = dataStreams.sort((a, b) => a.sortOrder > b.sortOrder ? 1 : -1);
var graph = [];
for (var h = 0; h < options.height; h++) {
var p = [];
graph.push(p);
for (var w = 0; w < options.width; w++) {
graph[h].push(" ");
}
}
var minVal;
var maxVal;
if (options.shareScale) {
if (NS) NS.print(dataStreams.map(x => x.Min()));
minVal = Math.min(...dataStreams.map(x => x.Min()));
maxVal = Math.max(...dataStreams.map(x => x.Max()));
}
var dataSize = Math.floor(options.width / options.fidelity);
for (var d in dataStreams) {
var data = dataStreams[d];
if (!options.shareScale) {
minVal = data.Min();
maxVal = data.Max();
}
if (minVal == maxVal && data.drawType == DrawType.StockOptions) {
minVal = 0;
}
else if(minVal == maxVal) {
var t = maxVal*0.1;
minVal -= t;
maxVal += t;
}
var step = Math.abs((maxVal - minVal) / (options.height-1));
var bigstep = Math.abs((maxVal - minVal) / (options.height));
if(step <= 0) step = 1;
if(bigstep <= 0) bigstep = 1;
var x = 0;
for (var i = Math.max(1, data.data.length - dataSize); i < data.data.length; i++) {
var p1 = data.data[i - 1];
var p2 = data.data[i];
if (p1 === '' || p2 === '') {
x++;
continue;
}
for (var slice = 0; slice < options.fidelity; slice++) {
var y = (Lerp(p1, p2, (slice) / options.fidelity) - minVal) / (data.drawType == DrawType.StockOptions ? step : bigstep);
var f = (y - Math.floor(y));
y = Math.floor(y);
var slope = (p2 - p1) / step;
if (step == 0) {
y = 0;
slope = 0;
f = 0;
}
if(y >= graph.length) {
y--;
f = 1 - Number.EPSILON;
}
if(y >= graph.length || Number.isNaN(y)) {
if(NS)
NS.print(`Lerp(${p1}, ${p2}, (${slice}/${options.fidelity})-${minVal}) / ${step}`);
continue;
}
var v = graph[y];
v[x * options.fidelity + slice] = GetCharForFrac(data.drawType, f, data.color, v[x * options.fidelity + slice]);
while (data.drawType == DrawType.Bar && --y >= 0) {
graph[y][x * options.fidelity + slice] = GetCharForFrac(data.drawType, .99, data.color, p2);
}
if (data.drawType == DrawType.StockOptions) {
y = (p1 - minVal) / step;
y = Math.floor(y);
var tbm = 3;
if (slope > 0) tbm = tbmBot;
if (slope < 0) tbm = tbmTop;
var strrr = '';
try {
while (data.drawType == DrawType.StockOptions && y >= 0 && y < graph.length) {
strrr = y;
graph[y][x * options.fidelity + slice] = GetCharForStock(data.color, slope, slice, options.fidelity, p1, p2, minVal, step, tbm, graph[y][x * options.fidelity + slice], NS);
tbm = tbmMid;
if (y == Math.floor((p2 - minVal) / step) || y < 0 || y >= graph.length) break;
if (Math.sign(Math.trunc(slope)) == 0) {
if (Math.floor((p1 - minVal) / step) == Math.floor((p2 - minVal) / step))
break;
}
y += Math.sign(slope);
if (y == Math.floor((p2 - minVal) / step)) tbm = (slope > 0) ? tbmTop : tbmBot;
}
}
catch (e) {
return strrr + ': ' + graph.length + '\n' + e;
}
}
}
x++;
}
}
var output = [];
graph = graph.reverse();
for (var g in graph) {
output.push(graph[g].join(''));
}
return '\n' + output.join('\n');
}
const lineSegments = WindowsOS ? '▁_⚊―—━▔‾' : '▁⎯⚊⎽⎼─⎻⎺';
//▁_⎽⚊―—–⎼‒━─⎯⎻⎺▔‾
//⎽⎼⎻⎺ too short on Windows 5:6
//ᐨ too short on Windows 3:4
//㇐ too long on Windows 10:6
//ꣻ too long on Windows 9:10
const barSegments = '▁▂▃▄▅▆▇█';
function GetCharForFrac(type, frac, color, prevStr) {
var resetNeed = '';
var prevChar = prevStr[prevStr.length - 1];
frac = Math.floor(frac * 8);
if (prevChar == '█') {
//prevStr = prevStr.substring(0,2) + prevStr[3] + 'm';
color = prevStr.substring(0, 2) + '4' + prevStr[3] + ';3' + color[3] + 'm'; //color[0] + '[3' + color[3] + ';4' + prevStr[3] + 'm';
resetNeed = Color.reset;
//
}
if (type == DrawType.SubcharacterLine) {
if (frac >= 0 && frac <= 8) return color + lineSegments[frac] + resetNeed;
return color + '-' + resetNeed;
}
if (type == DrawType.FullCharacterLine) {
return color + '#' + resetNeed;//*-+=.#
}
if (type == DrawType.Bar) {
if (frac >= 0 && frac <= 8) return color + barSegments[frac] + resetNeed;
return color + '-' + resetNeed;
}
return prevStr + resetNeed;
}
let tbmTop = 2;
let tbmMid = 1;
let tbmBot = 0;
function GetCharForStock(color, slope, slice, numSlices, inVal, finVal, minv, step, tbm, prevStr) {
var prevChar = prevStr[prevStr.length - 1];
var approxFrac = lineSegments.indexOf(prevChar);
var useUnderlineInstead = approxFrac < 3;
var useOverlineInstead = approxFrac > 4;
var ret = GetCharForStockInner(color, slope, slice, numSlices, inVal, finVal, minv, step, tbm, prevStr);
return ret;
}
function GetCharForStockInner(color, slope, slice, numSlices, inVal, finVal, minv, step, tbm, prevStr) {
var bar = '';//Color.reset;
var prevChar = prevStr[prevStr.length - 1];
var prevIndex = lineSegments.indexOf(prevChar);
var prevIsLine = prevIndex >= 0;
var underline = false;
if(prevIsLine && prevIndex >= 3 && prevIndex <= 5) {
bar = WindowsOS ? '' : '̶';
prevIsLine = false;
}
//`\x1b[3${color};4m${newchar}${Color.reset}`
if(prevIsLine && prevIndex <= 3) {
prevIsLine = false;
underline = true;
}
if(prevIsLine && prevIndex >= 6) {
prevIsLine = false;
bar = '̄';
bar = `${bar}${bar}`;
underline = false;
}
var sl = Math.sign(slope);
slope = Math.sign(Math.trunc(slope));
if (slope == 0 && Math.floor((inVal - minv) / step) != Math.floor((finVal - minv) / step)) {
slope = sl;
}
//return slope;
if (slice == Math.floor(numSlices / 2)) {
if (tbm == tbmBot && slope >= 1) {
var color2 = Color.green;
return (underline ? `\x1b[3${color2[3]};4m` : color2) + (WindowsOS ? bar : '') + (prevIsLine ? '┴' : '╛') + (WindowsOS ? '' : bar) + (underline ? Color.reset : '');//'╛';
}
if (tbm == tbmTop && slope >= 1) {
var color2 = Color.green;
return (underline ? `\x1b[3${color2[3]};4m` : color2) + (WindowsOS ? bar : '') + (prevIsLine ? '┬' : '╒') + (WindowsOS ? '' : bar) + (underline ? Color.reset : '');//'╒';
}
if (tbm == tbmTop && slope <= -1) {
var color2 = Color.red;
return (underline ? `\x1b[3${color2[3]};4m` : color2) + (WindowsOS ? bar : '') + (prevIsLine ? '┬' : '╕') + (WindowsOS ? '' : bar) + (underline ? Color.reset : '');//'╕';
}
if (tbm == tbmBot && slope <= -1) {
var color2 = Color.red;
return (underline ? `\x1b[3${color2[3]};4m` : color2) + (WindowsOS ? bar : '') + (prevIsLine ? '┴' : '╘') + (WindowsOS ? '' : bar) + (underline ? Color.reset : '');//'╘';
}
if (tbm == tbmMid) {
if (slope >= 1) {
var color2 = Color.green;
return (underline ? `\x1b[3${color2[3]};4m` : color2) + (WindowsOS ? bar : '') + (prevIsLine ? '┼' : '│') + (WindowsOS ? '' : bar) + (underline ? Color.reset : '');
}
if (slope <= -1) {
var color2 = Color.red;
return (underline ? `\x1b[3${color2[3]};4m` : color2) + (WindowsOS ? bar : '') + (prevIsLine ? '┼' : '│') + (WindowsOS ? '' : bar) + (underline ? Color.reset : '');
}
}
}
if (sl == 0) //return prevIsLine>=0 ? prevStr : (finVal < 1 ? (underline ? `\x1b[30;4m` : Color.black) + '┈' : (underline ? `\x1b[3${color[3]};4m` : color) + '═'+bar);
{
/*
(WindowsOS ? bar : '') + (orig) + (WindowsOS ? '' : bar)
*/
if (finVal < 1) {
return prevIndex >= 0 ? prevStr : (underline ? `\x1b[30;4m` : Color.black) + '┈';
}
return prevIsLine ? prevStr : ((WindowsOS ? bar : '') + (underline ? `\x1b[3${color[3]};4m` : color) + '═' + (WindowsOS ? '' : bar)) + (underline ? Color.reset : '');//
}
if (slice < Math.floor(numSlices / 2) && tbm == tbmBot && slope >= 1) {
var color2 = inVal < 1 ? Color.black : Color.green;
return (underline ? `\x1b[3${color2[3]};4m` : color2) + (WindowsOS ? bar : '') + (prevIsLine ? prevChar : '═') + (WindowsOS ? '' : bar) + (underline ? Color.reset : '');
}
if (slice > Math.floor(numSlices / 2) && tbm == tbmTop && slope >= 1) {
var color2 = Color.green;
return (underline ? `\x1b[3${color2[3]};4m` : color2) + (WindowsOS ? bar : '') + (prevIsLine ? prevChar : '═') + (WindowsOS ? '' : bar) + (underline ? Color.reset : '');
}
if (slice < Math.floor(numSlices / 2) && tbm == tbmTop && slope <= -1) {
var color2 = Color.red;
return (underline ? `\x1b[3${color2[3]};4m` : color2) + (WindowsOS ? bar : '') + (prevIsLine ? prevChar : '═') + (WindowsOS ? '' : bar) + (underline ? Color.reset : '');
}
if (slice > Math.floor(numSlices / 2) && tbm == tbmBot && slope <= -1) {
var color2 = finVal < 1 ? Color.black : Color.red;
return (underline ? `\x1b[3${color2[3]};4m` : color2) + (WindowsOS ? bar : '') + (prevIsLine ? prevChar : '═') + (WindowsOS ? '' : bar) + (underline ? Color.reset : '');
}
var color2 = sl < 1 ? (finVal < 1 && slice == numSlices-1 ? Color.black : Color.red) : (inVal < 1 && slice == 0 ? Color.black : Color.green);
if (slope == 0) return prevIsLine ? prevStr : ((WindowsOS ? bar : '')+(underline ? `\x1b[3${color2[3]};4m` : color2) + '═') + (WindowsOS ? '' : bar) + (underline ? Color.reset : '');
return prevStr;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment