Skip to content

Instantly share code, notes, and snippets.

@Fordi
Last active December 29, 2022 23:51
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Fordi/22cf46883020907b2998b1f45226c85d to your computer and use it in GitHub Desktop.
Save Fordi/22cf46883020907b2998b1f45226c85d to your computer and use it in GitHub Desktop.
Draw a line graph in Chrome/Chromium's dev console
/**
* Draw a line graph in Chrome/Chromium's dev console
* Examples:
* console.lineGraph([[5, 3], [10, 8], [-10, 1], [7, -3], [3, 0]], 320, 240);
* console.lineGraph(Math.sin, 420, 240, -Math.PI, Math.PI, 0.001);
* console.lineGraph(function (x) {
return [
Math.sqrt(1 - x*x) + Math.abs(x) - 0.75,
(Math.abs(x)-Math.sqrt(1 - x*x)) / 2 - 0.25
];
}, 240, 240, -1, 1, 0.001);
* @param f
* Function A generator function that accepts an index and returns a scalar or array of data
* Array An array of numbers or arrays of numbers;
* start, end, and step will default to 0, f.length - 1, and 1, respectively,
* if f is an Array. f[0] is the set of samples at index = 0.
* @param W
* Number graph's width in pixels
* @param H
* Number graph's height in pixels
* @param start
* Number First index (inclusive)
* @param end
* Number Maximum index (inclusive)
* @param step
* Number Amount by which index should be incremented per sample
*/
console.lineGraph = function (f, W, H, start, end, step) {
var colors = ['red','orange', 'brown', 'dkgreen', 'dkcyan', 'blue', 'indigo','violet'];
if (Array.isArray(f)) {
var DATA = f;
if (arguments.length < 4 || start == undefined) {
start = 0;
}
if (arguments.length < 5 || end == undefined) {
end = DATA.length - 1;
}
if (arguments.length < 6 || step == undefined) {
step = 1;
}
//Interpolation. This allows us to subsample arrays when a step of 1 would
// result in more loops than available points on the X axis.
f = function (x) {
if (x >= DATA.length) {
return DATA[DATA.length - 1];
}
if (x < 0) {
return DATA[0];
}
if (x === (x | 0)) {
return DATA[x];
}
var index = x | 0,
slope = x - index;
if (index >= DATA.length) {
return DATA[DATA.length];
} else
return DATA[index] * slope + DATA[index + 1] * (1 - slope);
}
}
var dw = (end - start);
//The trick here:
// While Chromium allows styles in logging, you can't change the `display` property,
// so we have to fake a block-level element to define a width.
//
// To get the right height, we just set the font-size to H, and divide by the default
// line-height (1.6666) to get a bounding box of the correct size.
//
// The metrics of Chrome's monospace font give the space a bounding aspect ratio of
// 14:16, when you include line height. We calculate the number of spaces needed
// to fix the graph, and simply ignore wrapping, since a developer is dealing with this,
// and can set W to something that makes sense.
var tW = Math.ceil(2 * W/(H * 0.875));
// The next trick: now that we have a suitable bounding box, we'll generate a line graph
// using an on-document canvas, and convert it to a data URL for use as a background.
var style = [
'font-size: ' + (H / 1.1666) + 'px'
];
// Get the data values, and work out the min/max of the graph
var rawData = [];
var min = Infinity;
var max = -Infinity;
//Subsample if step results in more loops than we've got width. Also cover undefined step here.
if (dw / step > W || !step) {
step = dw / W;
}
for (var i = start; i <= end; i += step) {
var si = Math.round((i - start) / step);
var d = f(i);
if (!Array.isArray(d)) {
d = [d];
}
for (var p = 0; p < d.length; p += 1) {
min = Math.min(d[p], min);
max = Math.max(d[p], max);
}
rawData[si] = d;
}
// Generate the canvas, and get a context for it.
var canvas = document.createElement('canvas');
canvas.width = W;
canvas.height = H;
var ctx = canvas.getContext('2d');
// Set basic line styling
ctx.fillStyle = 'none';
ctx.lineWidth = '1px';
//Transform and pivot the raw data, so that pointSeries[] is an array of point series
var pointSeries = [];
for (var i = start; i <= end; i += step) {
var si = Math.round((i - start) / step);
var x = Math.round((i - start) * (W - 1) / dw);
var d = rawData[si];
if (!Array.isArray(d)) {
d = [d];
}
for (var p = 0; p < d.length; p += 1) {
var y = (H - 1) - Math.round((d[p] - min) * (H - 1) / (max - min));
if (!pointSeries[p]) {
pointSeries[p] = [];
}
pointSeries[p].push([x, y]);
}
}
//Finally, draw the graph.
for (var ci = 0; ci < pointSeries.length; ci++) {
var points = pointSeries[ci];
ctx.strokeStyle = colors[ci % colors.length];
ctx.beginPath();
for (var pi = 0; pi < points.length; pi += 1) {
var x = points[pi][0];
var y = points[pi][1];
if (pi === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
}
// If the graph crosses zero, draw a line there for the X axis labels;
// otherwise, draw them on the base
ctx.font = '10px monospace';
var zero = H - 1;
var align = 'bottom';
if (max > 0 && min < 0) {
zero = (H - 1) - (0 - min) * (H - 1) / (max - min);
align = 'middle';
}
var sbox = ctx.measureText(start);
var ebox = ctx.measureText(end);
ctx.strokeStyle = 'black';
ctx.beginPath();
ctx.moveTo(sbox.width + 2, zero);
ctx.lineTo(W - 3 - ebox.width, zero);
ctx.stroke();
ctx.textBaseline = 'middle';
ctx.fillText(start, 0, zero);
ctx.fillText(end, W - 1 - ebox.width, zero);
//Draw the max and min of the graph.
ctx.textBaseline = 'top';
ctx.fillText(max, 0, 0);
ctx.textBaseline = 'bottom';
ctx.fillText(min, 0, H);
//Add the rendered canvas to the style, and write out the pseudo-block
style.push('background: url(' + canvas.toDataURL() + ') 0 0 no-repeat');
console.log('%c%s',
style.join(';'),
new Array(tW + 1).join(' ')
);
};
@Fordi
Copy link
Author

Fordi commented Jan 20, 2017

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