Last active
December 29, 2022 23:51
-
-
Save Fordi/22cf46883020907b2998b1f45226c85d to your computer and use it in GitHub Desktop.
Draw a line graph in Chrome/Chromium's dev console
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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(' ') | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Images of the examples in action