kroitor / asciichart

Nice-looking lightweight console ASCII line charts ╭┈╯ for NodeJS, browsers and terminal, no dependencies
MIT License
1.83k stars 94 forks source link

Way of labeling x axis #56

Open jackHedaya opened 3 years ago

jackHedaya commented 3 years ago

Hey!

I think it would be really awesome to be able to label the x axis. Are there any plans of supporting this?

kroitor commented 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...

jackHedaya commented 3 years ago

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 })) 
chrispahm commented 3 years ago

Hey 👋

For those interested, I wrote a helper function for adding (numeric) x-labels to an asciichart: https://next.observablehq.com/@chrispahm/hello-asciichart

image

I guess it's not polished enough to do a PR, but maybe it's still useful for anyone looking for that functionality!

kroitor commented 3 years ago

@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!

masautt commented 2 years ago

Any update on this implementation?

alexkli commented 1 year ago

FWIW, I took @chrispahm 's work (thanks!) and extended it a bit, so that it supports:

Here is an example screenshot:

Bildschirm­foto 2022-11-21 um 16 39 14

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

kroitor commented 1 year ago

@alexkli thank you for sharing it!