SHARE
TWEET

graphlayout

a guest Dec 14th, 2019 98 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import Foundation
  2.  
  3. public enum LegendIcon {
  4.     case square(Color)
  5.     case shape(ScatterPlotSeriesOptions.ScatterPattern, Color)
  6. }
  7.  
  8. public struct GraphLayout {
  9.     // Inputs.
  10.     var plotSize: Size = .zero
  11.    
  12.     var backgroundColor: Color = .white
  13.     var plotBackgroundColor: Color?
  14.     var plotTitle = PlotTitle()
  15.     var plotAnnotation = PlotAnnotation()
  16.     var plotLabel  = PlotLabel()
  17.     var plotLegend = PlotLegend()
  18.     var plotBorder = PlotBorder()
  19.     var grid = Grid()
  20.     var legendLabels: [(String, LegendIcon)] = []
  21.    
  22.     var enablePrimaryAxisGrid = true
  23.     var enableSecondaryAxisGrid = true
  24.     var markerTextSize: Float = 12
  25.     /// The amount of (horizontal) space to reserve for markers on the Y-axis.
  26.     var yMarkerMaxWidth: Float = 40
  27.    
  28.     struct Results {
  29.         struct Sizes {
  30.             var xLabelSize: Size?
  31.             var yLabelSize: Size?
  32.             var y2LabelSize: Size?
  33.             var titleSize: Size?
  34.         }
  35.        
  36.         var plotBorderRect: Rect
  37.         var sizes: Sizes
  38.        
  39.         var xLabelLocation: Point?
  40.         var yLabelLocation: Point?
  41.         var y2LabelLocation: Point?
  42.         var titleLocation: Point?
  43.         var annotationLocation: Point?
  44.  
  45.         var plotMarkers = PlotMarkers()
  46.         var xMarkersTextLocation = [Point]()
  47.         var yMarkersTextLocation = [Point]()
  48.         var y2MarkersTextLocation = [Point]()
  49.        
  50.         var legendRect: Rect?
  51.     }
  52.    
  53.     // Layout.
  54.        
  55.     func layout(renderer: Renderer, calculateMarkers: (inout PlotMarkers, Size)->Void) -> Results {
  56.         // 1. Measure the things outside of the plot's border (axis titles, plot title)
  57.         var sizes = Results.Sizes()
  58.         measureLabels(renderer: renderer, results: &sizes)
  59.         // 2. Calculate the plot's border
  60.         let borderRect = calcBorder(sizes: sizes, renderer: renderer)
  61.         // 3. Lay out the things outside of the plot's border (axis titles, plot title)
  62.         var results = Results(plotBorderRect: borderRect, sizes: sizes)
  63.         calcLabelLocations(&results)
  64.         // 4. Let the plot calculate its scale, calculate marker positions.
  65.         calculateMarkers(&results.plotMarkers, results.plotBorderRect.size)
  66.         // 5. Lay out remaining chrome.
  67.         calcMarkerTextLocations(renderer: renderer, results: &results)
  68.         calcLegend(legendLabels, renderer: renderer, results: &results)
  69.         return results
  70.     }
  71.    
  72.     static let xLabelPadding: Float = 10
  73.     static let yLabelPadding: Float = 10
  74.     static let titleLabelPadding: Float = 14
  75.    
  76.     /// Measures the sizes of chrome elements outside the plot's borders (axis titles, plot title, etc).
  77.     func measureLabels(renderer: Renderer, results: inout Results.Sizes) {
  78.         if !plotLabel.xLabel.isEmpty {
  79.             results.xLabelSize = renderer.getTextLayoutSize(text: plotLabel.xLabel, textSize: plotLabel.size)
  80.         }
  81.         if !plotLabel.yLabel.isEmpty {
  82.             results.yLabelSize = renderer.getTextLayoutSize(text: plotLabel.yLabel, textSize: plotLabel.size)
  83.         }
  84.         if !plotLabel.y2Label.isEmpty {
  85.             results.y2LabelSize = renderer.getTextLayoutSize(text: plotLabel.y2Label, textSize: plotLabel.size)
  86.         }
  87.         if !plotTitle.title.isEmpty {
  88.             results.titleSize = renderer.getTextLayoutSize(text: plotTitle.title, textSize: plotTitle.size)
  89.         }
  90.     }
  91.    
  92.     /// Calculates the region of the plot which is used for displaying the plot's data (inside all of the chrome).
  93.     func calcBorder(sizes: Results.Sizes, renderer: Renderer) -> Rect {
  94.         var borderRect = Rect(
  95.             origin: zeroPoint,
  96.             size: self.plotSize
  97.         )
  98.         if let xLabel = sizes.xLabelSize {
  99.             borderRect.clampingShift(dy: xLabel.height + 2 * Self.xLabelPadding)
  100.         }
  101.         if let yLabel = sizes.yLabelSize {
  102.             borderRect.clampingShift(dx: yLabel.height + 2 * Self.yLabelPadding)
  103.         }
  104.         if let y2Label = sizes.y2LabelSize {
  105.             borderRect.size.width -= (y2Label.height + 2 * Self.yLabelPadding)
  106.         }
  107.         if let titleLabel = sizes.titleSize {
  108.             borderRect.size.height -= (titleLabel.height + 2 * Self.titleLabelPadding)
  109.         } else {
  110.             // Add a space to the top when there is no title.
  111.             borderRect.size.height -= Self.titleLabelPadding
  112.         }
  113.         borderRect.contract(by: plotBorder.thickness)
  114.         // Give space for the markers.
  115.         borderRect.clampingShift(dy: (2 * markerTextSize) + 10) // X markers
  116.         // TODO: Better space calculation for Y/Y2 markers.
  117.         borderRect.clampingShift(dx: yMarkerMaxWidth + 10) // Y markers
  118.         borderRect.size.width -= yMarkerMaxWidth + 10 // Y2 markers
  119.  
  120.         return borderRect
  121.     }
  122.    
  123.     /// Lays out the chrome elements outside the plot's borders (axis titles, plot title, etc).
  124.     func calcLabelLocations(_ results: inout Results) {
  125.         if let xLabelSize = results.sizes.xLabelSize {
  126.             results.xLabelLocation = Point(results.plotBorderRect.midX - xLabelSize.width/2,
  127.                                            Self.xLabelPadding)
  128.         }
  129.         if let titleLabelSize = results.sizes.titleSize {
  130.             results.titleLocation = Point(results.plotBorderRect.midX - titleLabelSize.width/2,
  131.                                           plotSize.height  - titleLabelSize.height - Self.titleLabelPadding)
  132.         }
  133.         if let yLabelSize = results.sizes.yLabelSize {
  134.             results.yLabelLocation = Point(Self.yLabelPadding + yLabelSize.height,
  135.                                            results.plotBorderRect.midY - yLabelSize.width/2)
  136.         }
  137.         if let y2LabelSize = results.sizes.y2LabelSize {
  138.             results.y2LabelLocation = Point(plotSize.width - Self.yLabelPadding,
  139.                                             results.plotBorderRect.midY - y2LabelSize.width/2)
  140.         }
  141.     }
  142.    
  143.     func calcMarkerTextLocations(renderer: Renderer, results: inout Results) {
  144.        
  145.         for i in 0..<results.plotMarkers.xMarkers.count {
  146.             let textWidth = renderer.getTextWidth(text: results.plotMarkers.xMarkersText[i], textSize: markerTextSize)
  147.             let text_p = Point(results.plotMarkers.xMarkers[i] - (textWidth/2), -2.0 * markerTextSize)
  148.             results.xMarkersTextLocation.append(text_p)
  149.         }
  150.        
  151.         for i in 0..<results.plotMarkers.yMarkers.count {
  152.             var textWidth = renderer.getTextWidth(text: results.plotMarkers.yMarkersText[i], textSize: markerTextSize)
  153.             textWidth = min(textWidth, yMarkerMaxWidth)
  154.             let text_p = Point(-textWidth - 8, results.plotMarkers.yMarkers[i] - 4)
  155.             results.yMarkersTextLocation.append(text_p)
  156.         }
  157.        
  158.         for i in 0..<results.plotMarkers.y2Markers.count {
  159.             let text_p = Point(results.plotBorderRect.width + 8, results.plotMarkers.y2Markers[i] - 4)
  160.             results.y2MarkersTextLocation.append(text_p)
  161.         }
  162.     }
  163.    
  164.     func calcLegend(_ labels: [(String, LegendIcon)], renderer: Renderer, results: inout Results) {
  165.         guard !labels.isEmpty else { return }
  166.         let maxWidth = labels.lazy.map {
  167.             renderer.getTextWidth(text: $0.0, textSize: self.plotLegend.textSize)
  168.         }.max() ?? 0
  169.        
  170.         let legendWidth  = maxWidth + 3.5 * plotLegend.textSize
  171.         let legendHeight = (Float(labels.count)*2.0 + 1.0) * plotLegend.textSize
  172.        
  173.         let legendTopLeft = Point(results.plotBorderRect.minX + Float(20),
  174.                                   results.plotBorderRect.maxY - Float(20))
  175.         results.legendRect = Rect(
  176.             origin: legendTopLeft,
  177.             size: Size(width: legendWidth, height: -legendHeight)
  178.         ).normalized
  179.     }
  180.    
  181.     // Drawing.
  182.    
  183.     func drawBackground(results: Results, renderer: Renderer) {
  184.         renderer.drawSolidRect(Rect(origin: zeroPoint, size: plotSize), fillColor: backgroundColor, hatchPattern: .none)
  185.         if let plotBackgroundColor = plotBackgroundColor {
  186.             renderer.drawSolidRect(results.plotBorderRect, fillColor: plotBackgroundColor, hatchPattern: .none)
  187.         }
  188.         drawGrid(results: results, renderer: renderer)
  189.         drawBorder(results: results, renderer: renderer)
  190.         drawMarkers(results: results, renderer: renderer)
  191.     }
  192.    
  193.     func drawForeground(results: Results, renderer: Renderer) {
  194.         drawTitle(results: results, renderer: renderer)
  195.         drawLabels(results: results, renderer: renderer)
  196.         drawLegend(legendLabels, results: results, renderer: renderer)
  197.     }
  198.    
  199.     func drawTitle(results: Results, renderer: Renderer) {
  200.         if let titleLocation = results.titleLocation {
  201.             renderer.drawText(text: plotTitle.title,
  202.                               location: titleLocation,
  203.                               textSize: plotTitle.size,
  204.                               color: plotTitle.color,
  205.                               strokeWidth: 1.2,
  206.                               angle: 0)
  207.         }
  208.     }
  209.  
  210.     func drawAnnotation(results: Results, renderer: Renderer) {
  211.         if let annotationLocation = results.annotationLocation {
  212.             renderer.drawText(text: plotAnnotation.annotation,
  213.                               location: annotationLocation,
  214.                               textSize: plotAnnotation.size,
  215.                               color: plotAnnotation.color,
  216.                               strokeWidth: 1.2,
  217.                               angle: 0)
  218.         }
  219.     }
  220.  
  221.     func drawLabels(results: Results, renderer: Renderer) {
  222.         if let xLocation = results.xLabelLocation {
  223.             renderer.drawText(text: plotLabel.xLabel,
  224.                               location: xLocation,
  225.                               textSize: plotLabel.size,
  226.                               color: plotLabel.color,
  227.                               strokeWidth: 1.2,
  228.                               angle: 0)
  229.         }
  230.         if let yLocation = results.yLabelLocation {
  231.             renderer.drawText(text: plotLabel.yLabel,
  232.                               location: yLocation,
  233.                               textSize: plotLabel.size,
  234.                               color: plotLabel.color,
  235.                               strokeWidth: 1.2,
  236.                               angle: 90)
  237.         }
  238.         if let y2Location = results.y2LabelLocation {
  239.             renderer.drawText(text: plotLabel.y2Label,
  240.                               location: y2Location,
  241.                               textSize: plotLabel.size,
  242.                               color: plotLabel.color,
  243.                               strokeWidth: 1.2,
  244.                               angle: 90)
  245.         }
  246.     }
  247.    
  248.     func drawBorder(results: Results, renderer: Renderer) {
  249.         renderer.drawRect(results.plotBorderRect,
  250.                           strokeWidth: plotBorder.thickness,
  251.                           strokeColor: plotBorder.color)
  252.     }
  253.    
  254.     func drawGrid(results: Results, renderer: Renderer) {
  255.         guard enablePrimaryAxisGrid || enablePrimaryAxisGrid else { return }
  256.         let rect = results.plotBorderRect
  257.         for index in 0..<results.plotMarkers.xMarkers.count {
  258.             let p1 = Point(results.plotMarkers.xMarkers[index] + rect.minX, rect.minY)
  259.             let p2 = Point(results.plotMarkers.xMarkers[index] + rect.minX, rect.maxY)
  260.             renderer.drawLine(startPoint: p1,
  261.                               endPoint: p2,
  262.                               strokeWidth: grid.thickness,
  263.                               strokeColor: grid.color,
  264.                               isDashed: false)
  265.         }
  266.    
  267.         if (enablePrimaryAxisGrid) {
  268.             for index in 0..<results.plotMarkers.yMarkers.count {
  269.                 let p1 = Point(rect.minX, results.plotMarkers.yMarkers[index] + rect.minY)
  270.                 let p2 = Point(rect.maxX, results.plotMarkers.yMarkers[index] + rect.minY)
  271.                 renderer.drawLine(startPoint: p1,
  272.                                   endPoint: p2,
  273.                                   strokeWidth: grid.thickness,
  274.                                   strokeColor: grid.color,
  275.                                   isDashed: false)
  276.             }
  277.         }
  278.         if (enableSecondaryAxisGrid) {
  279.             for index in 0..<results.plotMarkers.y2Markers.count {
  280.                 let p1 = Point(rect.minX, results.plotMarkers.y2Markers[index] + rect.minY)
  281.                 let p2 = Point(rect.maxX, results.plotMarkers.y2Markers[index] + rect.minY)
  282.                 renderer.drawLine(startPoint: p1,
  283.                                   endPoint: p2,
  284.                                   strokeWidth: grid.thickness,
  285.                                   strokeColor: grid.color,
  286.                                   isDashed: false)
  287.             }
  288.         }
  289.     }
  290.  
  291.     func drawMarkers(results: Results, renderer: Renderer) {
  292.         let rect = results.plotBorderRect
  293.         for index in 0..<results.plotMarkers.xMarkers.count {
  294.             let p1 = Point(results.plotMarkers.xMarkers[index], -6) + rect.origin
  295.             let p2 = Point(results.plotMarkers.xMarkers[index], 0) + rect.origin
  296.             renderer.drawLine(startPoint: p1,
  297.                               endPoint: p2,
  298.                               strokeWidth: plotBorder.thickness,
  299.                               strokeColor: plotBorder.color,
  300.                               isDashed: false)
  301.             renderer.drawText(text: results.plotMarkers.xMarkersText[index],
  302.                               location: results.xMarkersTextLocation[index] + rect.origin,
  303.                               textSize: markerTextSize,
  304.                               color: plotBorder.color,
  305.                               strokeWidth: 0.7,
  306.                               angle: 0)
  307.         }
  308.  
  309.         for index in 0..<results.plotMarkers.yMarkers.count {
  310.             let p1 = Point(-6, results.plotMarkers.yMarkers[index]) + rect.origin
  311.             let p2 = Point(0, results.plotMarkers.yMarkers[index]) + rect.origin
  312.             renderer.drawLine(startPoint: p1,
  313.                               endPoint: p2,
  314.                               strokeWidth: plotBorder.thickness,
  315.                               strokeColor: plotBorder.color,
  316.                               isDashed: false)
  317.             renderer.drawText(text: results.plotMarkers.yMarkersText[index],
  318.                               location: results.yMarkersTextLocation[index]  + rect.origin,
  319.                               textSize: markerTextSize,
  320.                               color: plotBorder.color,
  321.                               strokeWidth: 0.7,
  322.                               angle: 0)
  323.         }
  324.        
  325.         if !results.plotMarkers.y2Markers.isEmpty {
  326.             for index in 0..<results.plotMarkers.y2Markers.count {
  327.                 let p1 = Point(results.plotBorderRect.width,
  328.                                (results.plotMarkers.y2Markers[index])) + rect.origin
  329.                 let p2 = Point(results.plotBorderRect.width + 6,
  330.                                (results.plotMarkers.y2Markers[index])) + rect.origin
  331.                 renderer.drawLine(startPoint: p1,
  332.                                   endPoint: p2,
  333.                                   strokeWidth: plotBorder.thickness,
  334.                                   strokeColor: plotBorder.color,
  335.                                   isDashed: false)
  336.                 renderer.drawText(text: results.plotMarkers.y2MarkersText[index],
  337.                                   location: results.y2MarkersTextLocation[index]  + rect.origin,
  338.                                   textSize: markerTextSize,
  339.                                   color: plotBorder.color,
  340.                                   strokeWidth: 0.7,
  341.                                   angle: 0)
  342.             }
  343.         }
  344.     }
  345.    
  346.     func drawLegend(_ entries: [(String, LegendIcon)], results: Results, renderer: Renderer) {
  347.        
  348.         guard let legendRect = results.legendRect else { return }
  349.         renderer.drawSolidRectWithBorder(legendRect,
  350.                                          strokeWidth: plotLegend.borderThickness,
  351.                                          fillColor: plotLegend.backgroundColor,
  352.                                          borderColor: plotLegend.borderColor)
  353.        
  354.         for i in 0..<entries.count {
  355.             let seriesIcon = Rect(
  356.                 origin: Point(legendRect.origin.x + plotLegend.textSize,
  357.                               legendRect.maxY - (2.0*Float(i) + 1.0)*plotLegend.textSize),
  358.                 size: Size(width: plotLegend.textSize, height: -plotLegend.textSize)
  359.             )
  360.             switch entries[i].1 {
  361.             case .square(let color):
  362.                 renderer.drawSolidRect(seriesIcon,
  363.                                        fillColor: color,
  364.                                        hatchPattern: .none)
  365.             case .shape(let shape, let color):
  366.                 shape.draw(in: seriesIcon,
  367.                            color: color,
  368.                            renderer: renderer)
  369.             }
  370.             let p = Point(seriesIcon.maxX + plotLegend.textSize, seriesIcon.minY)
  371.             renderer.drawText(text: entries[i].0,
  372.                               location: p,
  373.                               textSize: plotLegend.textSize,
  374.                               color: plotLegend.textColor,
  375.                               strokeWidth: 1.2,
  376.                               angle: 0)
  377.         }
  378.     }
  379. }
  380.  
  381. public protocol HasGraphLayout: AnyObject {
  382.    
  383.     var layout: GraphLayout { get set }
  384.    
  385.     var legendLabels: [(String, LegendIcon)] { get }
  386.    
  387.     func calculateScaleAndMarkerLocations(markers: inout PlotMarkers, size: Size, renderer: Renderer)
  388.    
  389.     func drawData(markers: PlotMarkers, size: Size, renderer: Renderer)
  390. }
  391.  
  392. extension HasGraphLayout {
  393.    
  394.     // Default implementation.
  395.     public var legendLabels: [(String, LegendIcon)] {
  396.         return []
  397.     }
  398.    
  399.     public var plotSize: Size {
  400.         get { layout.plotSize }
  401.         set { layout.plotSize = newValue }
  402.     }
  403.    
  404.     public var plotTitle: PlotTitle {
  405.         get { layout.plotTitle }
  406.         set { layout.plotTitle = newValue }
  407.     }
  408.     public var plotAnnotation : PlotAnnotation {
  409.         get { layout.plotAnnotation }
  410.         set {layout.plotAnnotation = newValue }
  411.     }
  412.     public var plotLabel: PlotLabel {
  413.         get { layout.plotLabel }
  414.         set { layout.plotLabel = newValue }
  415.     }
  416.     public var plotLegend: PlotLegend {
  417.         get { layout.plotLegend }
  418.         set { layout.plotLegend = newValue }
  419.     }
  420.     public var plotBorder: PlotBorder {
  421.         get { layout.plotBorder }
  422.         set { layout.plotBorder = newValue }
  423.     }
  424.     public var grid: Grid {
  425.         get { layout.grid }
  426.         set { layout.grid = newValue }
  427.     }
  428.     public var backgroundColor: Color {
  429.         get { layout.backgroundColor }
  430.         set { layout.backgroundColor = newValue }
  431.     }
  432.     public var plotBackgroundColor: Color? {
  433.         get { layout.plotBackgroundColor }
  434.         set { layout.plotBackgroundColor = newValue }
  435.     }
  436.  
  437.     public var markerTextSize: Float {
  438.         get { layout.markerTextSize }
  439.         set { layout.markerTextSize = newValue }
  440.     }
  441. }
  442.  
  443. extension Plot where Self: HasGraphLayout {
  444.    
  445.     public func drawGraph(size: Size, renderer: Renderer) {
  446.         layout.legendLabels = self.legendLabels
  447.         layout.plotSize = size
  448.         let results = layout.layout(renderer: renderer, calculateMarkers: { markers, size in
  449.             calculateScaleAndMarkerLocations(
  450.                 markers: &markers,
  451.                 size: size,
  452.                 renderer: renderer)
  453.         })
  454.         layout.drawBackground(results: results, renderer: renderer)
  455.         renderer.withAdditionalOffset(results.plotBorderRect.origin) { renderer in
  456.             drawData(markers: results.plotMarkers, size: results.plotBorderRect.size, renderer: renderer)
  457.         }
  458.         layout.drawForeground(results: results, renderer: renderer)
  459.         layout.drawAnnotation(results: results, renderer: renderer)
  460.     }
  461.    
  462. }
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top