Last active
February 13, 2021 01:16
-
-
Save balain/d47428ee2d5a96993f1e224336d3dd5b to your computer and use it in GitHub Desktop.
Slopegraph w/ normal or log scale
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
/** | |
* slopegraph.js | |
* @author John D. Lewis | |
* @requires {@link p5.js} | |
* @requires {@link p5.dom.js} | |
* @file P5 script to create a slopegraph | |
* | |
* See also: | |
* - https://www.edwardtufte.com/bboard/q-and-a-fetch-msg?msg_id=0003nk | |
* - http://charliepark.org/slopegraphs/ | |
* - http://skedasis.com/d3/slopegraph/ | |
* - http://neoformix.com/Projects/ObesitySlope/ | |
* - http://stackoverflow.com/questions/10112866/how-to-create-a-slopegraph-in-d3-js#10377940 | |
* | |
* Input data format: | |
* CSV in ./data/slopedata.csv | |
* Header row: yes | |
* 3 columns: | |
* 1. Label | |
* 2. Initial value | |
* 3. Final value | |
*/ | |
/** | |
* Determine minimum value in p5.tableRows | |
* @var {Object} getMin | |
* @private | |
*/ | |
var getMin = ( pre, cur ) => Math.min( pre, cur ); | |
/** | |
* Determine maximum value in p5.tableRows | |
* @var {Object} getMax | |
* @private | |
*/ | |
var getMax = ( pre, cur ) => Math.max( pre, cur ); | |
/** | |
* Starting X coordinate for left-hand column | |
* @var {Object} x0 | |
* @private | |
*/ | |
var x0 = 200; | |
/** | |
* Starting X coordinate for right-hand column | |
* @var {Object} x1 | |
* @private | |
*/ | |
var x1 = 400; | |
/** | |
* X-axis buffer between data point and label | |
* @var {Object} shiftX | |
* @private | |
*/ | |
var shiftX = 5; | |
/** | |
* Cache of used X coordinates (left-hand data); used to avoid overlaps | |
* @var {Object} usedXLeft | |
* @private | |
*/ | |
var usedXLeft = []; | |
/** | |
* Cache of used X coordinates (right-hand data); used to avoid overlaps | |
* @var {Object} usedXRight | |
* @private | |
*/ | |
var usedXRight = []; | |
/** | |
* Display using log scale? | |
* @var {Object} useLog | |
* @private | |
*/ | |
var useLog = false; | |
var slopegraph = function (p5obj) { | |
/** | |
* Data points for slope graph | |
* @var {Object} slopedata | |
* @private | |
*/ | |
var slopedata; | |
var cwidth = 600, cheight = 600, margin = 50; | |
/** | |
* @function scaleY | |
* @param {integer} y - initial data point value | |
* @description Calculate the normal scale of the provided data value | |
*/ | |
var scaleY = function ( y ) { | |
var result = 0; | |
if (useLog) { | |
result = ( ( Math.log( cheight - (2 * margin)) / valuesMax ) * y ) + margin; | |
} else { | |
result = ( ( ( cheight - (2 * margin)) / valuesMax ) * y ) + margin; | |
} | |
return(result); | |
} | |
/** | |
* @function logscaleY | |
* @param {integer} y - initial data point value | |
* @description Calculate the log scale of the provided data value | |
*/ | |
var logscaleY = function ( y ) { | |
var y1 = y; | |
if (y1 > 0) { | |
y1 = Math.log(y); | |
} | |
var result = ( ( ( cheight - (2 * margin)) / valuesMaxLog ) * y1 ) + margin; | |
return(result); | |
} | |
/** | |
* Amount to shift overlapping labels down | |
* @var {Object} downShift | |
* @private | |
*/ | |
var downShift = 12; | |
/** | |
* @function calcY | |
* @param {integer} y - initial data point value | |
* @param {array} arr - array of cached values | |
* @description Determine if the provided value has been plotted before; if it has, shift it down | |
*/ | |
var calcY = function ( y, arr ) { | |
for (var i = 0; i < arr.length; i++) { | |
if (y == arr[i]) { | |
y = y + downShift; | |
return(y); | |
} | |
} | |
return(y); | |
} | |
/** | |
* @function plotYStart | |
* @param {integer} y - initial data point value | |
* @description Determine (and cache) the starting Y position | |
*/ | |
var plotYStart = function ( y ) { | |
y = calcY(y, usedXLeft); | |
usedXLeft.push(y); | |
return(y); | |
} | |
/** | |
* @function plotYEnd | |
* @param {integer} y - initial data point value | |
* @description Determine (and cache) the ending Y position | |
*/ | |
var plotYEnd = function ( y ) { | |
y = calcY(y, usedXRight); | |
usedXRight.push(y); | |
return(y); | |
} | |
/** | |
* @function plot | |
* @param {string} name - Name/label for the data point | |
* @param {integer} first - initial data point value | |
* @param {integer} last - final data point value | |
* @description Plot the data point | |
*/ | |
var plot = function ( name, first, last ) { | |
var itemColorR = p5obj.random(0, 150); | |
var itemColorG = p5obj.random(0, 150); | |
var itemColorB = p5obj.random(0, 150); | |
var itemColor = p5obj.color(itemColorR, itemColorG, itemColorB); | |
p5obj.noFill(); | |
p5obj.stroke(itemColor); | |
if (useLog) { | |
startY = cheight - logscaleY(first); | |
endY = cheight - logscaleY(last); | |
} else { | |
startY = cheight - scaleY(first); | |
endY = cheight - scaleY(last); | |
} | |
// Adjust vertically so text is middle-aligned-ish | |
p5obj.line( x0, startY, x1, endY); | |
p5obj.fill(itemColor); | |
p5obj.noStroke(); | |
var textXStart = x0 - shiftX; | |
var textXEnd = x1 + shiftX; | |
startY += 5; | |
endY += 5; | |
p5obj.textAlign(p5obj.RIGHT); | |
p5obj.text( name + " " + first + " ", textXStart, plotYStart(startY) ); | |
p5obj.textAlign(p5obj.LEFT); | |
p5obj.text( " " + last + " " + name, textXEnd, plotYEnd(endY) ); | |
} | |
/** | |
* @function preload | |
* @description Read in all the data from the CSV | |
*/ | |
p5obj.preload = function() { | |
slopedata = p5obj.loadTable("data/slopedata.csv", "csv", "header"); | |
} | |
/** | |
* @function toggleUseLog | |
* @description Handler function for useLog checkbox | |
*/ | |
function toggleUseLog() { | |
useLog = !useLog; | |
usedXLeft = []; | |
usedXRight = []; | |
p5obj.redraw(); | |
} | |
/** | |
* @function setup | |
* @description Standard P5 Setup function | |
* Determine min/max values | |
*/ | |
p5obj.setup = function() { | |
p5obj.createCanvas(cwidth, cheight); | |
// Enable toggling between normal/log scales | |
checkbox = p5obj.createCheckbox('Use Log Scale?', false); | |
checkbox.changed(toggleUseLog); | |
slopedataArray = slopedata.getArray(); | |
// Get min and max values of data columns | |
valuesNow = slopedata.getColumn("Now").map(Number); | |
valuesNowMin = valuesNow.reduce(getMin); | |
valuesNowMax = valuesNow.reduce(getMax); | |
valuesLater = slopedata.getColumn("Later").map(Number) | |
valuesLaterMin = valuesLater.reduce(getMin); | |
valuesLaterMax = valuesLater.reduce(getMax); | |
valuesMax = Math.max(valuesNowMax, valuesLaterMax); | |
valuesMin = Math.min(valuesNowMin, valuesLaterMin); | |
valuesMaxLog = Math.log(valuesMax); | |
if (valuesMin > 0) { | |
valuesMinLog = Math.log(valuesMin); | |
} else { | |
valuesMinLog = 0.001; | |
} | |
p5obj.noLoop(); | |
}; | |
/** | |
* @function draw | |
* @description Draws to the canvas | |
*/ | |
p5obj.draw = function() { | |
// TODO pull labels from data file | |
this.printChart("Now", "Later", 254); | |
}; | |
/** | |
* @function printChart | |
* @param {string} titleLeft - Name for the left-hand column | |
* @param {string} titleRight - Name for the right-hand column | |
* @param {string} background - Color for chart background | |
* @description Main display function for the chart data | |
*/ | |
printChart = function (titleLeft, titleRight, background) { | |
p5obj.background(background); | |
printScale(); | |
p5obj.fill(20); | |
p5obj.noStroke(); | |
// Print headers | |
p5obj.textAlign(p5obj.RIGHT); | |
p5obj.text(titleLeft, x0 - shiftX, margin/2 ); | |
p5obj.textAlign(p5obj.LEFT); | |
p5obj.text(titleRight, x1 + shiftX, margin/2 ); | |
// Print header underlines | |
var underlineLength = 50; | |
p5obj.stroke(1); | |
p5obj.line(x0 - underlineLength, margin/2 + 5, x0, margin/2 + 5); | |
p5obj.line(x1, margin/2 + 5, x1 + shiftX + underlineLength, margin/2 + 5); | |
// Print data | |
slopedataArray.forEach(function (el) { | |
plot(el[0], Number(el[1]), Number(el[2])); | |
}) | |
} | |
/** | |
* @function printScale | |
* @description Print the chart scale | |
*/ | |
printScale = function () { | |
// How many labels to print | |
labelCount = 3; | |
p5obj.fill(180); | |
p5obj.noStroke(); | |
// Calculate the vertical space between scale labels | |
deltaVal = valuesMax - valuesMin; | |
spacerVal = deltaVal/(labelCount+1); | |
if (useLog) { | |
startYMin = cheight - logscaleY(valuesMin); | |
startYMax = cheight - logscaleY(valuesMax); | |
delta = startYMin - startYMax; | |
spacer = Math.log(delta / (labelCount + 1)); | |
spacer = delta / (labelCount + 1); | |
} else { | |
startYMin = cheight - scaleY(valuesMin); | |
startYMax = cheight - scaleY(valuesMax); | |
delta = startYMin - startYMax; | |
spacer = delta / (labelCount + 1); | |
} | |
// Print min value | |
p5obj.text(valuesMin, 50, startYMin ); | |
// Print intermediate scale points | |
for (var i = 1; i <= labelCount; i++) { | |
yPos1 = startYMax + (i * spacer); | |
if (useLog) { | |
// TODO fix this | |
// p5obj.text(valuesMax - (spacerVal * i), 50, Math.log(yPos1)/Math.log(valuesMax)); | |
} else { | |
p5obj.text(valuesMax - (spacerVal * i), 50, yPos1); | |
} | |
} | |
// Print max value | |
p5obj.text(valuesMax, 50, startYMax ); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment