Skip to content

Instantly share code, notes, and snippets.

@mtmccrea
Last active September 24, 2022 10:30
Show Gist options
  • Save mtmccrea/c085627a31c835e5ac1ee80a91287996 to your computer and use it in GitHub Desktop.
Save mtmccrea/c085627a31c835e5ac1ee80a91287996 to your computer and use it in GitHub Desktop.
A proposed solution to the `GridLines` taxonomy.
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