Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Way of labeling x axis #56

Open
jackHedaya opened this issue Dec 27, 2020 · 7 comments
Open

Way of labeling x axis #56

jackHedaya opened this issue Dec 27, 2020 · 7 comments

Comments

@jackHedaya
Copy link

Hey!

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

@kroitor kroitor self-assigned this Dec 27, 2020
@kroitor
Copy link
Owner

kroitor commented Dec 27, 2020

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
Copy link
Author

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
Copy link

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
Copy link
Owner

kroitor commented Mar 30, 2021

@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
Copy link

masautt commented May 27, 2022

Any update on this implementation?

@alexkli
Copy link

alexkli commented Nov 18, 2022

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

  • display of x axis, which should reliably show the highest x value
  • custom width scaling for the console output: width
  • label for x axis: xLabel
  • label for y axis: yLabel
  • label for lines (legend): lineLabels
  • title line centered above: title

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
Copy link
Owner

kroitor commented Nov 18, 2022

@alexkli thank you for sharing it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants