Skip to content
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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/ggplotnim.nim
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,40 @@ proc geom_line*(aes: Aesthetics = aes(),
statKind: stKind)
assignBinFields(result, stKind, bins, binWidth, breaks)

proc geom_ribbon*(aes: Aesthetics = aes(),
data = DataFrame(),
color = none[Color](), # color of the line
size = none[float](), # line width of the line
lineType = none[LineType](),
fillColor = none[Color](),
alpha = none[float](),
bins = 30,
binWidth = 0.0,
breaks: seq[float] = @[],
position = "identity",
stat = "identity",
binPosition = "none"
): Geom =
let dfOpt = if data.len > 0: some(data) else: none[DataFrame]()
let pkKind = parseEnum[PositionKind](position)
let stKind = parseEnum[StatKind](stat)
let bpKind = parseEnum[BinPositionKind](binPosition)
let style = initGgStyle(lineType = lineType,
lineWidth = size,
color = color,
fillColor = fillColor,
alpha = alpha)
let gid = incId()
result = Geom(gid: gid,
data: dfOpt,
kind: gkRibbon,
aes: aes.fillIds({gid}),
userStyle: style,
position: pkKind,
binPosition: bpKind,
statKind: stKind)
assignBinFields(result, stKind, bins, binWidth, breaks)

proc geom_histogram*(aes: Aesthetics = aes(),
data = DataFrame(),
binWidth = 0.0, bins = 30,
Expand Down
99 changes: 77 additions & 22 deletions src/ggplotnim/ggplot_drawing.nim
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ proc getDrawPosImpl(
case fg.geom.kind
of gkPoint, gkErrorBar:
result = getDiscretePoint(fg, axKind)
of gkLine, gkFreqPoly:
of gkLine, gkFreqPoly, gkRibbon:
result = view.getDiscreteLine(axKind)
of gkHistogram, gkBar:
result = getDiscreteHisto(fg, width, axKind)
Expand All @@ -299,7 +299,7 @@ proc getDrawPosImpl(
case fg.geom.kind
of gkPoint, gkErrorBar:
result = view.getContinuous(fg, val, axKind)
of gkLine, gkFreqPoly:
of gkLine, gkFreqPoly, gkRibbon:
result = view.getContinuous(fg, val, axKind)
of gkHistogram, gkBar:
result = view.getContinuous(fg, val, axKind)
Expand Down Expand Up @@ -399,7 +399,7 @@ proc draw(view: var Viewport, fg: FilledGeom, pos: Coord,
quant(binWidth, ukData),
quant(-y.toFloat(allowNull = true), ukData),
style = some(style))
of gkLine, gkFreqPoly:
of gkLine, gkFreqPoly, gkRibbon:
doAssert false, "Already handled in `drawSubDf`!"
of gkTile:
view.addObj view.initRect(pos,
Expand All @@ -416,7 +416,7 @@ proc draw(view: var Viewport, fg: FilledGeom, pos: Coord,
proc calcBinWidths(df: DataFrame, idx: int, fg: FilledGeom): tuple[x, y: float] =
const CoordFlipped = false
case fg.geom.kind
of gkHistogram, gkBar, gkPoint, gkLine, gkFreqPoly, gkErrorBar, gkText:
of gkHistogram, gkBar, gkPoint, gkLine, gkFreqPoly, gkRibbon, gkErrorBar, gkText:
when not CoordFlipped:
result.x = readOrCalcBinWidth(df, idx, fg.xcol, dcKind = fg.dcKindX)
else:
Expand Down Expand Up @@ -496,37 +496,92 @@ proc drawSubDf[T](view: var Viewport, fg: FilledGeom,
when defined(defaultBackend):
var xT = df[$fg.xcol]
var yT = df[$fg.ycol]
var aesyMin, aesyMax: type(xT)
if fg.geom.aes.yMin.isSome:
aesyMin = evaluate(fg.geom.aes.yMin.unsafeGet.col, df)
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
Comment on lines +499 to +584
Copy link
Owner

@Vindaar Vindaar Apr 13, 2020

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.

Copy link
Owner

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 the geomKind field for the FilledGeom to include gkRibbon to the gkErrorBar case, so that xMin etc. fields are defined for gkRibbon.
  • in postprocess_scales.nimin fillOptFields you have to do the same, add gkRibbon to the gkErrorBar case so that these fields are actually filled and we don't have to access the aes of the raw geom.

else: locView.draw(fg, pos, p.y, binWidths, df, i, style)
of pkDodge:
discard
Expand All @@ -537,7 +592,7 @@ proc drawSubDf[T](view: var Viewport, fg: FilledGeom,
if viewMap.len == 0:
view = locView
# for `gkLine`, `gkFreqPoly` now draw the lines
if fg.geom.kind in {gkLine, gkFreqPoly}:
if fg.geom.kind in {gkLine, gkFreqPoly, gkRibbon}:
if styles.len == 1:
let style = mergeUserStyle(styles[0], fg.geom.userStyle, fg.geom.kind)
# connect line down to axis, if fill color is not transparent
Expand Down
6 changes: 6 additions & 0 deletions src/ggplotnim/ggplot_styles.nim
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const LineDefaultStyle = Style(lineWidth: 1.0,
size: 5.0, # used to draw error bar 'T' horizontal
color: grey20,
fillColor: transparent)
const RibbonDefaultStyle = Style(lineWidth: 1.0,
lineType: ltNone,
color: transparent,
fillColor: grey20)
Copy link
Owner

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.

const BarDefaultStyle = Style(lineWidth: 1.0,
lineType: ltSolid,
color: grey20,
Expand All @@ -42,6 +46,8 @@ func defaultStyle(geomKind: GeomKind): Style =
result = PointDefaultStyle
of gkLine, gkFreqPoly, gkErrorBar:
result = LineDefaultStyle
of gkRibbon:
result = RibbonDefaultStyle
of gkBar:
result = BarDefaultStyle
of gkHistogram:
Expand Down
2 changes: 1 addition & 1 deletion src/ggplotnim/ggplot_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ type
font*: Option[Font]

GeomKind* = enum
gkPoint, gkBar, gkHistogram, gkFreqPoly, gkTile, gkLine, gkErrorBar, gkText
gkPoint, gkBar, gkHistogram, gkFreqPoly, gkTile, gkLine, gkRibbon, gkErrorBar, gkText
Geom* = object
gid*: uint16 # unique id of the geom
data*: Option[DataFrame] # optionally a geom may have its own data frame
Expand Down
2 changes: 1 addition & 1 deletion src/ggplotnim/postprocess_scales.nim
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ proc postProcessScales*(filledScales: var FilledScales, p: GgPlot) =
var df = if g.data.isSome: g.data.get else: p.data
var filledGeom: FilledGeom
case g.kind
of gkPoint, gkLine, gkErrorBar, gkTile, gkText:
of gkPoint, gkLine, gkRibbon, gkErrorBar, gkTile, gkText:
# can be handled the same
# need x and y data for sure
case g.statKind
Expand Down