diff --git a/README.md b/README.md index 19179772..7d2c04eb 100644 --- a/README.md +++ b/README.md @@ -414,6 +414,8 @@ In order to display the plots in Jupyter notebook, we encode the image(which is |---------------------------------------------------| |cgColor: CGColor (return the corresponding CGColor)| +Built-in Colors can be found [here](https://github.com/KarthikRIyer/swiftplot/blob/master/Sources/SwiftPlot/Color.swift). + ### PlotLabel diff --git a/Sources/AGG/include/agg_blur.h b/Sources/AGG/include/agg_blur.h index fd38e942..1f934a87 100644 --- a/Sources/AGG/include/agg_blur.h +++ b/Sources/AGG/include/agg_blur.h @@ -1487,13 +1487,13 @@ namespace agg } template - void apply_slight_blur(renderer_base& img, const rect_i& bounds, double r = 1) + void apply_slight_blur(blender_base& img, const rect_i& bounds, double r = 1) { if (r > 0) slight_blur(r).blur(img.ren(), bounds); } template - void apply_slight_blur(renderer_base& img, double r = 1) + void apply_slight_blur(blender_base& img, double r = 1) { if (r > 0) slight_blur(r).blur(img.ren(), img.clip_box()); } diff --git a/Sources/AGGRenderer/AGGRenderer/AGGRenderer.swift b/Sources/AGGRenderer/AGGRenderer/AGGRenderer.swift index d01de4bd..16f2d440 100644 --- a/Sources/AGGRenderer/AGGRenderer/AGGRenderer.swift +++ b/Sources/AGGRenderer/AGGRenderer/AGGRenderer.swift @@ -125,6 +125,16 @@ public class AGGRenderer: Renderer{ fillColor.a, agg_object) } + + public func drawEmptyCircle(center c: Point, + radius rx: Float, + radius2 ry: Float) { + draw_empty_circle(c.x + xOffset, + c.y + yOffset, + rx, + ry, + agg_object) + } public func drawSolidTriangle(point1: Point, point2: Point, diff --git a/Sources/AGGRenderer/CAGGRenderer/CAGGRenderer.cpp b/Sources/AGGRenderer/CAGGRenderer/CAGGRenderer.cpp index 13f20f53..39cc0b8c 100644 --- a/Sources/AGGRenderer/CAGGRenderer/CAGGRenderer.cpp +++ b/Sources/AGGRenderer/CAGGRenderer/CAGGRenderer.cpp @@ -22,6 +22,10 @@ void draw_solid_circle(float cx, float cy, float radius, float r, float g, float CPPAGGRenderer::draw_solid_circle(cx, cy, radius, r, g, b, a, object); } +void draw_empty_circle(float cx, float cy, float rx, float ry, const void *object){ + CPPAGGRenderer::draw_empty_circle(cx, cy, rx, ry, object); +} + void draw_solid_triangle(float x1, float x2, float x3, float y1, float y2, float y3, float r, float g, float b, float a, const void *object){ CPPAGGRenderer::draw_solid_triangle(x1, x2, x3, y1, y2, y3, r, g, b, a, object); } diff --git a/Sources/AGGRenderer/CAGGRenderer/include/CAGGRenderer.h b/Sources/AGGRenderer/CAGGRenderer/include/CAGGRenderer.h index 439d9e26..e5cf2bc0 100644 --- a/Sources/AGGRenderer/CAGGRenderer/include/CAGGRenderer.h +++ b/Sources/AGGRenderer/CAGGRenderer/include/CAGGRenderer.h @@ -13,6 +13,8 @@ void draw_rect(const float *x, const float *y, float thickness, float r, float g void draw_solid_rect(const float *x, const float *y, float r, float g, float b, float a, int hatch_pattern, const void *object); void draw_solid_circle(float cx, float cy, float radius, float r, float g, float b, float a, const void *object); + +void draw_empty_circle(float cx, float cy, float rx, float ry, const void *object); void draw_solid_triangle(float x1, float x2, float x3, float y1, float y2, float y3, float r, float g, float b, float a, const void *object); diff --git a/Sources/AGGRenderer/CPPAGGRenderer/CPPAGGRenderer.cpp b/Sources/AGGRenderer/CPPAGGRenderer/CPPAGGRenderer.cpp index 19455c7d..69ce39aa 100644 --- a/Sources/AGGRenderer/CPPAGGRenderer/CPPAGGRenderer.cpp +++ b/Sources/AGGRenderer/CPPAGGRenderer/CPPAGGRenderer.cpp @@ -44,7 +44,8 @@ typedef agg::rgba Color; const Color black(0.0,0.0,0.0,1.0); const Color blue_light(0.529,0.808,0.922,1.0); const Color white(1.0,1.0,1.0,1.0); -const Color white_translucent(1.0,1.0,1.0,0.8); +const Color gray_translucent(0.66,0.66,0.66,0.2); +const Color transparent(1.0, 1.0, 1.0, 0.0); namespace CPPAGGRenderer{ @@ -293,6 +294,23 @@ namespace CPPAGGRenderer{ ren_aa.color(c); agg::render_scanlines(m_ras, m_sl_p8, ren_aa); } + + void draw_empty_circle(float cx, float cy, float rx, float ry) { + agg::rendering_buffer rbuf = agg::rendering_buffer(buffer, frame_width, frame_height, -frame_width*3); + pixfmt pixf = pixfmt(rbuf); + renderer_base rb = renderer_base(pixf); + ren_aa = renderer_aa(rb); + agg::ellipse circle(cx, cy, rx, ry, 100); + agg::trans_affine matrix; + matrix *= agg::trans_affine_translation(0, 0); + agg::conv_transform trans(circle, matrix); + agg::conv_curve> curve(trans); + agg::conv_stroke>> stroke(curve); + stroke.width(1); + m_ras.add_path(stroke); + ren_aa.color(gray_translucent); + agg::render_scanlines(m_ras, m_sl_p8, ren_aa); + } void draw_solid_triangle(float x1, float x2, float x3, float y1, float y2, float y3, float r, float g, float b, float a) { agg::rendering_buffer rbuf = agg::rendering_buffer(buffer, frame_width, frame_height, -frame_width*3); @@ -503,6 +521,11 @@ namespace CPPAGGRenderer{ Plot *plot = (Plot *)object; plot -> draw_solid_circle(cx, cy, radius, r, g, b, a); } + + void draw_empty_circle(float cx, float cy, float rx, float ry, const void *object){ + Plot *plot = (Plot *)object; + plot -> draw_empty_circle(cx, cy, rx, ry); + } void draw_solid_triangle(float x1, float x2, float x3, float y1, float y2, float y3, float r, float g, float b, float a, const void *object){ Plot *plot = (Plot *)object; diff --git a/Sources/AGGRenderer/CPPAGGRenderer/include/CPPAGGRenderer.h b/Sources/AGGRenderer/CPPAGGRenderer/include/CPPAGGRenderer.h index 69de6f94..59cdce5c 100644 --- a/Sources/AGGRenderer/CPPAGGRenderer/include/CPPAGGRenderer.h +++ b/Sources/AGGRenderer/CPPAGGRenderer/include/CPPAGGRenderer.h @@ -11,6 +11,8 @@ namespace CPPAGGRenderer{ void draw_solid_rect(const float *x, const float *y, float r, float g, float b, float a, int hatch_pattern, const void *object); void draw_solid_circle(float cx, float cy, float radius, float r, float g, float b, float a, const void *object); + + void draw_empty_circle(float cx, float cy, float rx, float ry, const void *object); void draw_solid_triangle(float x1, float x2, float x3, float y1, float y2, float y3, float r, float g, float b, float a, const void *object); diff --git a/Sources/SVGRenderer/SVGRenderer.swift b/Sources/SVGRenderer/SVGRenderer.swift index da0ae2c3..e1e72ab3 100644 --- a/Sources/SVGRenderer/SVGRenderer.swift +++ b/Sources/SVGRenderer/SVGRenderer.swift @@ -145,6 +145,15 @@ public class SVGRenderer: Renderer{ let circle: String = #""# lines.append(circle) } + + public func drawEmptyCircle(center c: Point, + radius rx: Float, + radius2 ry: Float) { + let c = convertToSVGCoordinates(c) + let color = "gray" + let circle: String = #""# + lines.append(circle) + } public func drawSolidTriangle(point1: Point, point2: Point, diff --git a/Sources/SwiftPlot/GraphLayout.swift b/Sources/SwiftPlot/GraphLayout.swift index 39712bd7..4fb3794d 100644 --- a/Sources/SwiftPlot/GraphLayout.swift +++ b/Sources/SwiftPlot/GraphLayout.swift @@ -469,7 +469,7 @@ extension Plot where Self: HasGraphLayout { let tup = layoutData(size: size, renderer: renderer) return (tup.0, tup.1, self.legendLabels) } - layout.drawBackground(results: results, renderer: renderer) + // layout.drawBackground(results: results, renderer: renderer) renderer.withAdditionalOffset(results.plotBorderRect.origin) { renderer in drawData(drawingData, size: results.plotBorderRect.size, renderer: renderer) } diff --git a/Sources/SwiftPlot/PolarChart.swift b/Sources/SwiftPlot/PolarChart.swift new file mode 100644 index 00000000..a66fbc85 --- /dev/null +++ b/Sources/SwiftPlot/PolarChart.swift @@ -0,0 +1,470 @@ +import Foundation + +// This code is identical to LineChart.swift, with a few minor adjustments. + +fileprivate let MAX_DIV: Float = 50 + +// class defining a polarGraph and all its logic +public struct PolarGraph: Plot { + + public var layout = GraphLayout() + public var maxRadii = Float(0.0) + public var origin = Pair(T(Float(0.0)), U(Float(0.0))) + public var reference = Pair(T(Float(1.0)), U(Float(1.0))) + // Data. + var primaryAxis = Axis() + var secondaryAxis: Axis? = nil + // Polargraph layout properties. + public var plotLineThickness: Float = 1.5 + + public init(enablePrimaryAxisGrid: Bool = false, + enableSecondaryAxisGrid: Bool = false){ + self.enablePrimaryAxisGrid = enablePrimaryAxisGrid + self.enableSecondaryAxisGrid = enableSecondaryAxisGrid + } + + public init(points : [Pair], + enablePrimaryAxisGrid: Bool = false, + enableSecondaryAxisGrid: Bool = false){ + self.init(enablePrimaryAxisGrid: enablePrimaryAxisGrid, enableSecondaryAxisGrid: enableSecondaryAxisGrid) + primaryAxis.series.append(Series(values: points, label: "Plot")) + } +} + +// Setting data. +extension PolarGraph { + + public mutating func linspace(start: Float, end: Float, num: Int) -> Array{ + var increment = (end - start) / Float((num - 1)) + var current = start + var outputs = [Float]() + for i in 1...num { + outputs.append(current) + current += increment + } + outputs[outputs.count-1] = end + return outputs + } + + // functions to add series + public mutating func addSeries(_ s: Series, + axisType: Axis.Location = .primaryAxis){ + switch axisType { + case .primaryAxis: + primaryAxis.series.append(s) + case .secondaryAxis: + if secondaryAxis == nil { + secondaryAxis = Axis() + } + secondaryAxis!.series.append(s) + } + } + public mutating func addSeries(points : [Pair], + label: String, color: Color = Color.lightBlue, + axisType: Axis.Location = .primaryAxis){ + let s = Series(values: points,label: label, color: color) + addSeries(s, axisType: axisType) + } + public mutating func addSeries(_ y: [U], + label: String, + color: Color = Color.lightBlue, + axisType: Axis.Location = .primaryAxis){ + var points = [Pair]() + for i in 0..(T(i), y[i])) + } + let s = Series(values: points, label: label, color: color) + addSeries(s, axisType: axisType) + } + public mutating func addSeries(_ r: [T], + _ theta: [U], + label: String, + color: Color = .lightBlue, + axisType: Axis.Location = .primaryAxis){ + var x = theta.map{angle in r[theta.firstIndex(of: angle)!].toFloat() * cos(angle.toFloat())} + var y = theta.map{angle in r[theta.firstIndex(of: angle)!].toFloat() * sin(angle.toFloat())} + + var points = [Pair]() + for i in 0..(T(x[i]), U(y[i]))) + } + let s = Series(values: points, label: label, color: color) + addSeries(s, axisType: axisType) + } + public mutating func addFunction(_ function: (T)->U, + minX: T = T(0), + maxX: T = T(2*Float.pi), + numberOfSamples: Int = 400, + clampY: ClosedRange? = nil, + label: String, + color: Color = Color.lightBlue, + axisType: Axis.Location = .primaryAxis) { + + let theta = linspace(start: minX.toFloat(), end: maxX.toFloat(), num: numberOfSamples) + let r = theta.map{val in function(T(val))} + + var points = [Pair]() + + var x = theta.map{angle in r[theta.firstIndex(of: angle)!].toFloat() * cos(angle.toFloat())} + var y = theta.map{angle in r[theta.firstIndex(of: angle)!].toFloat() * sin(angle.toFloat())} + + for i in 0..(T(x[i]), U(y[i]))) + } + + let s = Series(values: points, label: label, color: color) + addSeries(s, axisType: axisType) + } +} + +// Layout properties. +extension PolarGraph { + + public var enablePrimaryAxisGrid: Bool { + get { layout.enablePrimaryAxisGrid } + set { layout.enablePrimaryAxisGrid = newValue } + } + + public var enableSecondaryAxisGrid: Bool { + get { layout.enableSecondaryAxisGrid } + set { layout.enableSecondaryAxisGrid = newValue } + } +} + +// Layout and drawing of data. +extension PolarGraph: HasGraphLayout { + + public var legendLabels: [(String, LegendIcon)] { + var allSeries: [Series] = primaryAxis.series + if (secondaryAxis != nil) { + allSeries = allSeries + secondaryAxis!.series + } + return allSeries.map { ($0.label, .square($0.color)) } + } + + public struct DrawingData { + var primaryAxisInfo: AxisLayoutInfo? + var secondaryAxisInfo: AxisLayoutInfo? + } + + // functions implementing plotting logic + public func layoutData(size: Size, renderer: Renderer) -> (DrawingData, PlotMarkers?) { + var results = DrawingData() + var markers = PlotMarkers() + guard !primaryAxis.series.isEmpty, !primaryAxis.series[0].values.isEmpty else { return (results, markers) } + + results.primaryAxisInfo = AxisLayoutInfo(series: primaryAxis.series, size: size) + results.secondaryAxisInfo = secondaryAxis.map { + var info = AxisLayoutInfo(series: $0.series, size: size) + info.mergeXAxis(with: &results.primaryAxisInfo!) + return info + } + + (markers.xMarkers, markers.xMarkersText, markers.yMarkers, markers.yMarkersText) = + results.primaryAxisInfo!.calculateMarkers() + if let secondaryAxisInfo = results.secondaryAxisInfo { + (_, _, markers.y2Markers, markers.y2MarkersText) = secondaryAxisInfo.calculateMarkers() + } + + return (results, markers) + } + + //functions to draw the plot + public func drawData(_ data: DrawingData, size: Size, renderer: Renderer) { + if let axisInfo = data.primaryAxisInfo { + for dataset in primaryAxis.series { + var angles = [0, 0.25, 0.5, 0.75]; + var angles2 = angles.map{val in 1+val} + angles += angles2 + var theta = angles.map{angle in angle*Double.pi} + + let maxRadii = dataset.values.map{pair in pair.x}.max()! + + var x = theta.map{angle in maxRadii.toFloat() * cos(angle.toFloat())} + var y = theta.map{angle in maxRadii.toFloat() * sin(angle.toFloat())} + + for i in 0..]() + points.append(Pair(T(0), U(0))) + points.append(Pair(T(maxRadii.toFloat() * cos(angles[i].toFloat())), U(maxRadii.toFloat() * sin(angles[i].toFloat())))) + let s = Series(values: points, label: "Boi", color: .lightBlue) + addSeries(s, axisType: .primaryAxis) + } + + let points = dataset.values.map { axisInfo.convertCoordinate(fromData: $0) } + var pointOrigin = axisInfo.convertCoordinate(fromData: origin) + var referenceRadius = axisInfo.convertCoordinate(fromData: reference) + renderer.drawPlotLines(points: points, + strokeWidth: plotLineThickness, + strokeColor: dataset.color, + isDashed: false) + for i in 1...maxRadii.toInt(){ + renderer.drawEmptyCircle(center: pointOrigin, radius: Float(i)*Float(referenceRadius.x-pointOrigin.x), radius2: Float(i)*Float(referenceRadius.y-pointOrigin.y)) + } + } + } + if let secondaryAxis = secondaryAxis, let axisInfo = data.secondaryAxisInfo { + for dataset in secondaryAxis.series { + let points = dataset.values.map { axisInfo.convertCoordinate(fromData: $0) } + renderer.drawPlotLines(points: points, + strokeWidth: plotLineThickness, + strokeColor: dataset.color, + isDashed: true) + } + } + } +} + +extension PolarGraph { + + struct AxisLayoutInfo { + let size: Size + let rightMargin: Float + let topMargin: Float + var bounds: (x: ClosedRange, y: ClosedRange) + + var scaleX: Float = 1 + var scaleY: Float = 1 + // The "origin" is just a known value at a known location, + // used for calculating where other points are located. + var origin: Point = zeroPoint + var originValue = Pair(T(0), U(0)) + + init(series: [Series], size: Size) { + self.size = size + rightMargin = size.width * 0.05 + topMargin = size.height * 0.05 + bounds = AxisLayoutInfo.getBounds(series) + boundsDidChange() + } + + private static func getBounds(_ series: [Series]) -> (x: ClosedRange, y: ClosedRange) { + var maximumX: T = maxX(points: series[0].values) + var minimumX: T = minX(points: series[0].values) + var maximumY: U = maxY(points: series[0].values) + var minimumY: U = minY(points: series[0].values) + for s in series { + var x: T = maxX(points: s.values) + var y: U = maxY(points: s.values) + maximumX = max(x, maximumX) + maximumY = max(y, maximumY) + x = minX(points: s.values) + y = minY(points: s.values) + minimumX = min(x, minimumX) + minimumY = min(y, minimumY) + } + return (x: minimumX...maximumX, y: minimumY...maximumY) + } + + private mutating func boundsDidChange() { + let availableWidth = size.width - (2 * rightMargin) + let availableHeight = size.height - (2 * topMargin) + let xRange_primary = Float(bounds.x.upperBound - bounds.x.lowerBound) + let yRange_primary = Float(bounds.y.upperBound - bounds.y.lowerBound) + + scaleX = xRange_primary / availableWidth + scaleY = yRange_primary / availableHeight + calculateOrigin() + } + + private mutating func calculateOrigin() { + let originLocX: Float + let originLocY: Float + if Float(bounds.x.lowerBound) >= 0, Float(bounds.x.upperBound) >= 0 { + // All points on positive X axis. + originLocX = rightMargin + originValue.x = bounds.x.lowerBound + } else if Float(bounds.x.lowerBound) < 0, Float(bounds.x.upperBound) < 0 { + // All points on negative X axis. + originLocX = size.width - rightMargin + originValue.x = bounds.x.upperBound + } else { + // Both sides of X axis. + originLocX = (Float(bounds.x.lowerBound).magnitude / scaleX) + rightMargin + originValue.x = T(0) + } + + if Float(bounds.y.lowerBound) >= 0, Float(bounds.y.upperBound) >= 0 { + // All points on positive Y axis. + originLocY = topMargin + originValue.y = bounds.y.lowerBound + } else if Float(bounds.y.lowerBound) < 0, Float(bounds.y.upperBound) < 0 { + // All points on negative Y axis. + originLocY = size.height - topMargin + originValue.y = bounds.y.upperBound + } else { + // Both sides of Y axis. + originLocY = (Float(bounds.y.lowerBound).magnitude / scaleY) + topMargin + originValue.y = U(0) + } + origin = Pair(originLocX, originLocY) + + // If the zero-coordinate is already in view, snap the origin to it. + let zeroLocation = convertCoordinate(fromData: Pair(T(0), U(0))) + if (0...size.width).contains(zeroLocation.x) { + origin.x = zeroLocation.x + originValue.x = T(0) + } + if (0...size.height).contains(zeroLocation.y) { + origin.y = zeroLocation.y + originValue.y = U(0) + } + } + + func calculateMarkers() -> (x: [Float], xLabels: [String], y: [Float], yLabels: [String]) { + var yIncrement: Float = -1 + var xIncrement: Float = -1 + var xIncRound: Int = 1 + var yIncRound: Int = 1 + let xRange = Float(bounds.x.upperBound - bounds.x.lowerBound) + let yRange = Float(bounds.y.upperBound - bounds.y.lowerBound) + + if yRange <= 2.0, yRange >= 1.0 { + let differenceY = yRange + yIncrement = 0.5*(1.0/differenceY) + var c = 0 + while(abs(yIncrement)*pow(10.0,Float(c))<1.0) { + c+=1 + } + yIncrement = yIncrement/scaleY + yIncRound = c+1 + } else if yRange < 1.0 { + let differenceY = yRange + yIncrement = differenceY/10.0 + var c = 0 + while(abs(yIncrement)*pow(10.0,Float(c))<1.0) { + c+=1 + } + yIncrement = yIncrement/scaleY + yIncRound = c+1 + } + + if xRange <= 2.0, xRange >= 1.0 { + let differenceX = xRange + xIncrement = 0.5*(1.0/differenceX) + var c = 0 + while(abs(xIncrement)*pow(10.0,Float(c))<1.0) { + c+=1 + } + xIncrement = yIncrement/scaleX + xIncRound = c+1 + } else if xRange < 1.0 { + let differenceX = xRange + xIncrement = differenceX/10 + var c = 0 + while(abs(xIncrement)*pow(10.0,Float(c))<1.0) { + c+=1 + } + xIncrement = yIncrement/scaleX + xIncRound = c+1 + } + + let nD1: Int = max(getNumberOfDigits(Float(bounds.y.upperBound)), getNumberOfDigits(Float(bounds.y.lowerBound))) + var v1: Float + if (nD1 > 1 && bounds.y.upperBound <= U(pow(Float(10), Float(nD1 - 1)))) { + v1 = Float(pow(Float(10), Float(nD1 - 2))) + } else if (nD1 > 1) { + v1 = Float(pow(Float(10), Float(nD1 - 1))) + } else { + v1 = Float(pow(Float(10), Float(0))) + } + let nY: Float = v1/scaleY + if(yIncrement == -1) { + yIncrement = nY + if(size.height/nY > MAX_DIV){ + yIncrement = (size.height/nY)*yIncrement/MAX_DIV + } + } + + let nD2: Int = max(getNumberOfDigits(Float(bounds.x.upperBound)), getNumberOfDigits(Float(bounds.x.lowerBound))) + var v2: Float + if (nD2 > 1 && bounds.x.upperBound <= T(pow(Float(10), Float(nD2 - 1)))) { + v2 = Float(pow(Float(10), Float(nD2 - 2))) + } else if (nD2 > 1) { + v2 = Float(pow(Float(10), Float(nD2 - 1))) + } else { + v2 = Float(pow(Float(10), Float(0))) + } + + let nX: Float = v2/scaleX + if(xIncrement == -1) { + xIncrement = nX + var noXD: Float = size.width/nX + if(noXD > MAX_DIV){ + xIncrement = (size.width/nX)*xIncrement/MAX_DIV + noXD = MAX_DIV + } + } + + var xMarkerLocations = [Float]() + var xM = origin.x + if size.width > 0 { + while xM<=size.width { + if(xM+xIncrement<0.0 || xM<0.0) { + xM = xM+xIncrement + continue + } + xMarkerLocations.append(xM) + xM = xM + xIncrement + } + + xM = origin.x - xIncrement + while xM>0.0 { + if (xM > size.width) { + xM = xM - xIncrement + continue + } + xMarkerLocations.append(xM) + xM = xM - xIncrement + } + } + let xMarkerLabels = xMarkerLocations.map { offset -> String in + let offsetValue = scaleX * (offset - origin.x) + return "\(roundToN(offsetValue + Float(originValue.x), xIncRound))" + } + + var yMarkerLocations = [Float]() + var yM = origin.y + if size.height > 0 { + while yM<=size.height { + if(yM+yIncrement<0.0 || yM<0.0){ + yM = yM + yIncrement + continue + } + yMarkerLocations.append(yM) + yM = yM + yIncrement + } + yM = origin.y - yIncrement + while yM>0.0 { + yMarkerLocations.append(yM) + yM = yM - yIncrement + } + } + let yMarkerLabels = yMarkerLocations.map { offset -> String in + let offsetValue = scaleY * (offset - origin.y) + return "\(roundToN(offsetValue + Float(originValue.y), yIncRound))" + } + + print(xMarkerLocations.count) + print(xMarkerLocations) + print(yMarkerLocations.count) + print(yMarkerLocations) + + return (x: xMarkerLocations, xLabels: xMarkerLabels, + y: yMarkerLocations, yLabels: yMarkerLabels) + } + + func convertCoordinate(fromData value: Pair) -> Point { + return Point(Float(((value.x - originValue.x) / T(scaleX)) + T(origin.x)), + Float(((value.y - originValue.y) / U(scaleY)) + U(origin.y))) + } + + mutating func mergeXAxis(with other: inout AxisLayoutInfo) { + bounds.x = + min(bounds.x.lowerBound, other.bounds.x.lowerBound)...max(bounds.x.upperBound, other.bounds.x.upperBound) + other.bounds.x = self.bounds.x + boundsDidChange() + other.boundsDidChange() + } + } +} diff --git a/Sources/SwiftPlot/Renderer.swift b/Sources/SwiftPlot/Renderer.swift index c1028271..21e662ef 100644 --- a/Sources/SwiftPlot/Renderer.swift +++ b/Sources/SwiftPlot/Renderer.swift @@ -127,6 +127,18 @@ public protocol Renderer: AnyObject{ func drawSolidCircle(center c: Point, radius r: Float, fillColor: Color) + + /*drawEmptyCircle() + *params: center c: Point, + * radius r: Float + *description: Draws a circle with specified fill color, center and radius + * This function can operate in both coordinate systems with and + * without shifted origin. + * This is decided by the boolean parameter isOriginShifted. + */ + func drawEmptyCircle(center c: Point, + radius rx: Float, + radius2 ry: Float) /*drawSolidTriangle() *params: point1: Point,