Open jackHedaya opened 3 years ago
Technically, since it's x-aligned, you can do that after plotting out the series. Also, those labels can vary a lot, could be dates, or timeframes, or arbitrary non-time units... So, it can be easily added in the userland. Since there's so much variation in horizontal labels, we would leave it to the user for now. If you can suggest a good compact way of plotting arbitrary horizontal labels, we will consider adding it.
Maybe some kind of a labels
option, that would contain the indexess in the data series and the corresponding marks or symbols or the horizontal axis...
I like that labels idea. Perhaps usage would look like so:
var s = []
for (var i = 0; i < 120; i++)
s[i] = 15 * Math.cos (i * ((Math.PI * 8) / 120))
// If labels returns undefined or false, the label is skipped. Otherwise plots.
console.log (asciichart.plot (s, { labels: (index) => index % 10 === 0 ? index: false }))
Hey 👋
For those interested, I wrote a helper function for adding (numeric) x-labels to an asciichart: https://next.observablehq.com/@chrispahm/hello-asciichart
I guess it's not polished enough to do a PR, but maybe it's still useful for anyone looking for that functionality!
@chrispahm thanks so much for your involvement! This is really cool! I will try to add that to the master in a generalized way ) Thx again!
Any update on this implementation?
FWIW, I took @chrispahm 's work (thanks!) and extended it a bit, so that it supports:
width
xLabel
yLabel
lineLabels
title
Here is an example screenshot:
Example config for the screenshot above (I just replaced the data with simpler arrays):
const plotConfig = {
title: "this is an interesting graph",
height: 15,
width: 100,
colors: [
plot.blue,
plot.green,
],
lineLabels: [
"precision",
"recall"
],
xLabel: "threshold",
yLabel: "percent"
};
console.log(plot.plot([[ 1, 2, 3], [ 4, 5, 6]], plotConfig));
Code - just use the plot()
function in place of asciichart.plot()
:
const asciichart = require ('asciichart');
const stripAnsi = require('strip-ansi');
const assert = require('assert');
function plot(yArray,config = {}) {
yArray = Array.isArray(yArray[0]) ? yArray : [yArray];
yArray.forEach(a => assert(a.length > 0, "Cannot plot empty array"));
const originalWidth = yArray[0].length;
if (config.width) {
yArray = yArray.map((arr) => {
const newArr = [];
for (let i = 0; i < config.width; i++) {
newArr.push(arr[Math.floor(i * arr.length/config.width)]);
}
return newArr;
});
}
const plot = asciichart.plot(yArray, config);
const xArray = config.xArray || (Array.isArray(yArray[0]) ? yArray[0] : yArray).map((v,i) => i);
// determine the overall width of the plot (in characters)
const plotFirstLine = stripAnsi(plot).split('\n')[0];
const fullWidth = plotFirstLine.length;
// get the number of characters reserved for the y-axis legend
const leftMargin = plotFirstLine.split(/┤|┼╮|┼/)[0].length + 1;
// the difference between the two is the actual width of the x axis
const widthXaxis = fullWidth - leftMargin;
// get the number of characters of the longest x-axis label
const longestXLabel = xArray.map(l => l.toString().length).sort((a,b) => b - a)[0]
const tickDistance = longestXLabel + 2;
let ticks = ' '.repeat(leftMargin-1);
for (let i = 0; i < widthXaxis; i++) {
if ((i % tickDistance === 0 && (i + tickDistance) < widthXaxis) || i === (widthXaxis-1)) {
ticks += "┬";
} else {
ticks += "─";
}
}
const lastTickValue = originalWidth - 1;
let tickLabels = ' '.repeat(leftMargin-1);
if (widthXaxis <= tickDistance) {
// too short, just last tick
tickLabels += (lastTickValue.toFixed()).padStart(widthXaxis - (tickLabels.length - leftMargin + 1));
} else {
for (let i = 0; i < widthXaxis; i++) {
const tickValue = Math.round(i/widthXaxis * originalWidth);
if ((i % tickDistance === 0 && (i + tickDistance) < widthXaxis)) {
tickLabels += tickValue.toFixed().padEnd(tickDistance);
// final tick
if (i >= (widthXaxis - 2 * tickDistance)) {
if (widthXaxis % tickDistance === 0) {
tickLabels += (lastTickValue.toFixed()).padStart(widthXaxis - (tickLabels.length - leftMargin + 1));
} else {
tickLabels += (lastTickValue.toFixed()).padStart(widthXaxis - (tickLabels.length - leftMargin + 1));
}
}
}
}
}
const title = config.title ? `${' '.repeat(leftMargin + (widthXaxis - config.title.length)/2)}${config.title}\n` : '';
let yLabel = '';
if (config.yLabel || Array.isArray(config.lineLabels)) {
if (config.yLabel) {
yLabel += `${asciichart.darkgray}${config.yLabel.padStart(leftMargin + config.yLabel.length/2)}${asciichart.reset}`;
}
if (Array.isArray(config.lineLabels)) {
let legend = '';
for (let i = 0; i < Math.min(yArray.length, config.lineLabels.length); i++) {
const color = Array.isArray(config.colors) ? config.colors[i] : asciichart.default;
legend += ` ${color}─── ${config.lineLabels[i]}${asciichart.reset}`;
}
yLabel += ' ' .repeat(fullWidth - 1 - stripAnsi(legend).length - stripAnsi(yLabel).length) + legend;
}
yLabel += `\n${'╷'.padStart(leftMargin)}\n`;
}
const xLabel = config.xLabel ? `\n${asciichart.darkgray}${config.xLabel.padStart(fullWidth - 1)}${asciichart.reset}` : '';
return `\n${title}${yLabel}${plot}\n${ticks}\n${tickLabels}${xLabel}\n`;
}
Feel free to use or extend this!
EDIT 1: added optional title
EDIT 2: fixed line label alignment
@alexkli thank you for sharing it!
Hey!
I think it would be really awesome to be able to label the x axis. Are there any plans of supporting this?