-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
first draft of geom ribbon #63
base: master
Are you sure you want to change the base?
Conversation
First of all thanks a lot! This is great and was definitely missing. It's a really great idea to just insert the min values at the beginning and the max values at the end to avoid having to do weird things to actually fill the resulting polygon! I think the drawing code can be simplified significantly, but the rest is mostly fine as it is. I'll comment the code specifically.
To be fair, this probably often due to two reasons:
So given that I'm really happy to see that you were able to do this in the first place. It means to me that probably the code isn't as horrible anymore as it was, haha.
Do you mean just the fact I mentioned above? That is the setting being called The former, as I said, is just inconsistent and should be changed. The latter is by design, because I consider the name
Do you mean the whole So for instance in edit: I accidentally pressed Ctrl+Enter when I wasn't done yet... |
Aside from the mentioned changes, it'd be great if you could make your example a recipe, by adding it to To fully add a recipe you also have to:
Especially if you've never written Org files or used And finally a changelog entry would be nice. And feel free to tag the PR and your github tag in the changelog, which will make it easier to thank the contributors when a new full release is done. If you don't want to do any of the above, it's fine and I'll do it after merging. And again: thanks a lot for your efforts! |
const RibbonDefaultStyle = Style(lineWidth: 1.0, | ||
lineType: ltNone, | ||
color: transparent, | ||
fillColor: grey20) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would propose default fillColor
of
const grey70 = color(0.7, 0.7, 0.7)
,
which you could add above these default styles definitions. The other consts are defined in ginger
at the moment. This is pretty annoying, which is why I wanted to change it anyways. At some point it's probably wise to put all those consts for colors (and possibly way more?) into this repository in its own file or something.
With this default grey it would look nice even without alpha.
if fg.geom.aes.yMax.isSome: | ||
aesyMax = evaluate(fg.geom.aes.yMax.unsafeGet.col, df) | ||
else: | ||
var xT = df[$fg.xcol].toTensor(Value) | ||
var yT = df[$fg.ycol].toTensor(Value) | ||
var aesyMax, aesyMin: Tensor[Value] | ||
if fg.geom.aes.yMin.isSome: | ||
aesyMin = evaluate(fg.geom.aes.yMin.unsafeGet.col, df).toTensor(Value) | ||
if fg.geom.aes.yMax.isSome: | ||
aesyMax = evaluate(fg.geom.aes.yMax.unsafeGet.col, df).toTensor(Value) | ||
if fg.geom.kind == gkRibbon: | ||
if not (fg.geom.aes.yMin.isSome and fg.geom.aes.yMax.isSome): | ||
echo "WARNING: using geom ribbon requires min and max aesthetics!" | ||
for i in 0 ..< df.len: | ||
if styles.len > 1: | ||
style = mergeUserStyle(styles[i], fg.geom.userStyle, fg.geom.kind) | ||
# get current x, y values, possibly clipping them | ||
p = getXY(view, df, xT, yT, fg, i, theme, xOutsideRange, | ||
yOutsideRange, xMaybeString = true) | ||
if viewMap.len > 0: | ||
# get correct viewport if any is discrete | ||
viewIdx = getView(viewMap, p, fg) | ||
locView = view[viewIdx] | ||
if needBinWidth: | ||
# potentially move the positions according to `binPosition` | ||
binWidths = calcBinWidths(df, i, fg) | ||
moveBinPositions(p, binWidths, fg) | ||
pos = getDrawPos(locView, viewIdx, | ||
fg, | ||
p = p, | ||
binWidths = binWidths, | ||
df, i, | ||
prevVals) | ||
if fg.geom.kind != gkRibbon: # we handle the ribbon points separately below | ||
p = getXY(view, df, xT, yT, fg, i, theme, xOutsideRange, | ||
yOutsideRange, xMaybeString = true) | ||
if viewMap.len > 0: | ||
# get correct viewport if any is discrete | ||
viewIdx = getView(viewMap, p, fg) | ||
locView = view[viewIdx] | ||
if needBinWidth: | ||
# potentially move the positions according to `binPosition` | ||
binWidths = calcBinWidths(df, i, fg) | ||
moveBinPositions(p, binWidths, fg) | ||
pos = getDrawPos(locView, viewIdx, | ||
fg, | ||
p = p, | ||
binWidths = binWidths, | ||
df, i, | ||
prevVals) | ||
case fg.geom.position | ||
of pkIdentity: | ||
case fg.geom.kind | ||
of gkLine, gkFreqPoly: linePoints.add pos | ||
of gkRibbon: | ||
# add yMax points to end of linePoints | ||
# and add yMin points to beginning of linePoints | ||
block: # namespace hygiene required maybe for template from getXY? | ||
# add yMax point as next point | ||
p = getXY(view, df, xT, aesyMax, fg, i, theme, xOutsideRange, | ||
yOutsideRange, xMaybeString = true) | ||
if viewMap.len > 0: | ||
# get correct viewport if any is discrete | ||
viewIdx = getView(viewMap, p, fg) | ||
locView = view[viewIdx] | ||
if needBinWidth: | ||
# potentially move the positions according to `binPosition` | ||
binWidths = calcBinWidths(df, i, fg) | ||
moveBinPositions(p, binWidths, fg) | ||
pos = getDrawPos(locView, viewIdx, | ||
fg, | ||
p = p, | ||
binWidths = binWidths, | ||
df, i, | ||
prevVals) | ||
linePoints.add pos | ||
block: | ||
# add yMin point as very first point | ||
p = getXY(view, df, xT, aesyMin, fg, i, theme, xOutsideRange, | ||
yOutsideRange, xMaybeString = true) | ||
if viewMap.len > 0: | ||
# get correct viewport if any is discrete | ||
viewIdx = getView(viewMap, p, fg) | ||
locView = view[viewIdx] | ||
if needBinWidth: | ||
# potentially move the positions according to `binPosition` | ||
binWidths = calcBinWidths(df, i, fg) | ||
moveBinPositions(p, binWidths, fg) | ||
pos = getDrawPos(locView, viewIdx, | ||
fg, | ||
p = p, | ||
binWidths = binWidths, | ||
df, i, | ||
prevVals) | ||
linePoints.insert(pos,0) | ||
else: locView.draw(fg, pos, p.y, binWidths, df, i, style) | ||
of pkStack: | ||
case fg.geom.kind | ||
of gkLine, gkFreqPoly: linePoints.add pos | ||
of gkLine, gkFreqPoly, gkRibbon: linePoints.add pos |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of these changes, I propose the following. We already have readErrorBar
, which handles xMin, yMin, ... etc. for error bar plots. This is essentially all that's required. Therefore, the code in this proc can remain as is, instead of the blocks from 540 to 580 and line 584 we just write:
of gkRibbon: linePoints.addRibbonData(locView, pos, df, i, fg)
where addRibbonData
is a proc you define above:
proc addRibbonData(linePoints: var seq[Coord],
view: Viewport,
pos: Coord,
df: DataFrame,
idx: int,
fg: FilledGeom) =
## adds the error bars for the ribbon to the `linePoints`.
## TODO: handle stacked positions properly?
template toC1(val: float, axKind: AxisKind): Coord1D =
block:
var res: Coord1D
case axKind
of akX:
res = Coord1D(pos: val,
scale: view.xScale,
axis: akX,
kind: ukData)
of akY:
res = Coord1D(pos: val,
scale: view.yScale,
axis: akY,
kind: ukData)
res
let (xMin, xMax, yMin, yMax) = readErrorData(df, idx, fg)
var posMin: Coord
var posMax: Coord
posMin.x = if xMin.isSome: toC1(xMin.unsafeGet, akX) else: pos.x
posMin.y = if yMin.isSome: toC1(yMin.unsafeGet, akY) else: pos.y
posMax.x = if xMax.isSome: toC1(xMax.unsafeGet, akX) else: pos.x
posMax.y = if yMax.isSome: toC1(yMax.unsafeGet, akY) else: pos.y
linePoints.add posMax
linePoints.insert(posMin, 0)
And remove the added changes at the beginning of the proc.
This has the added benefit of also supporting ribbons for xMin, xMax
too (and even weird combinations, which will result in some funky plots I assume), and instead of adding checks along the lines of only x or y related procs, I'd either add:
- a check for either xMin, xMax / yMin, yMax to
geom_ribbon
- just explain in the doc string for
geom_ribbon
that the user should either use this or that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change requires two small changes in different files.
- in
ggplot_types.nim
you need to extend thegeomKind
field for theFilledGeom
to includegkRibbon
to thegkErrorBar
case, so thatxMin
etc. fields are defined forgkRibbon
. - in
postprocess_scales.nim
infillOptFields
you have to do the same, addgkRibbon
to thegkErrorBar
case so that these fields are actually filled and we don't have to access theaes
of the rawgeom
.
Thanks for the detailed feedback - just what I was looking for.
Hah, I appreciate the positivity, but I'm not so sure, since insertions are probably quite costly (looking) yep, it's a while loop O(n) for each insertion. I'll probably refactor to resize
Great! That's what I was hoping for.
True! But it took me way longer than I thought just to make sense of what was going on. It would be nice to have a rough guide for each of how to add geoms, shapes, themes, etc. but I understand the code is in flux so it's not yet worth the effort of making a quickly outdated guide.
Yes, the ggplot2 inconsistency, and that it was being followed only sometimes. I'm totally on board with using
No, the Question on backends:
I'll do that.
I'll do that - org files and tangling don't look too complicated. Thanks again for responding so quickly and giving detailed feedback. I'll get to this on the weekend at the latest. |
Oh well sure, the implementation using
which is neat, because then the filling of the polygon is natural. Feel free to optimize it, otherwise I'll think of something.
So lately I feel like the current design has proven to be performant enough, while being relatively elegant in terms of DRY violations etc. (which doesn't imply simple though).
I suppose there's some argument to be had about improving some things from
It probably doesn't help that there's a
This was indeed something I thought about originally when I wrote the first data frame implementation. It wasn't a major aspect that changed my mind on whether to use arraymancer or not for it, but I knew from the code for my PhD that arraymancer does unfortunately take some time to compile.
Yeah, no rush! And also, feel free to ping me on Nim's IRC channel or just send me a message on gitter. |
I actually tried to allow |
64fd62f
to
11455aa
Compare
[feature request] geom_ribbon
So this is sort of a feature request, but I also took a first stab at implementing it and, behold, here is a working version. Discussion and suggestions very welcome. I still don't understand lots of ggplotnim, like there are some things I am confused about the convention like why ggplot() uses
color
andfill
and everything else changes it tocolor
andfillColor
? Or how should the interfacing with the arraymancer backend be done correctly and for consistency? You'll see my messy block of code about this!Weird things to look for:
aesyMin
,aesyMax
and fill them if their aesthetics exist throughisSome
.ltNone
.linePoints
Example