From 3fdc6502c20b3ae6bb86e1c58d11c2e6cf4ad945 Mon Sep 17 00:00:00 2001 From: JorySchossau Date: Sun, 12 Apr 2020 19:34:17 -0400 Subject: [PATCH] first draft of geom ribbon --- src/ggplotnim.nim | 34 ++++++++++ src/ggplotnim/ggplot_drawing.nim | 99 +++++++++++++++++++++------- src/ggplotnim/ggplot_styles.nim | 6 ++ src/ggplotnim/ggplot_types.nim | 2 +- src/ggplotnim/postprocess_scales.nim | 2 +- 5 files changed, 119 insertions(+), 24 deletions(-) diff --git a/src/ggplotnim.nim b/src/ggplotnim.nim index f808ef3c..0eb70598 100644 --- a/src/ggplotnim.nim +++ b/src/ggplotnim.nim @@ -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, diff --git a/src/ggplotnim/ggplot_drawing.nim b/src/ggplotnim/ggplot_drawing.nim index 6a916068..6150e3d8 100644 --- a/src/ggplotnim/ggplot_drawing.nim +++ b/src/ggplotnim/ggplot_drawing.nim @@ -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) @@ -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) @@ -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, @@ -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: @@ -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 else: locView.draw(fg, pos, p.y, binWidths, df, i, style) of pkDodge: discard @@ -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 diff --git a/src/ggplotnim/ggplot_styles.nim b/src/ggplotnim/ggplot_styles.nim index 6f35567e..e8ca4f2b 100644 --- a/src/ggplotnim/ggplot_styles.nim +++ b/src/ggplotnim/ggplot_styles.nim @@ -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) const BarDefaultStyle = Style(lineWidth: 1.0, lineType: ltSolid, color: grey20, @@ -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: diff --git a/src/ggplotnim/ggplot_types.nim b/src/ggplotnim/ggplot_types.nim index 7966d7ac..f7e72ec9 100644 --- a/src/ggplotnim/ggplot_types.nim +++ b/src/ggplotnim/ggplot_types.nim @@ -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 diff --git a/src/ggplotnim/postprocess_scales.nim b/src/ggplotnim/postprocess_scales.nim index dcbe9256..503cb21b 100644 --- a/src/ggplotnim/postprocess_scales.nim +++ b/src/ggplotnim/postprocess_scales.nim @@ -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