Skip to content

Instantly share code, notes, and snippets.

@balain
Last active February 13, 2021 01:16
Show Gist options
  • Save balain/d47428ee2d5a96993f1e224336d3dd5b to your computer and use it in GitHub Desktop.
Save balain/d47428ee2d5a96993f1e224336d3dd5b to your computer and use it in GitHub Desktop.
Slopegraph w/ normal or log scale
/**
* 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