Last active
September 24, 2022 10:30
-
-
Save mtmccrea/c085627a31c835e5ac1ee80a91287996 to your computer and use it in GitHub Desktop.
A proposed solution to the `GridLines` taxonomy.
This file contains 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
AbstractGridLines { | |
var <>spec; | |
*new { arg spec; | |
^super.newCopyArgs(spec.asSpec).prCheckWarp; | |
} | |
asGrid { ^this } | |
niceNum { arg val,round; | |
// http://books.google.de/books?id=fvA7zLEFWZgC&pg=PA61&lpg=PA61 | |
var exp,f,nf,rf; | |
exp = floor(log10(val)); | |
f = val / 10.pow(exp); | |
rf = 10.pow(exp); | |
if(round,{ | |
if(f < 1.5,{ | |
^rf * 1.0 | |
}); | |
if(f < 3.0,{ | |
^rf * 2.0 | |
}); | |
if( f < 7.0,{ | |
^rf * 5.0 | |
}); | |
^rf * 10.0 | |
},{ | |
if(f <= 1.0,{ | |
^rf * 1.0; | |
}); | |
if(f <= 2,{ | |
^rf * 2.0 | |
}); | |
if(f <= 5,{ | |
^rf * 5.0; | |
}); | |
^rf * 10.0 | |
}); | |
} | |
ideals { arg min,max,ntick=5; | |
var nfrac,d,graphmin,graphmax,range,x; | |
range = this.niceNum(max - min,false); | |
d = this.niceNum(range / (ntick - 1),true); | |
graphmin = floor(min / d) * d; | |
graphmax = ceil(max / d) * d; | |
nfrac = max( floor(log10(d)).neg, 0 ); | |
^[graphmin,graphmax,nfrac,d]; | |
} | |
looseRange { arg min,max,ntick=5; | |
^this.ideals(min,max).at( [ 0,1] ) | |
} | |
getParams { ^this.subclassResponsibility } | |
formatLabel { arg val, numDecimalPlaces; | |
if (numDecimalPlaces == 0) { | |
^val.asInteger.asString | |
} { | |
^val.round( (10**numDecimalPlaces).reciprocal).asString | |
} | |
} | |
prCheckWarp { | |
if(this.isKindOf(this.spec.gridClass).not) { | |
format( | |
"% expects a spec with a corresponding warp type, " | |
"but was passed a spec with a %.", | |
this.class, this.spec.warp.class, | |
).warn | |
}; | |
} | |
} | |
GridLines { | |
// redirect to/return the class suited to the spec (determined by its warp) | |
*new { arg spec; | |
^spec.gridClass.newCopyArgs(spec.asSpec); | |
} | |
} | |
ExponentialGridLines : AbstractGridLines { | |
getParams { |valueMin, valueMax, pixelMin, pixelMax, numTicks, tickSpacing = 64| | |
var lines,p,pixRange; | |
var nfrac,d,graphmin,graphmax,range, nfracarr; | |
var nDecades, first, step, tick, expRangeIsValid, expRangeIsPositive, roundFactor; | |
pixRange = pixelMax - pixelMin; | |
lines = []; | |
nfracarr = []; | |
expRangeIsValid = ( | |
(valueMin > 0) and: { valueMax > 0 } | |
) or: { | |
(valueMin < 0) and: { valueMax < 0 } | |
}; | |
if(expRangeIsValid) { | |
expRangeIsPositive = valueMin > 0; | |
if(expRangeIsPositive) { | |
nDecades = log10(valueMax/valueMin); | |
first = step = 10**(valueMin.abs.log10.trunc); | |
roundFactor = step; | |
} { | |
nDecades = log10(valueMin/valueMax); | |
step = 10**(valueMin.abs.log10.trunc - 1); | |
first = 10 * step.neg; | |
roundFactor = 10**(valueMax.abs.log10.trunc); | |
}; | |
//workaround for small ranges | |
if(nDecades < 1) { | |
step = step * 0.1; | |
roundFactor = roundFactor * 0.1; | |
nfrac = valueMin.abs.log10.floor.neg + 1; | |
}; | |
numTicks ?? {numTicks = (pixRange / (tickSpacing * nDecades))}; | |
tick = first; | |
while ({ tick <= (valueMax + step) }) { | |
var drawLabel = true, maxNumTicks; | |
if(round(tick, roundFactor).inclusivelyBetween(valueMin, valueMax)) { | |
if((numTicks > 4) | |
or: { ((numTicks > 2.5).and(tick.abs.round(1).asInteger == this.niceNum(tick.abs, true).round(1).asInteger)).and(tick >= 1) } | |
or: { ((numTicks > 2).and((tick - this.niceNum(tick, true)).abs < 1e-15)) } | |
or: { (tick.abs.round(roundFactor).log10.frac < 0.01) } | |
or: { (tick.absdif(valueMax) < 1e-15) } | |
or: { (tick.absdif(valueMin) < 1e-15) } | |
) { | |
maxNumTicks = tickSpacing.linlin(32, 64, 8, 5, nil); | |
maxNumTicks = maxNumTicks * tick.asFloat.asString.bounds.width.linlin(24, 40, 0.7, 1.5); // 10.0.asString.bounds.width to 1000.0.asString.bounds.width | |
if( | |
(numTicks < maxNumTicks) and: | |
{ ((tick.abs.round(1).asInteger == this.niceNum(tick.abs, true).round(1).asInteger)).and(tick >= 1).not } and: | |
{ (((tick - this.niceNum(tick, true)).abs < 1e-15)).not } and: | |
{ (tick.abs.log10.frac > numTicks.linlin(4, maxNumTicks, 0.7, 0.93)) } | |
) { | |
drawLabel = false // drop labels for tightly spaced upper area of the decade | |
}; | |
lines = lines.add([tick, drawLabel]) | |
}; | |
}; | |
if(tick >= (step * 9.9999)) { step = (step * 10) }; | |
if(expRangeIsPositive) { | |
if((round(tick,roundFactor) >= (round(step*10,roundFactor))) and: { (nDecades > 1) }) { step = (step*10) }; | |
} { | |
if((round(tick.abs,roundFactor) <= (round(step,roundFactor))) and: { (nDecades > 1) }) { step = (step*0.1) }; | |
}; | |
tick = (tick+step); | |
}; | |
nfracarr = lines.collect({ arg arr; | |
var val = arr[0]; | |
val.abs.log10.floor.neg.max(0) | |
}); | |
} { | |
format("Unable to get exponential GridLines for values between % and %", valueMin, valueMax).warn; | |
numTicks ?? { | |
numTicks = (pixRange / tickSpacing); | |
numTicks = numTicks.max(3).round(1); | |
}; // set numTicks regardless to avoid errors | |
}; | |
p = (); | |
p['lines'] = lines.flop.first; | |
if(pixRange / numTicks > 9) { | |
if (sum(p['lines'] % 1) == 0) { nfrac = 0 }; | |
p['labels'] = lines.collect({ arg arr, inc; | |
var val, drawLabel, thisLabel; | |
#val, drawLabel = arr; | |
[val, this.formatLabel(val, nfrac ? nfracarr[inc] ? 1), nil, nil, drawLabel.not] }); | |
}; | |
^p | |
} | |
} | |
LinearGridLines : AbstractGridLines { | |
getParams { |valueMin, valueMax, pixelMin, pixelMax, numTicks, tickSpacing = 64| | |
var lines,p,pixRange; | |
var nfrac,d,graphmin,graphmax,range; | |
pixRange = pixelMax - pixelMin; | |
if(numTicks.isNil,{ | |
numTicks = (pixRange / tickSpacing); | |
numTicks = numTicks.max(3).round(1); | |
}); | |
# graphmin,graphmax,nfrac,d = this.ideals(valueMin,valueMax,numTicks); | |
lines = []; | |
if(d != inf,{ | |
forBy(graphmin,graphmax + (0.5*d),d,{ arg tick; | |
if(tick.inclusivelyBetween(valueMin,valueMax),{ | |
lines = lines.add( tick ); | |
}) | |
}); | |
}); | |
p = (); | |
p['lines'] = lines; | |
if(pixRange / numTicks > 9) { | |
if (sum(lines % 1) == 0) { nfrac = 0 }; | |
p['labels'] = lines.collect({ arg val; [val, this.formatLabel(val, nfrac)] }); | |
}; | |
^p | |
} | |
} | |
// BlankGridLines will result from a nil spec arg, nil.asSpec is \linear | |
BlankGridLines : LinearGridLines { | |
getParams { ^() } | |
} | |
+ Nil { | |
asGrid { ^BlankGridLines.new } | |
gridClass { ^BlankGridLines } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment