Repository: dagronf/DSFSparkline Branch: master Commit: 37caab4b80f7 Files: 228 Total size: 1.6 MB Directory structure: gitextract_xvgxkjtt/ ├── .github/ │ └── workflows/ │ └── swift.yml ├── .gitignore ├── .swiftpm/ │ └── xcode/ │ ├── package.xcworkspace/ │ │ └── contents.xcworkspacedata │ └── xcshareddata/ │ └── xcschemes/ │ └── DSFSparkline.xcscheme ├── Demos/ │ ├── Documentation Project/ │ │ ├── Documentation Project/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AccentColor.colorset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── Main.storyboard │ │ │ ├── CenteringClipView.swift │ │ │ ├── Documentation_Project.entitlements │ │ │ ├── Info.plist │ │ │ ├── Utilities.swift │ │ │ └── ViewController.swift │ │ └── Documentation Project.xcodeproj/ │ │ ├── project.pbxproj │ │ └── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── swiftpm/ │ │ └── Package.resolved │ ├── Playground/ │ │ ├── Sparklines Playground.playground/ │ │ │ ├── Contents.swift │ │ │ ├── contents.xcplayground │ │ │ └── playground.xcworkspace/ │ │ │ └── contents.xcworkspacedata │ │ └── Sparklines Playground.xcworkspace/ │ │ └── contents.xcworkspacedata │ ├── Samples/ │ │ ├── Demos.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ └── contents.xcworkspacedata │ │ │ ├── xcshareddata/ │ │ │ │ └── xcschemes/ │ │ │ │ ├── All Demos.xcscheme │ │ │ │ ├── SwiftUI Sparkline Demo (iOS).xcscheme │ │ │ │ ├── SwiftUI Sparkline Demo (macOS).xcscheme │ │ │ │ ├── iOS Sparkline Demo.xcscheme │ │ │ │ ├── macOS Sparkline Demo Objc.xcscheme │ │ │ │ ├── macOS Sparkline Demo.xcscheme │ │ │ │ ├── macOS Table Demo.xcscheme │ │ │ │ └── tvOS Sparkline Demo.xcscheme │ │ │ └── xcuserdata/ │ │ │ └── dford.xcuserdatad/ │ │ │ └── xcdebugger/ │ │ │ └── Breakpoints_v2.xcbkptlist │ │ ├── SwiftUI Sparkline Crossplatform/ │ │ │ ├── Shared/ │ │ │ │ ├── ActiveView.swift │ │ │ │ ├── ActivityGridView.swift │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ ├── AccentColor.colorset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── AttributedString.swift │ │ │ │ ├── BarDemoContentView.swift │ │ │ │ ├── BitmapGenerationView.swift │ │ │ │ ├── CircularGaugeView.swift │ │ │ │ ├── CircularProgress.swift │ │ │ │ ├── ContentView.swift │ │ │ │ ├── DataBarGraphContent.swift │ │ │ │ ├── DotGraphView.swift │ │ │ │ ├── Extensions.swift │ │ │ │ ├── LineDemoContentView.swift │ │ │ │ ├── PercentBarView.swift │ │ │ │ ├── PieGraphDemoView.swift │ │ │ │ ├── ReportView.swift │ │ │ │ ├── StackLineDemoContentView.swift │ │ │ │ ├── StripesDemoView.swift │ │ │ │ ├── SwiftUI-Overlays/ │ │ │ │ │ ├── OverlayView.swift │ │ │ │ │ ├── StripesOverlaidView.swift │ │ │ │ │ ├── SuperCoolLineSpark.swift │ │ │ │ │ ├── SwiftUIContentView.swift │ │ │ │ │ └── SwiftUIView.swift │ │ │ │ ├── SwiftUI_Sparkline_DemoApp.swift │ │ │ │ ├── TabletView.swift │ │ │ │ ├── TestingView.swift │ │ │ │ ├── WinLossGraphContentView.swift │ │ │ │ └── WiperGaugeDemoView.swift │ │ │ ├── iOS/ │ │ │ │ └── Info.plist │ │ │ └── macOS/ │ │ │ ├── Info.plist │ │ │ └── macOS.entitlements │ │ ├── iOS Sparkline Demo/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ ├── SceneDelegate.swift │ │ │ ├── ViewController.swift │ │ │ └── iOS Sparkline Demo.entitlements │ │ ├── macOS Sparkline Demo/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ ├── ViewController.swift │ │ │ ├── cpuUsage.swift │ │ │ └── macOS_Sparkline_Demo.entitlements │ │ ├── macOS Sparkline Demo Objc/ │ │ │ ├── AppDelegate.h │ │ │ ├── AppDelegate.m │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ ├── ViewController.h │ │ │ ├── ViewController.m │ │ │ ├── macOS_Sparkline_Demo_Objc.entitlements │ │ │ └── main.m │ │ ├── macOS Table Demo/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── Main.storyboard │ │ │ ├── GridViewController.swift │ │ │ ├── Info.plist │ │ │ ├── ViewController.swift │ │ │ └── macOS_Table_Demo.entitlements │ │ └── tvOS Sparkline Demo/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── App Icon & Top Shelf Image.brandassets/ │ │ │ │ ├── App Icon - App Store.imagestack/ │ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── App Icon.imagestack/ │ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Top Shelf Image Wide.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Top Shelf Image.imageset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ViewController.swift │ └── Simple Wiper Gauge/ │ ├── Simple Wiper Gauge/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AccentColor.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── Simple_Wiper_Gauge.entitlements │ │ └── ViewController.swift │ └── Simple Wiper Gauge.xcodeproj/ │ ├── project.pbxproj │ └── project.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── swiftpm/ │ └── Package.resolved ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources/ │ └── DSFSparkline/ │ ├── DSFSparklines.h │ ├── Info.plist │ ├── datasource/ │ │ ├── DSFSparkline+DataSource.swift │ │ ├── DSFSparkline+StaticDataSource.swift │ │ └── data-core/ │ │ ├── SparklineData.swift │ │ └── SparklineWindow.swift │ ├── overlay/ │ │ ├── renderers/ │ │ │ ├── core/ │ │ │ │ ├── DSFSparklineOverlay+Centerable.swift │ │ │ │ ├── DSFSparklineOverlay+DataSource.swift │ │ │ │ ├── DSFSparklineOverlay+StaticDataSource.swift │ │ │ │ └── DSFSparklineOverlay.swift │ │ │ ├── overlays-components/ │ │ │ │ ├── DSFSparklineOverlay+GridLines.swift │ │ │ │ ├── DSFSparklineOverlay+RangeHighlight.swift │ │ │ │ └── DSFSparklineOverlay+ZeroLine.swift │ │ │ └── overlays-graph/ │ │ │ ├── DSFSparklineOverlay+ActivityGrid.swift │ │ │ ├── DSFSparklineOverlay+Bar.swift │ │ │ ├── DSFSparklineOverlay+CircularGauge.swift │ │ │ ├── DSFSparklineOverlay+CircularProgress.swift │ │ │ ├── DSFSparklineOverlay+DataBar.swift │ │ │ ├── DSFSparklineOverlay+Dot.swift │ │ │ ├── DSFSparklineOverlay+Line.swift │ │ │ ├── DSFSparklineOverlay+PercentBar.swift │ │ │ ├── DSFSparklineOverlay+Pie.swift │ │ │ ├── DSFSparklineOverlay+Stackline.swift │ │ │ ├── DSFSparklineOverlay+Stripes.swift │ │ │ ├── DSFSparklineOverlay+Tablet.swift │ │ │ ├── DSFSparklineOverlay+WinLossTie.swift │ │ │ └── DSFSparklineOverlay+WiperGauge.swift │ │ ├── surfaces/ │ │ │ ├── DSFSparklineSurface+AttributedString.swift │ │ │ ├── DSFSparklineSurface+Bitmap.swift │ │ │ ├── DSFSparklineSurface+SwiftUI.swift │ │ │ ├── DSFSparklineSurface+View.swift │ │ │ └── DSFSparklineSurface.swift │ │ └── types/ │ │ ├── DSFSparkline+ActivityGridDefinition.swift │ │ ├── DSFSparkline+GradientBucket.swift │ │ ├── DSFSparkline+GridLinesDefinition.swift │ │ ├── DSFSparkline+HighlightRangeDefinition.swift │ │ ├── DSFSparkline+Palette.swift │ │ ├── DSFSparkline+ValueBasedFill.swift │ │ ├── DSFSparkline+ZeroLineDefinition.swift │ │ ├── DSFSparkline.swift │ │ └── fill/ │ │ ├── DSFSparkline+FillColor.swift │ │ ├── DSFSparkline+FillGradient.swift │ │ └── DSFSparkline+Fillable.swift │ ├── prebuilt/ │ │ ├── DSFSparklineActivityGridView+SwiftUI.swift │ │ ├── DSFSparklineActivityGridView.swift │ │ ├── DSFSparklineBarGraphView+SwiftUI.swift │ │ ├── DSFSparklineBarGraphView.swift │ │ ├── DSFSparklineCircularGaugeView+SwiftUI.swift │ │ ├── DSFSparklineCircularGaugeView.swift │ │ ├── DSFSparklineCircularProgressView+SwiftUI.swift │ │ ├── DSFSparklineCircularProgressView.swift │ │ ├── DSFSparklineDataBarGraphView+SwiftUI.swift │ │ ├── DSFSparklineDataBarGraphView.swift │ │ ├── DSFSparklineDotGraphView+SwiftUI.swift │ │ ├── DSFSparklineDotGraphView.swift │ │ ├── DSFSparklineLineGraphView+SwiftUI.swift │ │ ├── DSFSparklineLineGraphView.swift │ │ ├── DSFSparklinePercentBarGraphView+SwiftUI.swift │ │ ├── DSFSparklinePercentBarGraphView.swift │ │ ├── DSFSparklinePieGraphView+SwiftUI.swift │ │ ├── DSFSparklinePieGraphView.swift │ │ ├── DSFSparklineStackLineGraphView+SwiftUI.swift │ │ ├── DSFSparklineStackLineGraphView.swift │ │ ├── DSFSparklineStripesGraphView+SwiftUI.swift │ │ ├── DSFSparklineStripesGraphView.swift │ │ ├── DSFSparklineTabletGraphView+SwiftUI.swift │ │ ├── DSFSparklineTabletGraphView.swift │ │ ├── DSFSparklineWinLossGraphView+SwiftUI.swift │ │ ├── DSFSparklineWinLossGraphView.swift │ │ ├── DSFSparklineWiperGaugeGraphView+SwiftUI.swift │ │ ├── DSFSparklineWiperGaugeGraphView.swift │ │ └── ui/ │ │ ├── DSFSparklineDataSourceView.swift │ │ └── DSFSparklineZeroLinedGraphView.swift │ └── util/ │ ├── Angle.swift │ ├── ArbitraryAnimator.swift │ ├── CGColor+BackwardsCompatibility.swift │ ├── CGContext+extensions.swift │ ├── CGPath+Hermite.swift │ ├── CGPath+innerShadow.swift │ ├── DSFSparkline+Shadow.swift │ ├── LayerInvalidating.swift │ ├── NSShadow+extensions.swift │ └── Utilities.swift ├── Tests/ │ └── DSFSparklineTests/ │ ├── CircularGaugeTests.swift │ ├── CircularProgressTests.swift │ ├── DSFSparklineTests.swift │ ├── LineGraphTests.swift │ ├── PresentationTests.swift │ └── utils/ │ └── TestFilesContainer.swift └── Tools/ └── sanity-check.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/swift.yml ================================================ # This workflow will build a Swift project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift name: Swift on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: macos_build_test: name: Build/test on macOS runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Build run: swift build -v - name: Run tests run: swift test -v ios_tests: name: Build/test on iOS runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Run tests on iOS run: xcodebuild -scheme DSFSparkline test -destination "platform=iOS Simulator,name=iPhone SE (3rd generation),OS=latest" ================================================ FILE: .gitignore ================================================ Demos/Demos.xcodeproj/project.xcworkspace/xcshareddata Demos/Demos.xcodeproj/project.xcworkspace/xcuserdata Demos/Demos.xcworkspace/xcshareddata Podfile.lock Demos/Demos.xcodeproj/xcshareddata Demos/Pods xcschememanagement.plist Demos/Demos.xcworkspace/xcuserdata .swiftpm/xcode/package.xcworkspace/xcuserdata .swiftpm/xcode/xcuserdata Demos/Demos.xcodeproj/xcuserdata Demos/Documentation Project/Documentation Project.xcodeproj/xcuserdata Demos/Documentation Project/Documentation Project.xcodeproj/project.xcworkspace/xcuserdata Demos/Samples/Demos.xcworkspace/xcuserdata Demos/Sparkline Demos/Sparkline Demos.xcodeproj/project.xcworkspace/xcuserdata/dford.xcuserdatad/UserInterfaceState.xcuserstate Demos/Sparkline Demos/Sparkline Demos.xcodeproj/xcuserdata/dford.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist Demos/Sparkline Demos/Sparkline Demos.xcodeproj/xcuserdata Demos/Sparkline Demos/Sparkline Demos.xcodeproj/project.xcworkspace/xcuserdata Demos/Playground/Sparklines Playground.xcworkspace/xcuserdata Demos/Playground/Sparklines Playground.playground/playground.xcworkspace/xcuserdata Demos/Playground/Sparklines Playground.xcworkspace/xcshareddata .build Demos/Samples/Demos.xcodeproj/project.xcworkspace/xcuserdata Demos/Simple Wiper Gauge/Simple Wiper Gauge.xcodeproj/xcuserdata IDEWorkspaceChecks.plist Demos/Simple Wiper Gauge/Simple Wiper Gauge.xcodeproj/project.xcworkspace/xcuserdata ================================================ FILE: .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: .swiftpm/xcode/xcshareddata/xcschemes/DSFSparkline.xcscheme ================================================ ================================================ FILE: Demos/Documentation Project/Documentation Project/AppDelegate.swift ================================================ // // AppDelegate.swift // Documentation project // // Created by Darren Ford on 14/2/21. // import Cocoa @main class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { // Insert code here to initialize your application } func applicationWillTerminate(_ aNotification: Notification) { // Insert code here to tear down your application } } ================================================ FILE: Demos/Documentation Project/Documentation Project/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Demos/Documentation Project/Documentation Project/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Demos/Documentation Project/Documentation Project/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Demos/Documentation Project/Documentation Project/Base.lproj/Main.storyboard ================================================ Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: Demos/Documentation Project/Documentation Project/CenteringClipView.swift ================================================ /* Copyright (C) 2016 Apple Inc. All Rights Reserved. See LICENSE.txt for this sample’s licensing information Abstract: Contains the definition for `CenteringClipView` which is a specialized clip view subclass to center its document. */ import Cocoa /** `CenteringClipView` is a clip view subclass that centers smaller documents views within its inset clip bounds (as described by the set `contentInsets`). */ class CenteringClipView: NSClipView { override func constrainBoundsRect(_ proposedBounds: NSRect) -> NSRect { guard let documentView = documentView else { return super.constrainBoundsRect(proposedBounds) } var newClipBoundsRect = super.constrainBoundsRect(proposedBounds) // Get the `contentInsets` scaled to the future bounds size. let insets = convertedContentInsetsToProposedBoundsSize(newClipBoundsRect.size) // Get the insets in terms of the view geometry edges, accounting for flippedness. let minYInset = isFlipped ? insets.top : insets.bottom let maxYInset = isFlipped ? insets.bottom : insets.top let minXInset = insets.left let maxXInset = insets.right /* Get and outset the `documentView`'s frame by the scaled contentInsets. The outset frame is used to align and constrain the `newClipBoundsRect`. */ let documentFrame = documentView.frame let outsetDocumentFrame = NSRect(x: documentFrame.minX - minXInset, y: documentFrame.minY - minYInset, width: (documentFrame.width + (minXInset + maxXInset)), height: documentFrame.height + (minYInset + maxYInset)) if newClipBoundsRect.width > outsetDocumentFrame.width { /* If the clip bounds width is larger than the document, center the bounds around the document. */ newClipBoundsRect.origin.x = outsetDocumentFrame.minX - (newClipBoundsRect.width - outsetDocumentFrame.width) / 2.0 } else if newClipBoundsRect.width < outsetDocumentFrame.width { /* Otherwise, the document is wider than the clip rect. Make sure that the clip rect stays within the document frame. */ if newClipBoundsRect.maxX > outsetDocumentFrame.maxX { // The clip rect is outside the maxX edge of the document, bring it in. newClipBoundsRect.origin.x = outsetDocumentFrame.maxX - newClipBoundsRect.width } else if newClipBoundsRect.minX < outsetDocumentFrame.minX { // The clip rect is outside the minX edge of the document, bring it in. newClipBoundsRect.origin.x = outsetDocumentFrame.minX } } if newClipBoundsRect.height > outsetDocumentFrame.height { /* If the clip bounds height is larger than the document, center the bounds around the document. */ newClipBoundsRect.origin.y = outsetDocumentFrame.minY - (newClipBoundsRect.height - outsetDocumentFrame.height) / 2.0 } else if newClipBoundsRect.height < outsetDocumentFrame.height { /* Otherwise, the document is taller than the clip rect. Make sure that the clip rect stays within the document frame. */ if newClipBoundsRect.maxY > outsetDocumentFrame.maxY { // The clip rect is outside the maxY edge of the document, bring it in. newClipBoundsRect.origin.y = outsetDocumentFrame.maxY - newClipBoundsRect.height } else if newClipBoundsRect.minY < outsetDocumentFrame.minY { // The clip rect is outside the minY edge of the document, bring it in. newClipBoundsRect.origin.y = outsetDocumentFrame.minY } } return backingAlignedRect(newClipBoundsRect, options: .alignAllEdgesNearest) } /** The `contentInsets` scaled to the scale factor of a new potential bounds rect. Used by `constrainBoundsRect(NSRect)`. */ fileprivate func convertedContentInsetsToProposedBoundsSize(_ proposedBoundsSize: NSSize) -> NSEdgeInsets { // Base the scale factor on the width scale factor to the new proposedBounds. let fromBoundsToProposedBoundsFactor = bounds.width > 0 ? (proposedBoundsSize.width / bounds.width) : 1.0 // Scale the set `contentInsets` by the width scale factor. var newContentInsets = contentInsets newContentInsets.top *= fromBoundsToProposedBoundsFactor newContentInsets.left *= fromBoundsToProposedBoundsFactor newContentInsets.bottom *= fromBoundsToProposedBoundsFactor newContentInsets.right *= fromBoundsToProposedBoundsFactor return newContentInsets } } /* Sample code project: Exhibition: An Adaptive OS X App Version: 1.2 IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2016 Apple Inc. All Rights Reserved. */ ================================================ FILE: Demos/Documentation Project/Documentation Project/Documentation_Project.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.files.user-selected.read-write ================================================ FILE: Demos/Documentation Project/Documentation Project/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSMainStoryboardFile Main NSPrincipalClass NSApplication ================================================ FILE: Demos/Documentation Project/Documentation Project/Utilities.swift ================================================ // // Utilities.swift // Documentation Project // // Created by Darren Ford on 14/4/21. // import Foundation import CoreGraphics /// A CGPath creation function using a block /// - Parameter block: The block used to add to a mutablepath object /// - Returns: The created path object /// /// Example :- /// ``` /// let path = CGPath.Create { path in /// path.move(to: CGPoint(x: 0, y: 5)) /// path.addLine(to: CGPoint(x: 10, y: 5)) /// path.move(to: CGPoint(x: 5, y: 0)) /// path.addLine(to: CGPoint(x: 10, y: 5)) /// } /// ``` @inlinable func CGPath(_ block: (CGMutablePath) throws -> Void) rethrows -> CGPath { let pth = CGMutablePath() try block(pth) return pth.copy()! } ================================================ FILE: Demos/Documentation Project/Documentation Project/ViewController.swift ================================================ // // ViewController.swift // Documentation project // // Created by Darren Ford on 14/2/21. // import Cocoa import DSFSparkline import SwiftImageReadWrite class FlippedClipView: NSClipView { override var isFlipped: Bool { return true } } public extension NSView { func snapshot() -> NSImage? { guard let bitmapRep = self.bitmapImageRepForCachingDisplay(in: self.bounds) else { return nil } self.cacheDisplay(in: self.bounds, to: bitmapRep) let image = NSImage() image.addRepresentation(bitmapRep) return image } } // Temperture anomolies // https://www.ncdc.noaa.gov/cag/global/time-series/globe/land_ocean/ytd/12/1880-2019 let land_ocean_temp_anomolies: [CGFloat] = [ -0.12, -0.09, -0.10, -0.19, -0.27, -0.26, -0.25, -0.29, -0.14, -0.10, -0.36, -0.27, -0.32, -0.34, -0.32, -0.25, -0.10, -0.11, -0.28, -0.16, -0.08, -0.16, -0.26, -0.37, -0.46, -0.28, -0.21, -0.39, -0.43, -0.44, -0.40, -0.44, -0.34, -0.32, -0.14, -0.09, -0.32, -0.39, -0.30, -0.24, -0.23, -0.16, -0.24, -0.25, -0.24, -0.18, -0.07, -0.17, -0.18, -0.33, -0.11, -0.06, -0.13, -0.26, -0.11, -0.16, -0.12, -0.01, -0.02, 0.01, 0.16, 0.27, 0.11, 0.11, 0.28, 0.18, -0.01, -0.04, -0.05, -0.08, -0.15, 0.00, 0.04, 0.13, -0.10, -0.13, -0.18, 0.07, 0.13, 0.08, 0.05, 0.09, 0.11, 0.12, -0.14, -0.07, -0.01, 0.00, -0.03, 0.11, 0.06, -0.07, 0.04, 0.19, -0.06, 0.01, -0.07, 0.21, 0.12, 0.23, 0.28, 0.32, 0.19, 0.36, 0.17, 0.16, 0.24, 0.38, 0.39, 0.29, 0.45, 0.39, 0.24, 0.28, 0.34, 0.47, 0.32, 0.51, 0.65, 0.44, 0.42, 0.57, 0.62, 0.64, 0.58, 0.67, 0.64, 0.62, 0.54, 0.64, 0.72, 0.57, 0.64, 0.68, 0.74, 0.93, 1.00, 0.91, 0.83, 0.95 ] var SimpleDataSet: DSFSparkline.DataSource = { let s = DSFSparkline.DataSource(values: [1, 5, 2, 3, 6, 1, 4]) s.range = 0 ... 7 return s }() var SimpleWinLossDataSet: DSFSparkline.DataSource = { let s = DSFSparkline.DataSource(values: [1, -5, -2, 3, 6, -1, 4]) return s }() class ViewController: NSViewController { @IBOutlet weak var primaryScrollView: NSScrollView! @IBOutlet weak var lineStandardView: DSFSparklineLineGraphView! @IBOutlet weak var lineCenteredView: DSFSparklineLineGraphView! @IBOutlet weak var lineInterpolatedStandardView: DSFSparklineLineGraphView! @IBOutlet weak var lineInterpolatedCenteredView: DSFSparklineLineGraphView! @IBOutlet weak var lineMarkersStandardView: DSFSparklineLineGraphView! @IBOutlet weak var lineMarkersCenteredView: DSFSparklineLineGraphView! @IBOutlet weak var lineMarkersCustomMarkersView1: DSFSparklineLineGraphView! @IBOutlet weak var lineMarkersCustomMarkersView2: DSFSparklineLineGraphView! @IBOutlet weak var barStandardView: DSFSparklineBarGraphView! @IBOutlet weak var barCenteredView: DSFSparklineBarGraphView! @IBOutlet weak var stackStandardView: DSFSparklineStackLineGraphView! @IBOutlet weak var stackCenteredView: DSFSparklineStackLineGraphView! @IBOutlet weak var dotStandardView: DSFSparklineDotGraphView! @IBOutlet weak var dotSecondView: DSFSparklineDotGraphView! @IBOutlet weak var winLoss: DSFSparklineWinLossGraphView! @IBOutlet weak var winLossTie: DSFSparklineWinLossGraphView! @IBOutlet weak var tablet1: DSFSparklineTabletGraphView! @IBOutlet weak var stripes1: DSFSparklineStripesGraphView! @IBOutlet weak var stripes2: DSFSparklineStripesGraphView! @IBOutlet weak var pie1: DSFSparklinePieGraphView! @IBOutlet weak var pie2: DSFSparklinePieGraphView! @IBOutlet weak var pie3: DSFSparklinePieGraphView! @IBOutlet weak var pie4: DSFSparklinePieGraphView! @IBOutlet weak var pie5: DSFSparklinePieGraphView! @IBOutlet weak var pie6: DSFSparklinePieGraphView! @IBOutlet weak var pie7: DSFSparklinePieGraphView! @IBOutlet weak var pie8: DSFSparklinePieGraphView! @IBOutlet weak var pieContainerView: NSView! @IBOutlet weak var databarPercent1: DSFSparklineDataBarGraphView! @IBOutlet weak var databarPercent2: DSFSparklineDataBarGraphView! @IBOutlet weak var databarPercent3: DSFSparklineDataBarGraphView! @IBOutlet weak var databarPercent4: DSFSparklineDataBarGraphView! @IBOutlet weak var databarPercent5: DSFSparklineDataBarGraphView! @IBOutlet weak var databarPercent6: DSFSparklineDataBarGraphView! @IBOutlet weak var databarContainerView: NSView! @IBOutlet weak var databarTotal1: DSFSparklineDataBarGraphView! @IBOutlet weak var databarTotal2: DSFSparklineDataBarGraphView! @IBOutlet weak var databarTotal3: DSFSparklineDataBarGraphView! @IBOutlet weak var databarTotal4: DSFSparklineDataBarGraphView! @IBOutlet weak var databarTotal5: DSFSparklineDataBarGraphView! @IBOutlet weak var databarTotal6: DSFSparklineDataBarGraphView! @IBOutlet weak var databarTotalContainerView: NSView! @IBOutlet weak var percentBarTotalContainerView: NSView! @IBOutlet weak var percentBarTotalContainerView2: NSView! @IBOutlet weak var wiperGauge: DSFSparklineWiperGaugeGraphView! @IBOutlet weak var percentBarGraph1: DSFSparklinePercentBarGraphView! @IBOutlet weak var percentBarGraph2: DSFSparklinePercentBarGraphView! @IBOutlet weak var percentBarGraph3: DSFSparklinePercentBarGraphView! @IBOutlet weak var percentBarGraph11: DSFSparklinePercentBarGraphView! @IBOutlet weak var percentBarGraph12: DSFSparklinePercentBarGraphView! @IBOutlet weak var percentBarGraph13: DSFSparklinePercentBarGraphView! @IBOutlet weak var wiperContainer: NSView! @IBOutlet weak var wiperGauge1: DSFSparklineWiperGaugeGraphView! @IBOutlet weak var wiperGauge2: DSFSparklineWiperGaugeGraphView! @IBOutlet weak var wiperGauge3: DSFSparklineWiperGaugeGraphView! @IBOutlet weak var wiperGauge4: DSFSparklineWiperGaugeGraphView! @IBOutlet weak var wiperGauge5: DSFSparklineWiperGaugeGraphView! @IBOutlet weak var wiperGauge6: DSFSparklineWiperGaugeGraphView! @IBOutlet weak var wiperGauge7: DSFSparklineWiperGaugeGraphView! @IBOutlet weak var activityGrid1: DSFSparklineActivityGridView! @IBOutlet weak var activityGrid2: DSFSparklineActivityGridView! var bitmapMap: [String: Data] = [:] var nameMap: [String: NSView] = [:] func buildNameMap() { nameMap["line-standard"] = lineStandardView nameMap["line-centered"] = lineCenteredView nameMap["line-interpolated"] = lineInterpolatedStandardView nameMap["line-interpolated-centered"] = lineInterpolatedCenteredView nameMap["line-markers"] = lineMarkersStandardView nameMap["line-markers-centered"] = lineMarkersCenteredView nameMap["line-custom-marker-1"] = self.lineMarkersCustomMarkersView1 nameMap["line-custom-marker-2"] = self.lineMarkersCustomMarkersView2 nameMap["bar-standard"] = barStandardView nameMap["bar-centered"] = barCenteredView nameMap["stackline-standard"] = stackStandardView nameMap["stackline-centered"] = stackCenteredView nameMap["dot-standard"] = dotStandardView nameMap["dot-inverted"] = dotSecondView nameMap["win-loss"] = self.winLoss nameMap["win-loss-tie"] = self.winLossTie nameMap["tablet"] = self.tablet1 nameMap["pie"] = self.pieContainerView nameMap["databar"] = self.databarContainerView nameMap["databar-max"] = self.databarTotalContainerView nameMap["stripes-standard"] = self.stripes1 nameMap["stripes-integral"] = self.stripes2 nameMap["percent-bar"] = self.percentBarTotalContainerView nameMap["percent-bar-2"] = self.percentBarTotalContainerView2 nameMap["wiper-gauge"] = self.wiperContainer nameMap["activity-grid-1"] = self.activityGrid1 nameMap["activity-grid-2"] = self.activityGrid2 } fileprivate var lineSource: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: 0 ... 1, zeroLineValue: 0.3) d.push(values: [ 0.72, 0.84, 0.15, 0.16, 0.30, 0.58, 0.87, 0.44, 0.02, 0.27, 0.48, 0.16, 0.15, 0.14, 0.81, 0.53, 0.67, 0.52, 0.07, 0.50 ]) return d }() fileprivate var dotSource: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 70, range: 0 ... 1) d.push(values: [ 0.73, 0.86, 0.72, 0.35, 0.36, 0.09, 0.06, 0.89, 0.94, 0.22, 0.52, 0.74, 0.53, 0.89, 0.34, 0.67, 0.88, 0.00, 0.78, 0.99, 0.84, 0.55, 0.02, 0.96, 0.66, 0.94, 0.70, 0.27, 0.18, 0.02, 0.18, 0.03, 0.00, 0.23, 0.93, 0.17, 0.48, 0.34, 0.89, 0.56, 0.70, 0.59, 0.12, 0.77, 0.98, 0.31, 0.10, 0.47, 0.42, 0.06, 1.00, 0.12, 0.50, 0.18, 0.02, 0.90, 0.33, 0.05, 0.60, 0.17, 0.53, 0.84, 0.72, 0.39, 0.56, 0.57, 0.61, 0.23, 0.96, 0.85 ]) return d }() fileprivate var winLossDataSource1: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 10, range: -1.0 ... 1) d.push(values: [1, -1, 0, 1, -1, -1, 1, -1, 0, 1]) return d }() fileprivate var landOceanTempAnomolies: DSFSparkline.DataSource = { let e = DSFSparkline.DataSource(windowSize: UInt(land_ocean_temp_anomolies.count)) e.set(values: land_ocean_temp_anomolies) return e }() fileprivate var twiggle: DSFSparkline.DataSource = { let e = DSFSparkline.DataSource(windowSize: 50) var v: Int = 0 let vv: [CGFloat] = (0 ..< 24).map { value in let d = v v = (v + 1) % 3 if d == 0 { return -1 } if d == 1 { return 0 } return 1 } e.set(values: vv) e.windowSize = 50 return e }() fileprivate var world: DSFSparkline.DataSource = { let e = DSFSparkline.DataSource(windowSize: UInt(rawData.count)) e.set(values: rawData) return e }() fileprivate var australianAnomaly: DSFSparkline.DataSource = { let e = DSFSparkline.DataSource(windowSize: UInt(temperature.count)) e.set(values: temperature) return e }() override func viewDidLoad() { super.viewDidLoad() do { let winloss = DSFSparkline.DataSource(values: [1, 1, 0, -1, 1, 1, 1, 0, 1, -1]) let bitmap = DSFSparklineSurface.Bitmap() let zeroLine = DSFSparklineOverlay.ZeroLine() zeroLine.dataSource = winloss // zeroLine.strokeWidth = 1.0 // zeroLine.strokeColor = NSColor.textColor.cgColor bitmap.addOverlay(zeroLine) let graph = DSFSparklineOverlay.WinLossTie() graph.dataSource = winloss bitmap.addOverlay(graph) // Generate an image with retina scale let image = bitmap.image(width: 75, height: 16, scale: 2) assert(image != nil) } self.buildNameMap() self.percentBarGraph1.displayStyle.barEdgeInsets = NSEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) self.percentBarGraph2.displayStyle.barEdgeInsets = NSEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) self.percentBarGraph3.displayStyle.barEdgeInsets = NSEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) self.percentBarGraph11.displayStyle.barEdgeInsets = NSEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) self.percentBarGraph12.displayStyle.barEdgeInsets = NSEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) self.percentBarGraph13.displayStyle.barEdgeInsets = NSEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) do { let source = DSFSparkline.DataSource(values: [4, 1, 8, 7, 5, 9, 3], range: 0 ... 10) let baseColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.033, 0.277, 0.650, 1.000])! let primaryStroke = baseColor // (gray: 0.0, alpha: 0.3)) let primaryFill = DSFSparkline.Fill.Color(baseColor.copy(alpha: 0.3)!) let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let line = DSFSparklineOverlay.Line() // Create a line overlay line.strokeWidth = 1 line.primaryStrokeColor = primaryStroke line.primaryFill = primaryFill line.dataSource = source // Assign the datasource to the overlay bitmap.addOverlay(line) // And add the overlay to the surface. let attr = bitmap.attributedString(size: CGSize(width: 40, height: 18), scale: 2)! let message = NSMutableAttributedString(string: "Inlined ") message.append(attr) message.append(NSAttributedString(string: " line graph")) Swift.print(message) } // do { // guard let scroll = self.primaryScrollView, // let documentView = scroll.documentView else { // return // } // // scroll.translatesAutoresizingMaskIntoConstraints = false // // let clip = scroll.contentView // clip.translatesAutoresizingMaskIntoConstraints = false // // scroll.addConstraint(NSLayoutConstraint(item: clip, attribute: .left, relatedBy: .equal, toItem: scroll, attribute: .left, multiplier: 1, constant: 0)) // scroll.addConstraint(NSLayoutConstraint(item: clip, attribute: .top, relatedBy: .equal, toItem: scroll, attribute: .top, multiplier: 1, constant: 0)) // scroll.addConstraint(NSLayoutConstraint(item: clip, attribute: .right, relatedBy: .equal, toItem: scroll, attribute: .right, multiplier: 1, constant: 0)) // scroll.addConstraint(NSLayoutConstraint(item: clip, attribute: .bottom, relatedBy: .equal, toItem: scroll, attribute: .bottom, multiplier: 1, constant: 0)) // // documentView.translatesAutoresizingMaskIntoConstraints = false // // clip.addConstraint(NSLayoutConstraint(item: clip, attribute: .left, relatedBy: .equal, toItem: documentView, attribute: .left, multiplier: 1, constant: 0)) // clip.addConstraint(NSLayoutConstraint(item: clip, attribute: .top, relatedBy: .equal, toItem: documentView, attribute: .top, multiplier: 1, constant: 0)) // //clip.addConstraint(NSLayoutConstraint(item: clip, attribute: .right, relatedBy: .equal, toItem: documentView, attribute: .right, multiplier: 1, constant: 0)) // } self.lineStandardView.dataSource = lineSource self.lineCenteredView.dataSource = lineSource self.lineInterpolatedStandardView.dataSource = lineSource self.lineInterpolatedCenteredView.dataSource = lineSource self.lineMarkersStandardView.dataSource = lineSource self.lineMarkersCenteredView.dataSource = lineSource self.lineMarkersCustomMarkersView1.dataSource = lineSource self.lineMarkersCustomMarkersView1.markerDrawingBlock = { context, markerFrames in let maxV = markerFrames.min { (a, b) -> Bool in a.value > b.value }! let minV = markerFrames.min { (a, b) -> Bool in a.value < b.value }! // Min context.setFillColor(DSFColor.systemRed.cgColor) context.fill(minV.rect) context.setLineWidth(0.5) context.setStrokeColor(DSFColor.white.cgColor) context.stroke(minV.rect) // Max context.setFillColor(DSFColor.systemGreen.cgColor) context.fill(maxV.rect) context.setLineWidth(0.5) context.setStrokeColor(DSFColor.white.cgColor) context.stroke(maxV.rect) } self.lineMarkersCustomMarkersView2.dataSource = lineSource self.lineMarkersCustomMarkersView2.markerDrawingBlock = { context, markerFrames in let lastFrames = markerFrames.suffix(5) lastFrames.forEach { marker in let path = CGPath { p in p.move(to: CGPoint(x: marker.rect.minX, y: marker.rect.midY)) p.addLine(to: CGPoint(x: marker.rect.maxX, y: marker.rect.midY)) p.move(to: CGPoint(x: marker.rect.midX, y: marker.rect.minY)) p.addLine(to: CGPoint(x: marker.rect.midX, y: marker.rect.maxY)) } context.addPath(path) context.setStrokeColor(DSFColor.systemBlue.cgColor) context.setLineWidth(2) context.strokePath() } } self.barStandardView.dataSource = lineSource self.barCenteredView.dataSource = lineSource self.stackStandardView.dataSource = lineSource self.stackCenteredView.dataSource = lineSource self.dotStandardView.dataSource = dotSource self.dotSecondView.dataSource = dotSource self.winLoss.dataSource = winLossDataSource1 self.winLossTie.dataSource = winLossDataSource1 self.tablet1.dataSource = winLossDataSource1 self.pie1.dataSource = DSFSparkline.StaticDataSource([10, 30, 20]) self.pie2.dataSource = DSFSparkline.StaticDataSource([33, 33, 33]) self.pie3.dataSource = DSFSparkline.StaticDataSource([40, 5, 80]) self.pie4.dataSource = DSFSparkline.StaticDataSource([1, 2, 3]) self.pie5.dataSource = DSFSparkline.StaticDataSource([66.7, 66, 66.9]) self.pie6.dataSource = DSFSparkline.StaticDataSource([9, 9, 4]) self.pie7.dataSource = DSFSparkline.StaticDataSource([0.5, 0.1, 0.1]) self.pie8.dataSource = DSFSparkline.StaticDataSource([1000, 2000, 300]) let palette = DSFSparkline.Palette([ DSFColor(deviceRed: 0, green: 0, blue: 1, alpha: 1), DSFColor(deviceRed: 0.33, green: 0.33, blue: 1, alpha: 1), DSFColor(deviceRed: 0.66, green: 0.66, blue: 1, alpha: 1) ]) self.pie2.palette = DSFSparkline.Palette.sharedGrays self.pie4.palette = palette self.pie6.palette = palette self.pie6.palette = DSFSparkline.Palette.sharedGrays self.pie8.palette = palette /// self.databarPercent1.dataSource = DSFSparkline.StaticDataSource([10, 30, 20]) self.databarPercent2.dataSource = DSFSparkline.StaticDataSource([33, 33, 33]) self.databarPercent3.dataSource = DSFSparkline.StaticDataSource([40, 5, 80]) self.databarPercent4.dataSource = DSFSparkline.StaticDataSource([1, 2, 3]) self.databarPercent5.dataSource = DSFSparkline.StaticDataSource([66.7, 66, 66.9]) self.databarPercent6.dataSource = DSFSparkline.StaticDataSource([9, 9, 4]) self.databarTotal1.dataSource = DSFSparkline.StaticDataSource([8, 19, 20]) self.databarTotal1.maximumTotalValue = 60 self.databarTotal2.dataSource = DSFSparkline.StaticDataSource([20, 20, 20]) self.databarTotal2.maximumTotalValue = 60 self.databarTotal3.dataSource = DSFSparkline.StaticDataSource([30, 5, 12]) self.databarTotal3.maximumTotalValue = 60 self.databarTotal4.dataSource = DSFSparkline.StaticDataSource([10, 15, 20]) self.databarTotal4.maximumTotalValue = 60 self.databarTotal5.dataSource = DSFSparkline.StaticDataSource([25, 10, 12]) self.databarTotal5.maximumTotalValue = 60 self.databarTotal6.dataSource = DSFSparkline.StaticDataSource([9, 9, 4]) self.databarTotal6.maximumTotalValue = 60 let gradient1 = DSFSparkline.GradientBucket(posts: [ DSFSparkline.GradientBucket.Post(r: 0, g: 0, b: 1, location: 0.0), DSFSparkline.GradientBucket.Post(r: 1, g: 1, b: 1, location: 0.5), DSFSparkline.GradientBucket.Post(r: 1, g: 0, b: 0, location: 1.0), ]) let gradient2 = DSFSparkline.GradientBucket(posts: [ DSFSparkline.GradientBucket.Post(color: NSColor.systemYellow.cgColor, location: 0), DSFSparkline.GradientBucket.Post(r: 0.3, g: 0, b: 0.3, location: 1.0) ]) self.stripes1.gradient = gradient1 self.stripes1.integral = false self.stripes1.dataSource = world //self.stripes1.dataSource = landOceanTempAnomolies //self.stripes1.dataSource = twiggle /// self.stripes2.dataSource = australianAnomaly australianAnomaly.range = -1.5 ... 1.5 self.stripes2.gradient = gradient2 self.stripes2.barSpacing = 1 self.stripes2.integral = true do { activityGrid1.cellTooltipString = { index in "\(index)" } activityGrid1.cellFillScheme = DSFSparkline.ActivityGrid.CellStyle.DefaultLight activityGrid1.setValues((0 ..< 1000).map { _ in CGFloat.random(in: 0 ... 1)}, range: 0 ... 1) let palette2 = [ DSFColor(red: 0.706, green: 0.020, blue: 0.151, alpha: 1.0), DSFColor(red: 0.845, green: 0.324, blue: 0.265, alpha: 1.0), DSFColor(red: 0.932, green: 0.520, blue: 0.408, alpha: 1.0), DSFColor(red: 0.970, green: 0.678, blue: 0.562, alpha: 1.0), DSFColor(red: 0.949, green: 0.795, blue: 0.720, alpha: 1.0), DSFColor(red: 0.867, green: 0.867, blue: 0.867, alpha: 1.0), DSFColor(red: 0.752, green: 0.832, blue: 0.960, alpha: 1.0), DSFColor(red: 0.621, green: 0.745, blue: 1.000, alpha: 1.0), DSFColor(red: 0.484, green: 0.621, blue: 0.978, alpha: 1.0), DSFColor(red: 0.352, green: 0.469, blue: 0.889, alpha: 1.0), DSFColor(red: 0.230, green: 0.299, blue: 0.751, alpha: 1.0), ] activityGrid2.cellFillScheme = .init(colors: palette2) activityGrid2.cellDimension = 9 activityGrid2.verticalCellCount = 8 activityGrid2.cellSpacing = 3 activityGrid2.cellBorderColor = .black.withAlphaComponent(1) activityGrid2.cellBorderWidth = 0.5 activityGrid2.setValues((0 ..< 1000).map { _ in CGFloat.random(in: 0 ... 1)}, range: 0 ... 1) let surface = DSFSparklineSurface.Bitmap() let grid = DSFSparklineOverlay.ActivityGrid() grid.dataSource = DSFSparkline.StaticDataSource((0 ... 1000).map { _ in CGFloat.random(in: 0 ... 1) }, range: 0 ... 1) grid.verticalCellCount = 1 grid.cellStyle = .init(fillScheme: DSFSparkline.ActivityGrid.CellStyle.DefaultLight, borderColor: .black, borderWidth: 0.5, cellDimension: 11, cellSpacing: 2.5) surface.addOverlay(grid) let img = surface.image(width: 200, height: 14, scale: 2)! let pngdata = try! img.representation.png(dpi: 144) bitmapMap["activity-grid-mini.png"] = pngdata } } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } @IBAction func generate(_ sender: Any) { let dialog = NSOpenPanel(); dialog.title = "Choose a folder"; dialog.showsResizeIndicator = true dialog.allowsMultipleSelection = false dialog.canChooseDirectories = true dialog.canChooseFiles = false dialog.canCreateDirectories = true dialog.beginSheetModal(for: self.view.window!, completionHandler: { response in guard response == NSApplication.ModalResponse.OK, let url = dialog.url else { return } DispatchQueue.main.async { [weak self] in self?.generate(path: url) } }) } func generate(path: URL) { self.nameMap.forEach { (name, view) in let name = "\(name).png" let outputFile = path.appendingPathComponent(name) guard let image = view.snapshot(), let tiff = image.tiffRepresentation, let imageRep = NSBitmapImageRep(data: tiff), let pngData = imageRep.representation(using: .png, properties: [:]) else { return } do { try pngData.write(to: outputFile) } catch { Swift.print("\(error)") } } self.bitmapMap.forEach { (name: String, value: Data) in let outputFile = path.appendingPathComponent(name) do { try value.write(to: outputFile) } catch { Swift.print("\(error)") } } } } ///// // https://www.metoffice.gov.uk/hadobs/hadcrut4/data/current/download.html // https://www.metoffice.gov.uk/hadobs/hadcrut4/data/current/time_series/HadCRUT.4.6.0.0.annual_ns_avg.txt // Data format :- https://www.metoffice.gov.uk/hadobs/hadcrut4/data/current/series_format.html let rawData: [CGFloat] = [ -0.373, -0.218, -0.228, -0.269, -0.248, -0.272, -0.358, -0.461, -0.467, -0.284, -0.343, -0.407, -0.524, -0.278, -0.494, -0.279, -0.251, -0.321, -0.238, -0.262, -0.276, -0.335, -0.227, -0.304, -0.368, -0.395, -0.384, -0.075, 0.035, -0.230, -0.227, -0.200, -0.213, -0.296, -0.409, -0.389, -0.367, -0.418, -0.307, -0.171, -0.416, -0.330, -0.455, -0.473, -0.410, -0.390, -0.186, -0.206, -0.412, -0.289, -0.203, -0.259, -0.402, -0.479, -0.520, -0.377, -0.283, -0.465, -0.511, -0.522, -0.490, -0.544, -0.437, -0.424, -0.244, -0.141, -0.383, -0.468, -0.333, -0.275, -0.247, -0.187, -0.302, -0.276, -0.294, -0.215, -0.108, -0.210, -0.206, -0.350, -0.137, -0.087, -0.137, -0.273, -0.131, -0.178, -0.147, -0.026, -0.006, -0.052, 0.014, 0.020, -0.027, -0.004, 0.144, 0.025, -0.071, -0.038, -0.039, -0.074, -0.173, -0.052, 0.028, 0.097, -0.129, -0.190, -0.267, -0.007, 0.046, 0.017, -0.049, 0.038, 0.014, 0.048, -0.223, -0.140, -0.068, -0.074, -0.113, 0.032, -0.027, -0.186, -0.065, 0.062, -0.214, -0.149, -0.241, 0.047, -0.062, 0.057, 0.092, 0.140, 0.011, 0.194, -0.014, -0.030, 0.045, 0.192, 0.198, 0.118, 0.296, 0.254, 0.105, 0.148, 0.208, 0.325, 0.183, 0.390, 0.539, 0.306, 0.294, 0.441, 0.496, 0.505, 0.447, 0.545, 0.506, 0.491, 0.395, 0.506, 0.560, 0.425, 0.470, 0.514, 0.579, 0.763, 0.797, 0.677, 0.597, 0.736, 0.768 ] // Annual mean temperature anomaly // http://www.bom.gov.au/climate/change/#tabs=Tracker&tracker=timeseries&tQ=graph%3Dtmean%26area%3Daus%26season%3D0112 let temperature: [CGFloat] = [ -0.50, -0.68, -0.20, -0.87, 0.12, 0.07, -0.57, -1.24, -0.54, -0.15, -0.53, -0.23, -0.47, -0.38, -0.69, -0.77, -0.17, -0.51, 0.16, -0.87, -0.24, -0.59, -0.42, -0.45, -0.36, -0.50, -0.14, -0.36, 0.19, -0.62, -0.24, -0.55, 0.08, -0.62, -0.40, -0.29, -0.73, -0.25, -0.45, -0.94, -0.61, -0.43, -0.43, -0.45, -0.36, -0.32, -0.92, 0.04, 0.14, 0.24, -0.66, 0.05, -0.11, -0.13, -0.22, 0.25, -0.50, -0.22, -0.39, -0.03, -0.10, -0.22, 0.15, 0.54, -0.70, -0.22, -0.75, -0.04, -0.31, 0.37, 0.74, 0.27, -0.04, 0.33, -0.38, 0.21, 0.22, 0.17, 0.73, -0.02, 0.47, 0.60, 0.12, 0.31, 0.18, 0.16, 0.60, 0.30, 0.97, 0.32, -0.04, 0.05, 0.71, 0.69, 0.54, 1.16, 0.50, 0.76, 0.45, 0.93, 0.33, -0.00, 0.24, 1.33, 1.04, 0.94, 0.99, 1.06, 1.12, 1.52, 1.15 ] ================================================ FILE: Demos/Documentation Project/Documentation Project.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 236A660C2626C0C0004B70C9 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236A660B2626C0C0004B70C9 /* Utilities.swift */; }; 239088F82B0D6A4C0059761E /* SwiftImageReadWrite in Frameworks */ = {isa = PBXBuildFile; productRef = 239088F72B0D6A4C0059761E /* SwiftImageReadWrite */; }; 23EE63DF25D875C7000C85E4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE63DE25D875C7000C85E4 /* AppDelegate.swift */; }; 23EE63E125D875C7000C85E4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE63E025D875C7000C85E4 /* ViewController.swift */; }; 23EE63E325D875C8000C85E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 23EE63E225D875C8000C85E4 /* Assets.xcassets */; }; 23EE63E625D875C8000C85E4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 23EE63E425D875C8000C85E4 /* Main.storyboard */; }; 23EE640925D87663000C85E4 /* DSFSparkline in Frameworks */ = {isa = PBXBuildFile; productRef = 23EE640825D87663000C85E4 /* DSFSparkline */; }; 23EE640C25D877E7000C85E4 /* CenteringClipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE640B25D877E7000C85E4 /* CenteringClipView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 236A660B2626C0C0004B70C9 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; 23EE63DB25D875C7000C85E4 /* Documentation Project.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Documentation Project.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 23EE63DE25D875C7000C85E4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23EE63E025D875C7000C85E4 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23EE63E225D875C8000C85E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23EE63E525D875C8000C85E4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23EE63E725D875C8000C85E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23EE63E825D875C8000C85E4 /* Documentation_Project.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Documentation_Project.entitlements; sourceTree = ""; }; 23EE640525D8763F000C85E4 /* DSFSparkline */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DSFSparkline; path = ../..; sourceTree = ""; }; 23EE640B25D877E7000C85E4 /* CenteringClipView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CenteringClipView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 23EE63D825D875C7000C85E4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 239088F82B0D6A4C0059761E /* SwiftImageReadWrite in Frameworks */, 23EE640925D87663000C85E4 /* DSFSparkline in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 23EE63D225D875C7000C85E4 = { isa = PBXGroup; children = ( 23EE640525D8763F000C85E4 /* DSFSparkline */, 23EE63DD25D875C7000C85E4 /* Documentation Project */, 23EE63DC25D875C7000C85E4 /* Products */, 23EE640725D87663000C85E4 /* Frameworks */, ); sourceTree = ""; }; 23EE63DC25D875C7000C85E4 /* Products */ = { isa = PBXGroup; children = ( 23EE63DB25D875C7000C85E4 /* Documentation Project.app */, ); name = Products; sourceTree = ""; }; 23EE63DD25D875C7000C85E4 /* Documentation Project */ = { isa = PBXGroup; children = ( 23EE63DE25D875C7000C85E4 /* AppDelegate.swift */, 23EE63E025D875C7000C85E4 /* ViewController.swift */, 23EE63E225D875C8000C85E4 /* Assets.xcassets */, 23EE63E425D875C8000C85E4 /* Main.storyboard */, 23EE640B25D877E7000C85E4 /* CenteringClipView.swift */, 23EE63E725D875C8000C85E4 /* Info.plist */, 23EE63E825D875C8000C85E4 /* Documentation_Project.entitlements */, 236A660B2626C0C0004B70C9 /* Utilities.swift */, ); path = "Documentation Project"; sourceTree = ""; }; 23EE640725D87663000C85E4 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 23EE63DA25D875C7000C85E4 /* Documentation Project */ = { isa = PBXNativeTarget; buildConfigurationList = 23EE63EB25D875C8000C85E4 /* Build configuration list for PBXNativeTarget "Documentation Project" */; buildPhases = ( 23EE63D725D875C7000C85E4 /* Sources */, 23EE63D825D875C7000C85E4 /* Frameworks */, 23EE63D925D875C7000C85E4 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Documentation Project"; packageProductDependencies = ( 23EE640825D87663000C85E4 /* DSFSparkline */, 239088F72B0D6A4C0059761E /* SwiftImageReadWrite */, ); productName = "Documentation Project"; productReference = 23EE63DB25D875C7000C85E4 /* Documentation Project.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 23EE63D325D875C7000C85E4 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1240; LastUpgradeCheck = 1240; TargetAttributes = { 23EE63DA25D875C7000C85E4 = { CreatedOnToolsVersion = 12.4; }; }; }; buildConfigurationList = 23EE63D625D875C7000C85E4 /* Build configuration list for PBXProject "Documentation Project" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 23EE63D225D875C7000C85E4; packageReferences = ( 239088F62B0D6A4C0059761E /* XCRemoteSwiftPackageReference "SwiftImageReadWrite" */, ); productRefGroup = 23EE63DC25D875C7000C85E4 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 23EE63DA25D875C7000C85E4 /* Documentation Project */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 23EE63D925D875C7000C85E4 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 23EE63E325D875C8000C85E4 /* Assets.xcassets in Resources */, 23EE63E625D875C8000C85E4 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 23EE63D725D875C7000C85E4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 23EE63E125D875C7000C85E4 /* ViewController.swift in Sources */, 23EE63DF25D875C7000C85E4 /* AppDelegate.swift in Sources */, 236A660C2626C0C0004B70C9 /* Utilities.swift in Sources */, 23EE640C25D877E7000C85E4 /* CenteringClipView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 23EE63E425D875C8000C85E4 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 23EE63E525D875C8000C85E4 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 23EE63E925D875C8000C85E4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 11.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 23EE63EA25D875C8000C85E4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 11.1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 23EE63EC25D875C8000C85E4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "Documentation Project/Documentation_Project.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Documentation Project/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.DSFSparkline.Documentation-Project"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Debug; }; 23EE63ED25D875C8000C85E4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "Documentation Project/Documentation_Project.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Documentation Project/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.DSFSparkline.Documentation-Project"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 23EE63D625D875C7000C85E4 /* Build configuration list for PBXProject "Documentation Project" */ = { isa = XCConfigurationList; buildConfigurations = ( 23EE63E925D875C8000C85E4 /* Debug */, 23EE63EA25D875C8000C85E4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 23EE63EB25D875C8000C85E4 /* Build configuration list for PBXNativeTarget "Documentation Project" */ = { isa = XCConfigurationList; buildConfigurations = ( 23EE63EC25D875C8000C85E4 /* Debug */, 23EE63ED25D875C8000C85E4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ 239088F62B0D6A4C0059761E /* XCRemoteSwiftPackageReference "SwiftImageReadWrite" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/dagronf/SwiftImageReadWrite"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 239088F72B0D6A4C0059761E /* SwiftImageReadWrite */ = { isa = XCSwiftPackageProductDependency; package = 239088F62B0D6A4C0059761E /* XCRemoteSwiftPackageReference "SwiftImageReadWrite" */; productName = SwiftImageReadWrite; }; 23EE640825D87663000C85E4 /* DSFSparkline */ = { isa = XCSwiftPackageProductDependency; productName = DSFSparkline; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 23EE63D325D875C7000C85E4 /* Project object */; } ================================================ FILE: Demos/Documentation Project/Documentation Project.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Demos/Documentation Project/Documentation Project.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "object": { "pins": [ { "package": "SwiftImageReadWrite", "repositoryURL": "https://github.com/dagronf/SwiftImageReadWrite", "state": { "branch": null, "revision": "42ace2412279f18bc264bc306e96b51c36e12a33", "version": "1.9.2" } } ] }, "version": 1 } ================================================ FILE: Demos/Playground/Sparklines Playground.playground/Contents.swift ================================================ import Cocoa import DSFSparkline var str = "Playground demonstrating the generation of a sparkline bitmap" // Set this to 'true' to save the generated bitmaps out to the /tmp folder let shouldSaveImage = false func SaveImage(image: NSImage, path: URL) { guard shouldSaveImage else { return } guard let tiff = image.tiffRepresentation, let imageRep = NSBitmapImageRep(data: tiff), let pngData = imageRep.representation(using: .png, properties: [:]) else { return } do { try pngData.write(to: path) } catch { Swift.print("\(error)") } } // - MARK: Simple definitions let source = DSFSparkline.DataSource(values: [4, 1, 8, 7, 5, 9, 3], range: 0 ... 10) let biggersource = DSFSparkline.DataSource(values: [4, 1, 8, 7, 5, 9, 7, 6, 7, 8, 3, 3, 5, 3, 4, 1, 2, 9, 1, 3, 3], range: 0 ... 10) let winloss = DSFSparkline.DataSource(values: [1, 1, 0, -1, 1, 1, 1, 0, 1, -1]) // Simple fill color let baseColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.033, 0.277, 0.650, 1.000])! let primaryStroke = baseColor // (gray: 0.0, alpha: 0.3)) let primaryFill = DSFSparkline.Fill.Color(baseColor.copy(alpha: 0.3)!) do { // A method to replace a value within a DataSource let replaceSource = DSFSparkline.DataSource(values: [0,1,2,3,4,5,6,7]) Swift.print(replaceSource) var currentData = replaceSource.data currentData.replaceSubrange(3...3, with: [33]) replaceSource.set(values: currentData) Swift.print(replaceSource) } // MARK: - Simple line graph do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let line = DSFSparklineOverlay.Line() // Create a line overlay line.strokeWidth = 1 line.primaryStrokeColor = primaryStroke line.primaryFill = primaryFill line.dataSource = source // Assign the datasource to the overlay bitmap.addOverlay(line) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 50, height: 25, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/line-simple-small.png")) // Generate an image with retina scale line.interpolated = true let image2 = bitmap.image(width: 50, height: 25, scale: 2)! SaveImage(image: image2, path: URL(fileURLWithPath: "/tmp/line-simple-small-interpolated.png")) let attr = bitmap.attributedString(size: CGSize(width: 40, height: 18), scale: 2)! var message = NSMutableAttributedString(string: "Inlined ") message.append(attr) message.append(NSAttributedString(string: " line graph")) do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let line = DSFSparklineOverlay.Line() // Create a line overlay line.strokeWidth = 1 line.primaryStrokeColor = primaryStroke line.primaryFill = primaryFill line.markerSize = 3 line.dataSource = source // Assign the datasource to the overlay bitmap.addOverlay(line) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 50, height: 25, scale: 2)! } } // MARK: - Simple bar graph do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let bar = DSFSparklineOverlay.Bar() // Create a bar overlay bar.dataSource = source // Assign the datasource to the overlay bar.primaryStrokeColor = baseColor bar.primaryFill = primaryFill bitmap.addOverlay(bar) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 50, height: 25, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/bar-simple-small.png")) } // MARK: - Simple stackline graph do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let stackline = DSFSparklineOverlay.Stackline() // Create a stackline overlay stackline.dataSource = source // Assign the datasource to the overlay stackline.strokeWidth = 1 stackline.primaryStrokeColor = baseColor stackline.primaryFill = primaryFill bitmap.addOverlay(stackline) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 50, height: 25, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/stackline-simple-small.png")) } // MARK: - Simple dot graph do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let dot = DSFSparklineOverlay.Dot() // Create a dot graph overlay dot.dataSource = biggersource // Assign the datasource to the overlay dot.onColor = primaryStroke bitmap.addOverlay(dot) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 50, height: 32, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/dot-simple-small.png")) } // MARK: - Simple win-loss graph do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let winLoss = DSFSparklineOverlay.WinLossTie() // Create a win-loss graph overlay winLoss.dataSource = winloss // Assign the datasource to the overlay winLoss.centerLine = .init(color: DSFColor.gray, lineWidth: 0.5, lineDashStyle: [0.5, 0.5]) winLoss.winStroke = primaryStroke winLoss.winFill = primaryFill winLoss.lossStroke = primaryStroke winLoss.lossFill = primaryFill bitmap.addOverlay(winLoss) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 75, height: 12, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/win-loss-small.png")) } do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let winlosstie = DSFSparklineOverlay.WinLossTie() // Create a win-loss graph overlay winlosstie.dataSource = winloss // Assign the datasource to the overlay winlosstie.winStroke = primaryStroke winlosstie.winFill = primaryFill winlosstie.lossStroke = primaryStroke winlosstie.lossFill = primaryFill winlosstie.tieStroke = primaryStroke winlosstie.tieFill = primaryFill bitmap.addOverlay(winlosstie) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 75, height: 16, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/win-loss-tie-small.png")) } // MARK: - Simple tablet graph do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let tablet = DSFSparklineOverlay.Tablet() // Create a tablet graph overlay tablet.dataSource = winloss // Assign the datasource to the overlay tablet.lineWidth = 1 tablet.winStrokeColor = primaryStroke tablet.winFill = DSFSparkline.Fill.Color(baseColor.copy(alpha: 0.7)!) tablet.lossStrokeColor = primaryStroke bitmap.addOverlay(tablet) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 90, height: 16, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/tablet-small.png")) } // MARK: - Simple stripes do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let stripe = DSFSparklineOverlay.Stripes() // Create a stripe graph overlay stripe.integral = true stripe.barSpacing = 1 stripe.dataSource = .init(values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) bitmap.addOverlay(stripe) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 90, height: 16, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/stripes-small.png")) } // MARK: - Simple pie do { let bitmap = DSFSparklineSurface.Bitmap() let pieGraph = DSFSparklineOverlay.Pie() pieGraph.dataSource = DSFSparkline.StaticDataSource([10, 55, 20]) pieGraph.lineWidth = 0.5 pieGraph.strokeColor = CGColor.black bitmap.addOverlay(pieGraph) // Generate an image with retina scale let image = bitmap.image(width: 18, height: 18, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/pie-simple.png")) } // MARK: - simple databar do { let bitmap = DSFSparklineSurface.Bitmap() let databar = DSFSparklineOverlay.DataBar() databar.dataSource = DSFSparkline.StaticDataSource([10, 20, 30]) databar.lineWidth = 0.5 databar.strokeColor = CGColor.black bitmap.addOverlay(databar) // Generate an image with retina scale let image = bitmap.image(width: 50, height: 18, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/databar-simple.png")) // databar with a maximum value defined databar.maximumTotalValue = 100 databar.unsetColor = DSFColor.black.cgColor // Generate an image with retina scale let image2 = bitmap.image(width: 50, height: 18, scale: 2)! SaveImage(image: image2, path: URL(fileURLWithPath: "/tmp/databar-simple-maxvalue.png")) } // MARK: - simple percent bar do { let style = DSFSparkline.PercentBar.Style() style.underBarColor = CGColor(gray: 0.8, alpha: 1.0) style.font = DSFFont(name: "Menlo", size: 10)! style.barEdgeInsets = DSFEdgeInsets(top: 1, left: 1, bottom: 1, right: 1) let bitmap = DSFSparklineSurface.Bitmap() let percentbar = DSFSparklineOverlay.PercentBar(style: style, value: 0.3) bitmap.addOverlay(percentbar) // Generate an image with retina scale let image = bitmap.image(width: 50, height: 18, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/percentbar.png")) percentbar.value = 0.7 style.showLabel = false percentbar.displayStyle = style // Generate an image with retina scale let image2 = bitmap.image(width: 50, height: 18, scale: 2)! SaveImage(image: image2, path: URL(fileURLWithPath: "/tmp/percentbar2.png")) } /// MARK: - A more complex sparkline do { // Line DataSource var LineSource1: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, /*range: 0 ... 1,*/ zeroLineValue: 0.3) d.push(values: [ 0.72, 0.84, 0.15, 0.16, 0.30, 0.58, 0.87, 0.44, 0.02, 0.27, 0.48, 0.16, 0.15, 0.14, 0.81, 0.53, 0.67, 0.52, 0.07, 0.50 ]) return d }() var bitmap = DSFSparklineSurface.Bitmap() // highlight overlay 1 do { let h1 = DSFSparklineOverlay.RangeHighlight() h1.dataSource = LineSource1 h1.fill = DSFSparkline.Fill.Color(NSColor.gray.withAlphaComponent(0.2).cgColor) h1.highlightRange = 0.3 ..< 0.7 bitmap.addOverlay(h1) } // highlight overlay 2 do { let h2 = DSFSparklineOverlay.RangeHighlight() h2.dataSource = LineSource1 h2.fill = DSFSparkline.Fill.Color(NSColor.systemRed.withAlphaComponent(0.1).cgColor) h2.highlightRange = 0.0 ..< 0.3 bitmap.addOverlay(h2) } // zero-line do { let zeroLine = DSFSparklineOverlay.ZeroLine() zeroLine.dataSource = LineSource1 bitmap.addOverlay(zeroLine) } // Stack overlay do { let stack = DSFSparklineOverlay.Stackline() stack.dataSource = LineSource1 stack.shadow = NSShadow(blurRadius: 1.0, offset: CGSize(width: 0.5, height: -0.5), color: DSFColor.black.withAlphaComponent(0.3)) stack.centeredAtZeroLine = true stack.strokeWidth = 1 stack.primaryStrokeColor = NSColor.systemPurple.cgColor stack.primaryFill = DSFSparkline.Fill.Color(NSColor.systemPurple.withAlphaComponent(0.7).cgColor) stack.secondaryStrokeColor = NSColor.systemPink.cgColor stack.secondaryFill = DSFSparkline.Fill.Color(NSColor.systemPink.withAlphaComponent(0.7).cgColor) bitmap.addOverlay(stack) } // Generate a bitmap let r = CGSize(width: 200, height: 40) let image = bitmap.image(size: r, scale: 2) } // MARK: - Simple wiper graph do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let wiper = DSFSparklineOverlay.WiperGauge() // Create a wiper graph overlay wiper.value = 0.75 wiper.valueColor = DSFSparkline.ValueBasedFill(flatColor: baseColor) bitmap.addOverlay(wiper) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 40, height: 20, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/wiper-small.png")) } // MARK: - Activity Graph do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let activity = DSFSparklineOverlay.ActivityGrid() let data: [CGFloat] = (0 ... 100).map { _ in CGFloat.random(in: 0...100) } activity.dataSource = .init(data) bitmap.addOverlay(activity) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 300, height: 100, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/activity-basic-1.png")) activity.verticalCellCount = 10 activity.cellDimension = 6 activity.cellSpacing = 1 activity.cellFillScheme = DSFSparkline.ActivityGrid.CellStyle.DefaultLight let image2 = bitmap.image(width: 300, height: 100, scale: 2)! SaveImage(image: image2, path: URL(fileURLWithPath: "/tmp/activity-basic-2.png")) } // MARK: Circular Gauge do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let wiper = DSFSparklineOverlay.CircularGauge() // Create a wiper graph overlay wiper.value = 0.65 //wiper.valueColor = DSFSparkline.ValueBasedFill(flatColor: baseColor) bitmap.addOverlay(wiper) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 40, height: 40, scale: 2)! SaveImage(image: image, path: URL(fileURLWithPath: "/tmp/circular-gauge-small.png")) } ================================================ FILE: Demos/Playground/Sparklines Playground.playground/contents.xcplayground ================================================ ================================================ FILE: Demos/Playground/Sparklines Playground.playground/playground.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Demos/Playground/Sparklines Playground.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Demos/Samples/Demos.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 60; objects = { /* Begin PBXAggregateTarget section */ 239716B923B554DD0034A4D4 /* All Demos */ = { isa = PBXAggregateTarget; buildConfigurationList = 239716BC23B554DD0034A4D4 /* Build configuration list for PBXAggregateTarget "All Demos" */; buildPhases = ( ); dependencies = ( 23369E8525D65B2D00A40023 /* PBXTargetDependency */, 23369E8725D65B2D00A40023 /* PBXTargetDependency */, 239716C023B554E70034A4D4 /* PBXTargetDependency */, 239716BE23B554E50034A4D4 /* PBXTargetDependency */, 239716C223B554E80034A4D4 /* PBXTargetDependency */, 23E2279A23B5A6EF00E59122 /* PBXTargetDependency */, ); name = "All Demos"; productName = "Add Demos"; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 23102B5423B0245A001A621D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23102B5323B0245A001A621D /* AppDelegate.swift */; }; 23102B5623B0245A001A621D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23102B5523B0245A001A621D /* ViewController.swift */; }; 23102B5823B0245B001A621D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 23102B5723B0245B001A621D /* Assets.xcassets */; }; 23102B5B23B0245B001A621D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 23102B5923B0245B001A621D /* Main.storyboard */; }; 23102BC423B02AED001A621D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23102BC323B02AED001A621D /* AppDelegate.swift */; }; 23102BC623B02AED001A621D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23102BC523B02AED001A621D /* SceneDelegate.swift */; }; 23102BC823B02AED001A621D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23102BC723B02AED001A621D /* ViewController.swift */; }; 23102BCB23B02AED001A621D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 23102BC923B02AED001A621D /* Main.storyboard */; }; 23102BCD23B02AEE001A621D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 23102BCC23B02AEE001A621D /* Assets.xcassets */; }; 23102BD023B02AEE001A621D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 23102BCE23B02AEE001A621D /* LaunchScreen.storyboard */; }; 23102BDD23B02B0A001A621D /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 23102BDC23B02B0A001A621D /* AppDelegate.m */; }; 23102BE023B02B0A001A621D /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 23102BDF23B02B0A001A621D /* ViewController.m */; }; 23102BE223B02B0B001A621D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 23102BE123B02B0B001A621D /* Assets.xcassets */; }; 23102BE523B02B0B001A621D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 23102BE323B02B0B001A621D /* Main.storyboard */; }; 23102BE823B02B0B001A621D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 23102BE723B02B0B001A621D /* main.m */; }; 23102BF423B02B32001A621D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23102BF323B02B32001A621D /* AppDelegate.swift */; }; 23102BF623B02B32001A621D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23102BF523B02B32001A621D /* ViewController.swift */; }; 23102BF923B02B32001A621D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 23102BF723B02B32001A621D /* Main.storyboard */; }; 23102BFB23B02B33001A621D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 23102BFA23B02B33001A621D /* Assets.xcassets */; }; 23102BFE23B02B33001A621D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 23102BFC23B02B33001A621D /* LaunchScreen.storyboard */; }; 23102C0423B03315001A621D /* cpuUsage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23102C0323B03315001A621D /* cpuUsage.swift */; }; 2322C06B262651D5007D8112 /* TestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2322C06A262651D5007D8112 /* TestingView.swift */; }; 2322C06C262651D5007D8112 /* TestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2322C06A262651D5007D8112 /* TestingView.swift */; }; 232BAF6D25EB16210090C447 /* SwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232BAF6C25EB16210090C447 /* SwiftUIView.swift */; }; 232BAF6E25EB16210090C447 /* SwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232BAF6C25EB16210090C447 /* SwiftUIView.swift */; }; 232D9ECE25EC7133004DFAFC /* SuperCoolLineSpark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232D9ECD25EC7133004DFAFC /* SuperCoolLineSpark.swift */; }; 232D9ECF25EC7133004DFAFC /* SuperCoolLineSpark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232D9ECD25EC7133004DFAFC /* SuperCoolLineSpark.swift */; }; 232F732323B5BE2A00FA3A3F /* GridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 232F732223B5BE2A00FA3A3F /* GridViewController.swift */; }; 2346EF3025E8D13A00050570 /* SwiftUIContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2346EF2F25E8D13A00050570 /* SwiftUIContentView.swift */; }; 2346EF3125E8D13A00050570 /* SwiftUIContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2346EF2F25E8D13A00050570 /* SwiftUIContentView.swift */; }; 23529A7425E84D2A00DFA52D /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23529A7325E84D2A00DFA52D /* Extensions.swift */; }; 23529A7525E84D2A00DFA52D /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23529A7325E84D2A00DFA52D /* Extensions.swift */; }; 235A585B2603181A00266642 /* PercentBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 235A585A2603181A00266642 /* PercentBarView.swift */; }; 235A585C2603181A00266642 /* PercentBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 235A585A2603181A00266642 /* PercentBarView.swift */; }; 236417E729628C0600C8A1D2 /* WiperGaugeDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236417E629628C0600C8A1D2 /* WiperGaugeDemoView.swift */; }; 236417E829628C0600C8A1D2 /* WiperGaugeDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236417E629628C0600C8A1D2 /* WiperGaugeDemoView.swift */; }; 236A57FB25C1392B00F2BACF /* SwiftUI_Sparkline_DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236A57E925C1392A00F2BACF /* SwiftUI_Sparkline_DemoApp.swift */; }; 236A57FC25C1392B00F2BACF /* SwiftUI_Sparkline_DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236A57E925C1392A00F2BACF /* SwiftUI_Sparkline_DemoApp.swift */; }; 236A57FD25C1392B00F2BACF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236A57EA25C1392A00F2BACF /* ContentView.swift */; }; 236A57FE25C1392B00F2BACF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236A57EA25C1392A00F2BACF /* ContentView.swift */; }; 236A57FF25C1392B00F2BACF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 236A57EB25C1392B00F2BACF /* Assets.xcassets */; }; 236A580025C1392B00F2BACF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 236A57EB25C1392B00F2BACF /* Assets.xcassets */; }; 237B824725E77B3800B3DE23 /* BarDemoContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237B824625E77B3800B3DE23 /* BarDemoContentView.swift */; }; 237B824825E77B3800B3DE23 /* BarDemoContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237B824625E77B3800B3DE23 /* BarDemoContentView.swift */; }; 237B827825E7891E00B3DE23 /* BitmapGenerationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237B827725E7891D00B3DE23 /* BitmapGenerationView.swift */; }; 237B827925E7891E00B3DE23 /* BitmapGenerationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 237B827725E7891D00B3DE23 /* BitmapGenerationView.swift */; }; 238053C725DCC2B100B996B5 /* ActiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 238053C625DCC2B100B996B5 /* ActiveView.swift */; }; 238053C825DCC2B100B996B5 /* ActiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 238053C625DCC2B100B996B5 /* ActiveView.swift */; }; 238053E525DCD14600B996B5 /* OverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 238053E425DCD14600B996B5 /* OverlayView.swift */; }; 238053E625DCD14600B996B5 /* OverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 238053E425DCD14600B996B5 /* OverlayView.swift */; }; 238DB6DC25EB2141009197AB /* StripesOverlaidView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 238DB6DB25EB2141009197AB /* StripesOverlaidView.swift */; }; 238DB6DD25EB2141009197AB /* StripesOverlaidView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 238DB6DB25EB2141009197AB /* StripesOverlaidView.swift */; }; 238DB6EA25EB3A2E009197AB /* TabletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 238DB6E925EB3A2E009197AB /* TabletView.swift */; }; 238DB6EB25EB3A2F009197AB /* TabletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 238DB6E925EB3A2E009197AB /* TabletView.swift */; }; 23905CA12B5B5D00007B7769 /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23905CA02B5B5D00007B7769 /* CircularProgress.swift */; }; 23905CA22B5B5D00007B7769 /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23905CA02B5B5D00007B7769 /* CircularProgress.swift */; }; 239DE4042BA177D300E13208 /* CircularGaugeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 239DE4032BA177D300E13208 /* CircularGaugeView.swift */; }; 239DE4052BA177D300E13208 /* CircularGaugeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 239DE4032BA177D300E13208 /* CircularGaugeView.swift */; }; 23B006F72DDD61C700D074A0 /* DSFSparkline in Frameworks */ = {isa = PBXBuildFile; productRef = 23B006F62DDD61C700D074A0 /* DSFSparkline */; }; 23B006F92DDD61FB00D074A0 /* DSFSparkline-shared in Frameworks */ = {isa = PBXBuildFile; productRef = 23B006F82DDD61FB00D074A0 /* DSFSparkline-shared */; }; 23B006FA2DDD61FB00D074A0 /* DSFSparkline-shared in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 23B006F82DDD61FB00D074A0 /* DSFSparkline-shared */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 23B006FD2DDD625E00D074A0 /* DSFSparkline in Frameworks */ = {isa = PBXBuildFile; productRef = 23B006FC2DDD625E00D074A0 /* DSFSparkline */; }; 23B006FF2DDD63E900D074A0 /* DSFSparkline-shared in Frameworks */ = {isa = PBXBuildFile; productRef = 23B006FE2DDD63E900D074A0 /* DSFSparkline-shared */; }; 23B007002DDD63E900D074A0 /* DSFSparkline-shared in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 23B006FE2DDD63E900D074A0 /* DSFSparkline-shared */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 23B007032DDD652800D074A0 /* DSFSparkline in Frameworks */ = {isa = PBXBuildFile; productRef = 23B007022DDD652800D074A0 /* DSFSparkline */; }; 23B007052DDD65A800D074A0 /* DSFSparkline in Frameworks */ = {isa = PBXBuildFile; productRef = 23B007042DDD65A800D074A0 /* DSFSparkline */; }; 23B007072DDD65B800D074A0 /* DSFSparkline-shared in Frameworks */ = {isa = PBXBuildFile; productRef = 23B007062DDD65B800D074A0 /* DSFSparkline-shared */; }; 23B007082DDD65B800D074A0 /* DSFSparkline-shared in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 23B007062DDD65B800D074A0 /* DSFSparkline-shared */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 23BC66862B0DD82A0064FF62 /* ActivityGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23BC66852B0DD82A0064FF62 /* ActivityGridView.swift */; }; 23BC66872B0DD82A0064FF62 /* ActivityGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23BC66852B0DD82A0064FF62 /* ActivityGridView.swift */; }; 23D10B1025ED9EB500BBE555 /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D10B0F25ED9EB500BBE555 /* AttributedString.swift */; }; 23D10B1125ED9EB500BBE555 /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D10B0F25ED9EB500BBE555 /* AttributedString.swift */; }; 23E2278C23B5846E00E59122 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23E2278B23B5846E00E59122 /* AppDelegate.swift */; }; 23E2278E23B5846E00E59122 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23E2278D23B5846E00E59122 /* ViewController.swift */; }; 23E2279023B5846E00E59122 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 23E2278F23B5846E00E59122 /* Assets.xcassets */; }; 23E2279323B5846E00E59122 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 23E2279123B5846E00E59122 /* Main.storyboard */; }; 23FC810025DB3FC50047A800 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC80FF25DB3FC40047A800 /* ReportView.swift */; }; 23FC810125DB3FC50047A800 /* ReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC80FF25DB3FC40047A800 /* ReportView.swift */; }; 23FC812025DB47A20047A800 /* WinLossGraphContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC811F25DB47A20047A800 /* WinLossGraphContentView.swift */; }; 23FC812125DB47A20047A800 /* WinLossGraphContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC811F25DB47A20047A800 /* WinLossGraphContentView.swift */; }; 23FC814025DB4E5D0047A800 /* LineDemoContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC813F25DB4E5D0047A800 /* LineDemoContentView.swift */; }; 23FC814125DB4E5D0047A800 /* LineDemoContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC813F25DB4E5D0047A800 /* LineDemoContentView.swift */; }; 23FC817525DB547E0047A800 /* StackLineDemoContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC817425DB547E0047A800 /* StackLineDemoContentView.swift */; }; 23FC817625DB547E0047A800 /* StackLineDemoContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC817425DB547E0047A800 /* StackLineDemoContentView.swift */; }; 23FC819325DB586B0047A800 /* PieGraphDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC819225DB586B0047A800 /* PieGraphDemoView.swift */; }; 23FC819425DB586B0047A800 /* PieGraphDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC819225DB586B0047A800 /* PieGraphDemoView.swift */; }; 23FC819F25DB59160047A800 /* DataBarGraphContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC819E25DB59160047A800 /* DataBarGraphContent.swift */; }; 23FC81A025DB59160047A800 /* DataBarGraphContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC819E25DB59160047A800 /* DataBarGraphContent.swift */; }; 23FC81B425DB59E70047A800 /* DotGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC81B325DB59E70047A800 /* DotGraphView.swift */; }; 23FC81B525DB59E70047A800 /* DotGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC81B325DB59E70047A800 /* DotGraphView.swift */; }; 23FC81DD25DB60760047A800 /* StripesDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC81DC25DB60760047A800 /* StripesDemoView.swift */; }; 23FC81DE25DB60760047A800 /* StripesDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FC81DC25DB60760047A800 /* StripesDemoView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 23369E8425D65B2D00A40023 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 23102B4523B0165B001A621D /* Project object */; proxyType = 1; remoteGlobalIDString = 236A57EF25C1392B00F2BACF; remoteInfo = "SwiftUI Sparkline Demo (iOS)"; }; 23369E8625D65B2D00A40023 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 23102B4523B0165B001A621D /* Project object */; proxyType = 1; remoteGlobalIDString = 236A57F625C1392B00F2BACF; remoteInfo = "SwiftUI Sparkline Demo (macOS)"; }; 239716BD23B554E50034A4D4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 23102B4523B0165B001A621D /* Project object */; proxyType = 1; remoteGlobalIDString = 23102B4F23B0245A001A621D; remoteInfo = "macOS Sparkline Demo"; }; 239716BF23B554E70034A4D4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 23102B4523B0165B001A621D /* Project object */; proxyType = 1; remoteGlobalIDString = 23102BC023B02AED001A621D; remoteInfo = "iOS Sparkline Demo"; }; 239716C123B554E80034A4D4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 23102B4523B0165B001A621D /* Project object */; proxyType = 1; remoteGlobalIDString = 23102BD823B02B0A001A621D; remoteInfo = "macOS Sparkline Demo Objc"; }; 23E2279923B5A6EF00E59122 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 23102B4523B0165B001A621D /* Project object */; proxyType = 1; remoteGlobalIDString = 23E2278823B5846E00E59122; remoteInfo = "macOS Table Demo"; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 23B006FB2DDD61FB00D074A0 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 23B006FA2DDD61FB00D074A0 /* DSFSparkline-shared in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; 23B007012DDD63E900D074A0 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 23B007002DDD63E900D074A0 /* DSFSparkline-shared in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; 23B007092DDD65B800D074A0 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 23B007082DDD65B800D074A0 /* DSFSparkline-shared in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 23102B5023B0245A001A621D /* macOS Sparkline Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "macOS Sparkline Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 23102B5323B0245A001A621D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23102B5523B0245A001A621D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23102B5723B0245B001A621D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23102B5A23B0245B001A621D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23102B5C23B0245B001A621D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23102B5D23B0245B001A621D /* macOS_Sparkline_Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS_Sparkline_Demo.entitlements; sourceTree = ""; }; 23102BC123B02AED001A621D /* iOS Sparkline Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS Sparkline Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 23102BC323B02AED001A621D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23102BC523B02AED001A621D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 23102BC723B02AED001A621D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23102BCA23B02AED001A621D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23102BCC23B02AEE001A621D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23102BCF23B02AEE001A621D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 23102BD123B02AEE001A621D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23102BD923B02B0A001A621D /* macOS Sparkline Demo Objc.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "macOS Sparkline Demo Objc.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 23102BDB23B02B0A001A621D /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 23102BDC23B02B0A001A621D /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 23102BDE23B02B0A001A621D /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 23102BDF23B02B0A001A621D /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 23102BE123B02B0B001A621D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23102BE423B02B0B001A621D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23102BE623B02B0B001A621D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23102BE723B02B0B001A621D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 23102BE923B02B0B001A621D /* macOS_Sparkline_Demo_Objc.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS_Sparkline_Demo_Objc.entitlements; sourceTree = ""; }; 23102BF123B02B32001A621D /* tvOS Sparkline Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "tvOS Sparkline Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 23102BF323B02B32001A621D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23102BF523B02B32001A621D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23102BF823B02B32001A621D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23102BFA23B02B33001A621D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23102BFD23B02B33001A621D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 23102BFF23B02B33001A621D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23102C0323B03315001A621D /* cpuUsage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = cpuUsage.swift; sourceTree = ""; }; 2322C06A262651D5007D8112 /* TestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingView.swift; sourceTree = ""; }; 232BAF6C25EB16210090C447 /* SwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIView.swift; sourceTree = ""; }; 232D9ECD25EC7133004DFAFC /* SuperCoolLineSpark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuperCoolLineSpark.swift; sourceTree = ""; }; 232F732223B5BE2A00FA3A3F /* GridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridViewController.swift; sourceTree = ""; }; 2346EF2F25E8D13A00050570 /* SwiftUIContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIContentView.swift; sourceTree = ""; }; 23529A5825E847B900DFA52D /* iOS Sparkline Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "iOS Sparkline Demo.entitlements"; sourceTree = ""; }; 23529A7325E84D2A00DFA52D /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 235A585A2603181A00266642 /* PercentBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PercentBarView.swift; sourceTree = ""; }; 236417E629628C0600C8A1D2 /* WiperGaugeDemoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WiperGaugeDemoView.swift; sourceTree = ""; }; 236A57E925C1392A00F2BACF /* SwiftUI_Sparkline_DemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUI_Sparkline_DemoApp.swift; sourceTree = ""; }; 236A57EA25C1392A00F2BACF /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 236A57EB25C1392B00F2BACF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 236A57F025C1392B00F2BACF /* SwiftUI Sparkline Demo (iOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftUI Sparkline Demo (iOS).app"; sourceTree = BUILT_PRODUCTS_DIR; }; 236A57F225C1392B00F2BACF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 236A57F725C1392B00F2BACF /* SwiftUI Sparkline Demo (macOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftUI Sparkline Demo (macOS).app"; sourceTree = BUILT_PRODUCTS_DIR; }; 236A57F925C1392B00F2BACF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 236A57FA25C1392B00F2BACF /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; 237B824625E77B3800B3DE23 /* BarDemoContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarDemoContentView.swift; sourceTree = ""; }; 237B827725E7891D00B3DE23 /* BitmapGenerationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitmapGenerationView.swift; sourceTree = ""; }; 238053C625DCC2B100B996B5 /* ActiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveView.swift; sourceTree = ""; }; 238053E425DCD14600B996B5 /* OverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayView.swift; sourceTree = ""; }; 238DB6DB25EB2141009197AB /* StripesOverlaidView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripesOverlaidView.swift; sourceTree = ""; }; 238DB6E925EB3A2E009197AB /* TabletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabletView.swift; sourceTree = ""; }; 23905CA02B5B5D00007B7769 /* CircularProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgress.swift; sourceTree = ""; }; 239DE4032BA177D300E13208 /* CircularGaugeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularGaugeView.swift; sourceTree = ""; }; 23BC66852B0DD82A0064FF62 /* ActivityGridView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityGridView.swift; sourceTree = ""; }; 23D10B0F25ED9EB500BBE555 /* AttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedString.swift; sourceTree = ""; }; 23E2278923B5846E00E59122 /* macOS Table Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "macOS Table Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 23E2278B23B5846E00E59122 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23E2278D23B5846E00E59122 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 23E2278F23B5846E00E59122 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23E2279223B5846E00E59122 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 23E2279423B5846E00E59122 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 23E2279523B5846E00E59122 /* macOS_Table_Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS_Table_Demo.entitlements; sourceTree = ""; }; 23FC80FF25DB3FC40047A800 /* ReportView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportView.swift; sourceTree = ""; }; 23FC811F25DB47A20047A800 /* WinLossGraphContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WinLossGraphContentView.swift; sourceTree = ""; }; 23FC813F25DB4E5D0047A800 /* LineDemoContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LineDemoContentView.swift; sourceTree = ""; }; 23FC817425DB547E0047A800 /* StackLineDemoContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackLineDemoContentView.swift; sourceTree = ""; }; 23FC819225DB586B0047A800 /* PieGraphDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PieGraphDemoView.swift; sourceTree = ""; }; 23FC819E25DB59160047A800 /* DataBarGraphContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBarGraphContent.swift; sourceTree = ""; }; 23FC81B325DB59E70047A800 /* DotGraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DotGraphView.swift; sourceTree = ""; }; 23FC81DC25DB60760047A800 /* StripesDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripesDemoView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 23102B4D23B0245A001A621D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 23B006F72DDD61C700D074A0 /* DSFSparkline in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 23102BBE23B02AED001A621D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 23B006FF2DDD63E900D074A0 /* DSFSparkline-shared in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 23102BD623B02B0A001A621D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 23B006F92DDD61FB00D074A0 /* DSFSparkline-shared in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 23102BEE23B02B32001A621D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 23B007032DDD652800D074A0 /* DSFSparkline in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 236A57ED25C1392B00F2BACF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 23B007072DDD65B800D074A0 /* DSFSparkline-shared in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 236A57F425C1392B00F2BACF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 23B007052DDD65A800D074A0 /* DSFSparkline in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 23E2278623B5846E00E59122 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 23B006FD2DDD625E00D074A0 /* DSFSparkline in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 23102B4423B0165B001A621D = { isa = PBXGroup; children = ( 23E2278A23B5846E00E59122 /* macOS Table Demo */, 23102B5223B0245A001A621D /* macOS Sparkline Demo */, 23102BDA23B02B0A001A621D /* macOS Sparkline Demo Objc */, 23102BC223B02AED001A621D /* iOS Sparkline Demo */, 23102BF223B02B32001A621D /* tvOS Sparkline Demo */, 238423FD25C74EE400A2A861 /* SwiftUI Sparkline Crossplatform */, 23B006F52DDD61C700D074A0 /* Frameworks */, 23102B5123B0245A001A621D /* Products */, ); sourceTree = ""; }; 23102B5123B0245A001A621D /* Products */ = { isa = PBXGroup; children = ( 23102B5023B0245A001A621D /* macOS Sparkline Demo.app */, 23102BC123B02AED001A621D /* iOS Sparkline Demo.app */, 23102BD923B02B0A001A621D /* macOS Sparkline Demo Objc.app */, 23102BF123B02B32001A621D /* tvOS Sparkline Demo.app */, 23E2278923B5846E00E59122 /* macOS Table Demo.app */, 236A57F025C1392B00F2BACF /* SwiftUI Sparkline Demo (iOS).app */, 236A57F725C1392B00F2BACF /* SwiftUI Sparkline Demo (macOS).app */, ); name = Products; sourceTree = ""; }; 23102B5223B0245A001A621D /* macOS Sparkline Demo */ = { isa = PBXGroup; children = ( 23102B5323B0245A001A621D /* AppDelegate.swift */, 23102B5523B0245A001A621D /* ViewController.swift */, 23102C0323B03315001A621D /* cpuUsage.swift */, 23102B5723B0245B001A621D /* Assets.xcassets */, 23102B5923B0245B001A621D /* Main.storyboard */, 23102B5C23B0245B001A621D /* Info.plist */, 23102B5D23B0245B001A621D /* macOS_Sparkline_Demo.entitlements */, ); path = "macOS Sparkline Demo"; sourceTree = ""; }; 23102BC223B02AED001A621D /* iOS Sparkline Demo */ = { isa = PBXGroup; children = ( 23529A5825E847B900DFA52D /* iOS Sparkline Demo.entitlements */, 23102BC323B02AED001A621D /* AppDelegate.swift */, 23102BC523B02AED001A621D /* SceneDelegate.swift */, 23102BC723B02AED001A621D /* ViewController.swift */, 23102BC923B02AED001A621D /* Main.storyboard */, 23102BCC23B02AEE001A621D /* Assets.xcassets */, 23102BCE23B02AEE001A621D /* LaunchScreen.storyboard */, 23102BD123B02AEE001A621D /* Info.plist */, ); path = "iOS Sparkline Demo"; sourceTree = ""; }; 23102BDA23B02B0A001A621D /* macOS Sparkline Demo Objc */ = { isa = PBXGroup; children = ( 23102BDB23B02B0A001A621D /* AppDelegate.h */, 23102BDC23B02B0A001A621D /* AppDelegate.m */, 23102BDE23B02B0A001A621D /* ViewController.h */, 23102BDF23B02B0A001A621D /* ViewController.m */, 23102BE123B02B0B001A621D /* Assets.xcassets */, 23102BE323B02B0B001A621D /* Main.storyboard */, 23102BE623B02B0B001A621D /* Info.plist */, 23102BE723B02B0B001A621D /* main.m */, 23102BE923B02B0B001A621D /* macOS_Sparkline_Demo_Objc.entitlements */, ); path = "macOS Sparkline Demo Objc"; sourceTree = ""; }; 23102BF223B02B32001A621D /* tvOS Sparkline Demo */ = { isa = PBXGroup; children = ( 23102BF323B02B32001A621D /* AppDelegate.swift */, 23102BF523B02B32001A621D /* ViewController.swift */, 23102BF723B02B32001A621D /* Main.storyboard */, 23102BFA23B02B33001A621D /* Assets.xcassets */, 23102BFC23B02B33001A621D /* LaunchScreen.storyboard */, 23102BFF23B02B33001A621D /* Info.plist */, ); path = "tvOS Sparkline Demo"; sourceTree = ""; }; 232BAF6B25EB16000090C447 /* SwiftUI-Overlays */ = { isa = PBXGroup; children = ( 2346EF2F25E8D13A00050570 /* SwiftUIContentView.swift */, 238053E425DCD14600B996B5 /* OverlayView.swift */, 232BAF6C25EB16210090C447 /* SwiftUIView.swift */, 238DB6DB25EB2141009197AB /* StripesOverlaidView.swift */, 232D9ECD25EC7133004DFAFC /* SuperCoolLineSpark.swift */, ); path = "SwiftUI-Overlays"; sourceTree = ""; }; 236A57E825C1392A00F2BACF /* Shared */ = { isa = PBXGroup; children = ( 238053C625DCC2B100B996B5 /* ActiveView.swift */, 23BC66852B0DD82A0064FF62 /* ActivityGridView.swift */, 236A57EB25C1392B00F2BACF /* Assets.xcassets */, 23D10B0F25ED9EB500BBE555 /* AttributedString.swift */, 239DE4032BA177D300E13208 /* CircularGaugeView.swift */, 23905CA02B5B5D00007B7769 /* CircularProgress.swift */, 237B824625E77B3800B3DE23 /* BarDemoContentView.swift */, 237B827725E7891D00B3DE23 /* BitmapGenerationView.swift */, 236A57EA25C1392A00F2BACF /* ContentView.swift */, 23FC819E25DB59160047A800 /* DataBarGraphContent.swift */, 23FC81B325DB59E70047A800 /* DotGraphView.swift */, 23529A7325E84D2A00DFA52D /* Extensions.swift */, 23FC813F25DB4E5D0047A800 /* LineDemoContentView.swift */, 235A585A2603181A00266642 /* PercentBarView.swift */, 23FC819225DB586B0047A800 /* PieGraphDemoView.swift */, 23FC80FF25DB3FC40047A800 /* ReportView.swift */, 23FC817425DB547E0047A800 /* StackLineDemoContentView.swift */, 23FC81DC25DB60760047A800 /* StripesDemoView.swift */, 236A57E925C1392A00F2BACF /* SwiftUI_Sparkline_DemoApp.swift */, 232BAF6B25EB16000090C447 /* SwiftUI-Overlays */, 238DB6E925EB3A2E009197AB /* TabletView.swift */, 23FC811F25DB47A20047A800 /* WinLossGraphContentView.swift */, 236417E629628C0600C8A1D2 /* WiperGaugeDemoView.swift */, 2322C06A262651D5007D8112 /* TestingView.swift */, ); path = Shared; sourceTree = ""; }; 236A57F125C1392B00F2BACF /* iOS */ = { isa = PBXGroup; children = ( 236A57F225C1392B00F2BACF /* Info.plist */, ); path = iOS; sourceTree = ""; }; 236A57F825C1392B00F2BACF /* macOS */ = { isa = PBXGroup; children = ( 236A57F925C1392B00F2BACF /* Info.plist */, 236A57FA25C1392B00F2BACF /* macOS.entitlements */, ); path = macOS; sourceTree = ""; }; 238423FD25C74EE400A2A861 /* SwiftUI Sparkline Crossplatform */ = { isa = PBXGroup; children = ( 236A57E825C1392A00F2BACF /* Shared */, 236A57F125C1392B00F2BACF /* iOS */, 236A57F825C1392B00F2BACF /* macOS */, ); path = "SwiftUI Sparkline Crossplatform"; sourceTree = ""; }; 23B006F52DDD61C700D074A0 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; 23E2278A23B5846E00E59122 /* macOS Table Demo */ = { isa = PBXGroup; children = ( 23E2278B23B5846E00E59122 /* AppDelegate.swift */, 23E2278D23B5846E00E59122 /* ViewController.swift */, 23E2278F23B5846E00E59122 /* Assets.xcassets */, 23E2279123B5846E00E59122 /* Main.storyboard */, 23E2279423B5846E00E59122 /* Info.plist */, 23E2279523B5846E00E59122 /* macOS_Table_Demo.entitlements */, 232F732223B5BE2A00FA3A3F /* GridViewController.swift */, ); path = "macOS Table Demo"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 23102B4F23B0245A001A621D /* macOS Sparkline Demo */ = { isa = PBXNativeTarget; buildConfigurationList = 23102B5E23B0245B001A621D /* Build configuration list for PBXNativeTarget "macOS Sparkline Demo" */; buildPhases = ( 23102B4C23B0245A001A621D /* Sources */, 23102B4D23B0245A001A621D /* Frameworks */, 23102B4E23B0245A001A621D /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "macOS Sparkline Demo"; productName = "macOS Sparkline Demo"; productReference = 23102B5023B0245A001A621D /* macOS Sparkline Demo.app */; productType = "com.apple.product-type.application"; }; 23102BC023B02AED001A621D /* iOS Sparkline Demo */ = { isa = PBXNativeTarget; buildConfigurationList = 23102BD423B02AEE001A621D /* Build configuration list for PBXNativeTarget "iOS Sparkline Demo" */; buildPhases = ( 23102BBD23B02AED001A621D /* Sources */, 23102BBE23B02AED001A621D /* Frameworks */, 23102BBF23B02AED001A621D /* Resources */, 23B007012DDD63E900D074A0 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "iOS Sparkline Demo"; productName = "iOS Sparkline Demo"; productReference = 23102BC123B02AED001A621D /* iOS Sparkline Demo.app */; productType = "com.apple.product-type.application"; }; 23102BD823B02B0A001A621D /* macOS Sparkline Demo Objc */ = { isa = PBXNativeTarget; buildConfigurationList = 23102BEA23B02B0B001A621D /* Build configuration list for PBXNativeTarget "macOS Sparkline Demo Objc" */; buildPhases = ( 23102BD523B02B0A001A621D /* Sources */, 23102BD623B02B0A001A621D /* Frameworks */, 23102BD723B02B0A001A621D /* Resources */, 23B006FB2DDD61FB00D074A0 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "macOS Sparkline Demo Objc"; productName = "macOS Sparkline Demo Objc"; productReference = 23102BD923B02B0A001A621D /* macOS Sparkline Demo Objc.app */; productType = "com.apple.product-type.application"; }; 23102BF023B02B32001A621D /* tvOS Sparkline Demo */ = { isa = PBXNativeTarget; buildConfigurationList = 23102C0023B02B33001A621D /* Build configuration list for PBXNativeTarget "tvOS Sparkline Demo" */; buildPhases = ( 23102BED23B02B32001A621D /* Sources */, 23102BEE23B02B32001A621D /* Frameworks */, 23102BEF23B02B32001A621D /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "tvOS Sparkline Demo"; productName = "tvOS Sparkline Demo"; productReference = 23102BF123B02B32001A621D /* tvOS Sparkline Demo.app */; productType = "com.apple.product-type.application"; }; 236A57EF25C1392B00F2BACF /* SwiftUI Sparkline Demo (iOS) */ = { isa = PBXNativeTarget; buildConfigurationList = 236A580525C1392B00F2BACF /* Build configuration list for PBXNativeTarget "SwiftUI Sparkline Demo (iOS)" */; buildPhases = ( 236A57EC25C1392B00F2BACF /* Sources */, 236A57ED25C1392B00F2BACF /* Frameworks */, 236A57EE25C1392B00F2BACF /* Resources */, 23B007092DDD65B800D074A0 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "SwiftUI Sparkline Demo (iOS)"; productName = "SwiftUI Sparkline Demo (iOS)"; productReference = 236A57F025C1392B00F2BACF /* SwiftUI Sparkline Demo (iOS).app */; productType = "com.apple.product-type.application"; }; 236A57F625C1392B00F2BACF /* SwiftUI Sparkline Demo (macOS) */ = { isa = PBXNativeTarget; buildConfigurationList = 236A580625C1392B00F2BACF /* Build configuration list for PBXNativeTarget "SwiftUI Sparkline Demo (macOS)" */; buildPhases = ( 236A57F325C1392B00F2BACF /* Sources */, 236A57F425C1392B00F2BACF /* Frameworks */, 236A57F525C1392B00F2BACF /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "SwiftUI Sparkline Demo (macOS)"; productName = "SwiftUI Sparkline Demo (macOS)"; productReference = 236A57F725C1392B00F2BACF /* SwiftUI Sparkline Demo (macOS).app */; productType = "com.apple.product-type.application"; }; 23E2278823B5846E00E59122 /* macOS Table Demo */ = { isa = PBXNativeTarget; buildConfigurationList = 23E2279823B5846E00E59122 /* Build configuration list for PBXNativeTarget "macOS Table Demo" */; buildPhases = ( 23E2278523B5846E00E59122 /* Sources */, 23E2278623B5846E00E59122 /* Frameworks */, 23E2278723B5846E00E59122 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "macOS Table Demo"; productName = "macOS Table Demo"; productReference = 23E2278923B5846E00E59122 /* macOS Table Demo.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 23102B4523B0165B001A621D /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1240; LastUpgradeCheck = 1130; TargetAttributes = { 23102B4F23B0245A001A621D = { CreatedOnToolsVersion = 11.3; }; 23102BC023B02AED001A621D = { CreatedOnToolsVersion = 11.3; }; 23102BD823B02B0A001A621D = { CreatedOnToolsVersion = 11.3; }; 23102BF023B02B32001A621D = { CreatedOnToolsVersion = 11.3; }; 236A57EF25C1392B00F2BACF = { CreatedOnToolsVersion = 12.4; }; 236A57F625C1392B00F2BACF = { CreatedOnToolsVersion = 12.4; }; 239716B923B554DD0034A4D4 = { CreatedOnToolsVersion = 11.3; }; 23E2278823B5846E00E59122 = { CreatedOnToolsVersion = 11.3; }; }; }; buildConfigurationList = 23102B4823B0165B001A621D /* Build configuration list for PBXProject "Demos" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 23102B4423B0165B001A621D; packageReferences = ( 23B006F42DDD61A400D074A0 /* XCLocalSwiftPackageReference "../../../DSFSparkline" */, ); productRefGroup = 23102B5123B0245A001A621D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 239716B923B554DD0034A4D4 /* All Demos */, 23102B4F23B0245A001A621D /* macOS Sparkline Demo */, 23102BD823B02B0A001A621D /* macOS Sparkline Demo Objc */, 23E2278823B5846E00E59122 /* macOS Table Demo */, 23102BC023B02AED001A621D /* iOS Sparkline Demo */, 23102BF023B02B32001A621D /* tvOS Sparkline Demo */, 236A57EF25C1392B00F2BACF /* SwiftUI Sparkline Demo (iOS) */, 236A57F625C1392B00F2BACF /* SwiftUI Sparkline Demo (macOS) */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 23102B4E23B0245A001A621D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 23102B5823B0245B001A621D /* Assets.xcassets in Resources */, 23102B5B23B0245B001A621D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 23102BBF23B02AED001A621D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 23102BD023B02AEE001A621D /* LaunchScreen.storyboard in Resources */, 23102BCD23B02AEE001A621D /* Assets.xcassets in Resources */, 23102BCB23B02AED001A621D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 23102BD723B02B0A001A621D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 23102BE223B02B0B001A621D /* Assets.xcassets in Resources */, 23102BE523B02B0B001A621D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 23102BEF23B02B32001A621D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 23102BFE23B02B33001A621D /* LaunchScreen.storyboard in Resources */, 23102BFB23B02B33001A621D /* Assets.xcassets in Resources */, 23102BF923B02B32001A621D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 236A57EE25C1392B00F2BACF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 236A57FF25C1392B00F2BACF /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 236A57F525C1392B00F2BACF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 236A580025C1392B00F2BACF /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 23E2278723B5846E00E59122 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 23E2279023B5846E00E59122 /* Assets.xcassets in Resources */, 23E2279323B5846E00E59122 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 23102B4C23B0245A001A621D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 23102B5623B0245A001A621D /* ViewController.swift in Sources */, 23102C0423B03315001A621D /* cpuUsage.swift in Sources */, 23102B5423B0245A001A621D /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 23102BBD23B02AED001A621D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 23102BC823B02AED001A621D /* ViewController.swift in Sources */, 23102BC423B02AED001A621D /* AppDelegate.swift in Sources */, 23102BC623B02AED001A621D /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 23102BD523B02B0A001A621D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 23102BE023B02B0A001A621D /* ViewController.m in Sources */, 23102BE823B02B0B001A621D /* main.m in Sources */, 23102BDD23B02B0A001A621D /* AppDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 23102BED23B02B32001A621D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 23102BF623B02B32001A621D /* ViewController.swift in Sources */, 23102BF423B02B32001A621D /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 236A57EC25C1392B00F2BACF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 238053C725DCC2B100B996B5 /* ActiveView.swift in Sources */, 238DB6EA25EB3A2E009197AB /* TabletView.swift in Sources */, 237B827825E7891E00B3DE23 /* BitmapGenerationView.swift in Sources */, 236417E729628C0600C8A1D2 /* WiperGaugeDemoView.swift in Sources */, 235A585B2603181A00266642 /* PercentBarView.swift in Sources */, 2322C06B262651D5007D8112 /* TestingView.swift in Sources */, 232BAF6D25EB16210090C447 /* SwiftUIView.swift in Sources */, 2346EF3025E8D13A00050570 /* SwiftUIContentView.swift in Sources */, 23FC81DD25DB60760047A800 /* StripesDemoView.swift in Sources */, 23FC819F25DB59160047A800 /* DataBarGraphContent.swift in Sources */, 23FC817525DB547E0047A800 /* StackLineDemoContentView.swift in Sources */, 238DB6DC25EB2141009197AB /* StripesOverlaidView.swift in Sources */, 237B824725E77B3800B3DE23 /* BarDemoContentView.swift in Sources */, 23905CA12B5B5D00007B7769 /* CircularProgress.swift in Sources */, 236A57FD25C1392B00F2BACF /* ContentView.swift in Sources */, 236A57FB25C1392B00F2BACF /* SwiftUI_Sparkline_DemoApp.swift in Sources */, 23BC66862B0DD82A0064FF62 /* ActivityGridView.swift in Sources */, 23D10B1025ED9EB500BBE555 /* AttributedString.swift in Sources */, 232D9ECE25EC7133004DFAFC /* SuperCoolLineSpark.swift in Sources */, 239DE4042BA177D300E13208 /* CircularGaugeView.swift in Sources */, 23FC810025DB3FC50047A800 /* ReportView.swift in Sources */, 23FC814025DB4E5D0047A800 /* LineDemoContentView.swift in Sources */, 23529A7425E84D2A00DFA52D /* Extensions.swift in Sources */, 23FC812025DB47A20047A800 /* WinLossGraphContentView.swift in Sources */, 238053E525DCD14600B996B5 /* OverlayView.swift in Sources */, 23FC81B425DB59E70047A800 /* DotGraphView.swift in Sources */, 23FC819325DB586B0047A800 /* PieGraphDemoView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 236A57F325C1392B00F2BACF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 238053C825DCC2B100B996B5 /* ActiveView.swift in Sources */, 238DB6EB25EB3A2F009197AB /* TabletView.swift in Sources */, 237B827925E7891E00B3DE23 /* BitmapGenerationView.swift in Sources */, 236417E829628C0600C8A1D2 /* WiperGaugeDemoView.swift in Sources */, 235A585C2603181A00266642 /* PercentBarView.swift in Sources */, 2322C06C262651D5007D8112 /* TestingView.swift in Sources */, 232BAF6E25EB16210090C447 /* SwiftUIView.swift in Sources */, 2346EF3125E8D13A00050570 /* SwiftUIContentView.swift in Sources */, 23FC81DE25DB60760047A800 /* StripesDemoView.swift in Sources */, 23FC81A025DB59160047A800 /* DataBarGraphContent.swift in Sources */, 23FC817625DB547E0047A800 /* StackLineDemoContentView.swift in Sources */, 238DB6DD25EB2141009197AB /* StripesOverlaidView.swift in Sources */, 237B824825E77B3800B3DE23 /* BarDemoContentView.swift in Sources */, 23905CA22B5B5D00007B7769 /* CircularProgress.swift in Sources */, 236A57FE25C1392B00F2BACF /* ContentView.swift in Sources */, 236A57FC25C1392B00F2BACF /* SwiftUI_Sparkline_DemoApp.swift in Sources */, 23BC66872B0DD82A0064FF62 /* ActivityGridView.swift in Sources */, 23D10B1125ED9EB500BBE555 /* AttributedString.swift in Sources */, 232D9ECF25EC7133004DFAFC /* SuperCoolLineSpark.swift in Sources */, 239DE4052BA177D300E13208 /* CircularGaugeView.swift in Sources */, 23FC810125DB3FC50047A800 /* ReportView.swift in Sources */, 23FC814125DB4E5D0047A800 /* LineDemoContentView.swift in Sources */, 23529A7525E84D2A00DFA52D /* Extensions.swift in Sources */, 23FC812125DB47A20047A800 /* WinLossGraphContentView.swift in Sources */, 238053E625DCD14600B996B5 /* OverlayView.swift in Sources */, 23FC81B525DB59E70047A800 /* DotGraphView.swift in Sources */, 23FC819425DB586B0047A800 /* PieGraphDemoView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 23E2278523B5846E00E59122 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 23E2278E23B5846E00E59122 /* ViewController.swift in Sources */, 23E2278C23B5846E00E59122 /* AppDelegate.swift in Sources */, 232F732323B5BE2A00FA3A3F /* GridViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 23369E8525D65B2D00A40023 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 236A57EF25C1392B00F2BACF /* SwiftUI Sparkline Demo (iOS) */; targetProxy = 23369E8425D65B2D00A40023 /* PBXContainerItemProxy */; }; 23369E8725D65B2D00A40023 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 236A57F625C1392B00F2BACF /* SwiftUI Sparkline Demo (macOS) */; targetProxy = 23369E8625D65B2D00A40023 /* PBXContainerItemProxy */; }; 239716BE23B554E50034A4D4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 23102B4F23B0245A001A621D /* macOS Sparkline Demo */; targetProxy = 239716BD23B554E50034A4D4 /* PBXContainerItemProxy */; }; 239716C023B554E70034A4D4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 23102BC023B02AED001A621D /* iOS Sparkline Demo */; targetProxy = 239716BF23B554E70034A4D4 /* PBXContainerItemProxy */; }; 239716C223B554E80034A4D4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 23102BD823B02B0A001A621D /* macOS Sparkline Demo Objc */; targetProxy = 239716C123B554E80034A4D4 /* PBXContainerItemProxy */; }; 23E2279A23B5A6EF00E59122 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 23E2278823B5846E00E59122 /* macOS Table Demo */; targetProxy = 23E2279923B5A6EF00E59122 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 23102B5923B0245B001A621D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 23102B5A23B0245B001A621D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 23102BC923B02AED001A621D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 23102BCA23B02AED001A621D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 23102BCE23B02AEE001A621D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 23102BCF23B02AEE001A621D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; 23102BE323B02B0B001A621D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 23102BE423B02B0B001A621D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 23102BF723B02B32001A621D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 23102BF823B02B32001A621D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 23102BFC23B02B33001A621D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 23102BFD23B02B33001A621D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; 23E2279123B5846E00E59122 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 23E2279223B5846E00E59122 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 23102B4923B0165B001A621D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Debug; }; 23102B4A23B0165B001A621D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Release; }; 23102B5F23B0245B001A621D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "macOS Sparkline Demo/macOS_Sparkline_Demo.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "macOS Sparkline Demo/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.sparklines.macOS-Sparkline-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 23102B6023B0245B001A621D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "macOS Sparkline Demo/macOS_Sparkline_Demo.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "macOS Sparkline Demo/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.sparklines.macOS-Sparkline-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; }; name = Release; }; 23102BD223B02AEE001A621D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "iOS Sparkline Demo/iOS Sparkline Demo.entitlements"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "iOS Sparkline Demo/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.sparklines.iOS-Sparkline-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,6"; }; name = Debug; }; 23102BD323B02AEE001A621D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "iOS Sparkline Demo/iOS Sparkline Demo.entitlements"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "iOS Sparkline Demo/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.sparklines.iOS-Sparkline-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTS_MACCATALYST = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,6"; VALIDATE_PRODUCT = YES; }; name = Release; }; 23102BEB23B02B0B001A621D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "macOS Sparkline Demo Objc/macOS_Sparkline_Demo_Objc.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "macOS Sparkline Demo Objc/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.sparklines.macOS-Sparkline-Demo-Objc"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; }; name = Debug; }; 23102BEC23B02B0B001A621D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "macOS Sparkline Demo Objc/macOS_Sparkline_Demo_Objc.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "macOS Sparkline Demo Objc/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.sparklines.macOS-Sparkline-Demo-Objc"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; }; name = Release; }; 23102C0123B02B33001A621D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "tvOS Sparkline Demo/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.sparklines.tvOS-Sparkline-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 14.0; }; name = Debug; }; 23102C0223B02B33001A621D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "tvOS Sparkline Demo/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.sparklines.tvOS-Sparkline-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 14.0; VALIDATE_PRODUCT = YES; }; name = Release; }; 236A580125C1392B00F2BACF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_PREVIEWS = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "SwiftUI Sparkline Crossplatform/iOS/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.SwiftUI-Sparkline-Demo"; PRODUCT_NAME = "SwiftUI Sparkline Demo (iOS)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 236A580225C1392B00F2BACF /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_NS_ASSERTIONS = NO; ENABLE_PREVIEWS = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "SwiftUI Sparkline Crossplatform/iOS/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.SwiftUI-Sparkline-Demo"; PRODUCT_NAME = "SwiftUI Sparkline Demo (iOS)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 236A580325C1392B00F2BACF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "SwiftUI Sparkline Crossplatform/macOS/macOS.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "SwiftUI Sparkline Crossplatform/macOS/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.SwiftUI-Sparkline-Demo"; PRODUCT_NAME = "SwiftUI Sparkline Demo (macOS)"; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 236A580425C1392B00F2BACF /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "SwiftUI Sparkline Crossplatform/macOS/macOS.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_PREVIEWS = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "SwiftUI Sparkline Crossplatform/macOS/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.SwiftUI-Sparkline-Demo"; PRODUCT_NAME = "SwiftUI Sparkline Demo (macOS)"; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; }; name = Release; }; 239716BA23B554DD0034A4D4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 3L6RK3LGGW; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; }; 239716BB23B554DD0034A4D4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 3L6RK3LGGW; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; 23E2279623B5846E00E59122 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "macOS Table Demo/macOS_Table_Demo.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "macOS Table Demo/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.sparklines.macOS-Table-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 23E2279723B5846E00E59122 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "macOS Table Demo/macOS_Table_Demo.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 3L6RK3LGGW; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "macOS Table Demo/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.darrenford.sparklines.macOS-Table-Demo"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 23102B4823B0165B001A621D /* Build configuration list for PBXProject "Demos" */ = { isa = XCConfigurationList; buildConfigurations = ( 23102B4923B0165B001A621D /* Debug */, 23102B4A23B0165B001A621D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 23102B5E23B0245B001A621D /* Build configuration list for PBXNativeTarget "macOS Sparkline Demo" */ = { isa = XCConfigurationList; buildConfigurations = ( 23102B5F23B0245B001A621D /* Debug */, 23102B6023B0245B001A621D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 23102BD423B02AEE001A621D /* Build configuration list for PBXNativeTarget "iOS Sparkline Demo" */ = { isa = XCConfigurationList; buildConfigurations = ( 23102BD223B02AEE001A621D /* Debug */, 23102BD323B02AEE001A621D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 23102BEA23B02B0B001A621D /* Build configuration list for PBXNativeTarget "macOS Sparkline Demo Objc" */ = { isa = XCConfigurationList; buildConfigurations = ( 23102BEB23B02B0B001A621D /* Debug */, 23102BEC23B02B0B001A621D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 23102C0023B02B33001A621D /* Build configuration list for PBXNativeTarget "tvOS Sparkline Demo" */ = { isa = XCConfigurationList; buildConfigurations = ( 23102C0123B02B33001A621D /* Debug */, 23102C0223B02B33001A621D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 236A580525C1392B00F2BACF /* Build configuration list for PBXNativeTarget "SwiftUI Sparkline Demo (iOS)" */ = { isa = XCConfigurationList; buildConfigurations = ( 236A580125C1392B00F2BACF /* Debug */, 236A580225C1392B00F2BACF /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 236A580625C1392B00F2BACF /* Build configuration list for PBXNativeTarget "SwiftUI Sparkline Demo (macOS)" */ = { isa = XCConfigurationList; buildConfigurations = ( 236A580325C1392B00F2BACF /* Debug */, 236A580425C1392B00F2BACF /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 239716BC23B554DD0034A4D4 /* Build configuration list for PBXAggregateTarget "All Demos" */ = { isa = XCConfigurationList; buildConfigurations = ( 239716BA23B554DD0034A4D4 /* Debug */, 239716BB23B554DD0034A4D4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 23E2279823B5846E00E59122 /* Build configuration list for PBXNativeTarget "macOS Table Demo" */ = { isa = XCConfigurationList; buildConfigurations = ( 23E2279623B5846E00E59122 /* Debug */, 23E2279723B5846E00E59122 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ 23B006F42DDD61A400D074A0 /* XCLocalSwiftPackageReference "../../../DSFSparkline" */ = { isa = XCLocalSwiftPackageReference; relativePath = ../../../DSFSparkline; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 23B006F62DDD61C700D074A0 /* DSFSparkline */ = { isa = XCSwiftPackageProductDependency; package = 23B006F42DDD61A400D074A0 /* XCLocalSwiftPackageReference "../../../DSFSparkline" */; productName = DSFSparkline; }; 23B006F82DDD61FB00D074A0 /* DSFSparkline-shared */ = { isa = XCSwiftPackageProductDependency; package = 23B006F42DDD61A400D074A0 /* XCLocalSwiftPackageReference "../../../DSFSparkline" */; productName = "DSFSparkline-shared"; }; 23B006FC2DDD625E00D074A0 /* DSFSparkline */ = { isa = XCSwiftPackageProductDependency; package = 23B006F42DDD61A400D074A0 /* XCLocalSwiftPackageReference "../../../DSFSparkline" */; productName = DSFSparkline; }; 23B006FE2DDD63E900D074A0 /* DSFSparkline-shared */ = { isa = XCSwiftPackageProductDependency; package = 23B006F42DDD61A400D074A0 /* XCLocalSwiftPackageReference "../../../DSFSparkline" */; productName = "DSFSparkline-shared"; }; 23B007022DDD652800D074A0 /* DSFSparkline */ = { isa = XCSwiftPackageProductDependency; package = 23B006F42DDD61A400D074A0 /* XCLocalSwiftPackageReference "../../../DSFSparkline" */; productName = DSFSparkline; }; 23B007042DDD65A800D074A0 /* DSFSparkline */ = { isa = XCSwiftPackageProductDependency; package = 23B006F42DDD61A400D074A0 /* XCLocalSwiftPackageReference "../../../DSFSparkline" */; productName = DSFSparkline; }; 23B007062DDD65B800D074A0 /* DSFSparkline-shared */ = { isa = XCSwiftPackageProductDependency; package = 23B006F42DDD61A400D074A0 /* XCLocalSwiftPackageReference "../../../DSFSparkline" */; productName = "DSFSparkline-shared"; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 23102B4523B0165B001A621D /* Project object */; } ================================================ FILE: Demos/Samples/Demos.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Demos/Samples/Demos.xcodeproj/xcshareddata/xcschemes/All Demos.xcscheme ================================================ ================================================ FILE: Demos/Samples/Demos.xcodeproj/xcshareddata/xcschemes/SwiftUI Sparkline Demo (iOS).xcscheme ================================================ ================================================ FILE: Demos/Samples/Demos.xcodeproj/xcshareddata/xcschemes/SwiftUI Sparkline Demo (macOS).xcscheme ================================================ ================================================ FILE: Demos/Samples/Demos.xcodeproj/xcshareddata/xcschemes/iOS Sparkline Demo.xcscheme ================================================ ================================================ FILE: Demos/Samples/Demos.xcodeproj/xcshareddata/xcschemes/macOS Sparkline Demo Objc.xcscheme ================================================ ================================================ FILE: Demos/Samples/Demos.xcodeproj/xcshareddata/xcschemes/macOS Sparkline Demo.xcscheme ================================================ ================================================ FILE: Demos/Samples/Demos.xcodeproj/xcshareddata/xcschemes/macOS Table Demo.xcscheme ================================================ ================================================ FILE: Demos/Samples/Demos.xcodeproj/xcshareddata/xcschemes/tvOS Sparkline Demo.xcscheme ================================================ ================================================ FILE: Demos/Samples/Demos.xcodeproj/xcuserdata/dford.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist ================================================ ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/ActiveView.swift ================================================ // // ActiveView.swift // Demos // // Created by Darren Ford on 17/2/21. // import SwiftUI import DSFSparkline struct UpperGraph: View { let label: String let dataSource: DSFSparkline.DataSource let graphColor: DSFColor let showZeroLine: Bool var zeroLineDefinition: DSFSparkline.ZeroLineDefinition = .shared let interpolated: Bool let lineShading: Bool var shadowed: Bool = false var body: some View { DSFSparklineLineGraphView.SwiftUI(dataSource: dataSource, graphColor: graphColor, interpolated: interpolated, lineShading: lineShading, shadowed: shadowed, showZeroLine: showZeroLine, zeroLineDefinition: self.zeroLineDefinition) .background( Rectangle() .fill(Color(.displayP3, white: 1.0, opacity: 0.1)) .shadow(color: .black, radius: 8, x: 4, y: -4) ) .clipShape(RoundedRectangle(cornerRadius: 8)) .padding(4) .background( RoundedRectangle(cornerRadius: 8) .fill(Color(.displayP3, white: 0.5, opacity: 0.1)) .shadow(color: .black, radius: 8, x: 4, y: -4) ) .overlay( VStack(alignment: .leading, spacing: nil, content: { Text(self.label) .shadow(color: .black, radius: 1) Color.clear }).padding(6), alignment: .leading ) } } var PreviewUpperGraphDataSource: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 10, range: 0.0 ... 100.0, zeroLineValue: 25) d.push(values: [20, 77, 90, 22, 4, 16, 66, 99, 88, 44]) return d }() struct UpperGraph_Previews: PreviewProvider { static var previews: some View { UpperGraph(label: "Testing", dataSource: PreviewUpperGraphDataSource, graphColor: DSFColor.systemOrange, showZeroLine: true, zeroLineDefinition: DSFSparkline.ZeroLineDefinition(), interpolated: true, lineShading: true, shadowed: true) } } ///////////// var PreviewGlobalDataSource1: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 10, range: 0 ... 1.0) d.push(values: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]) return d }() var PreviewGlobalDataSource2: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 10, range: -1.0 ... 1.0) d.push(values: [-0.5, -0.4, -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5]) return d }() var PreviewGlobalDataSource3: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: 0.0 ... 100.0) d.push(values: [50, 40, 30, 20, 10, 0, 100, 90, 80, 70]) return d }() var PreviewGlobalDataSource4: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: 0.0 ... 100.0) d.push(values: [20, 77, 90, 22, 4, 16, 66, 99, 88, 44]) return d }() var PreviewGlobalDataSource5: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: 0.0 ... 100.0) d.push(values: [20, 77, 90, 22, 4, 16, 66, 99, 88, 44]) return d }() struct ActiveView_Previews: PreviewProvider { static var previews: some View { ActiveView(dataSource: DataSource(PreviewGlobalDataSource1, PreviewGlobalDataSource2, PreviewGlobalDataSource3, PreviewGlobalDataSource4, PreviewGlobalDataSource5) ) } } func MakeActiveView() -> ActiveView { return ActiveView(dataSource: globalSource) } struct ActiveView: View { let dataSource: DataSource let BigCyanZeroLine = DSFSparkline.ZeroLineDefinition( color: .cyan, lineWidth: 3, lineDashStyle: [4, 1, 2, 1] ) @State var selectedType = 1 let gradient = DSFSparkline.GradientBucket(posts: [ DSFSparkline.GradientBucket.Post(color: DSFColor.systemRed.cgColor, location: 0), DSFSparkline.GradientBucket.Post(color: DSFColor.systemOrange.cgColor, location: 1 / 6), DSFSparkline.GradientBucket.Post(color: DSFColor.systemYellow.cgColor, location: 2 / 6), DSFSparkline.GradientBucket.Post(color: DSFColor.systemGreen.cgColor, location: 3 / 6), DSFSparkline.GradientBucket.Post(color: DSFColor.systemBlue.cgColor, location: 4 / 6), DSFSparkline.GradientBucket.Post(color: DSFColor.systemIndigo.cgColor, location: 5 / 6), DSFSparkline.GradientBucket.Post(color: DSFColor.systemPurple.cgColor, location: 6 / 6), ]) var body: some View { VStack { HStack(alignment: .center, spacing: 8, content: { UpperGraph(label: "Left", dataSource: dataSource.PreviewGlobalDataSource4, graphColor: DSFColor.systemOrange, showZeroLine: false, interpolated: false, lineShading: true).frame(height: 60) UpperGraph(label: "Middle", dataSource: dataSource.PreviewGlobalDataSource4, graphColor: DSFColor.systemYellow, showZeroLine: true, interpolated: true, lineShading: true).frame(height: 60) UpperGraph(label: "Right", dataSource: dataSource.PreviewGlobalDataSource4, graphColor: DSFColor.systemPurple, showZeroLine: false, interpolated: false, lineShading: false).frame(height: 60) }) HStack { VStack { DSFSparklineBarGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource1, graphColor: DSFColor.systemBlue, barSpacing: 1, showZeroLine: true, showHighlightRange: true, highlightDefinitions: [ DSFSparkline.HighlightRangeDefinition( range: 0 ..< 0.5, fillColor: DSFColor.gray.withAlphaComponent(0.3).cgColor ), ]) .frame(height: 60) DSFSparklineStripesGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource1, barSpacing: 1, gradient: self.gradient) .frame(height: 60) .padding(2) } DSFSparklineBarGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource2, graphColor: DSFColor.systemGreen, lineWidth: 2, barSpacing: 2, showZeroLine: true, zeroLineDefinition: BigCyanZeroLine, centeredAtZeroLine: true) .frame(height: 60) } HStack { DSFSparklineDotGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource3, graphColor: DSFColor.systemBlue, unsetGraphColor: DSFColor.darkGray.withAlphaComponent(0.2)) .frame(height: 60) .padding(2) DSFSparklineDotGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource3, graphColor: DSFColor.systemRed, unsetGraphColor: DSFColor.darkGray.withAlphaComponent(0.2), upsideDown: true) .frame(height: 60) .padding(2) VStack(alignment: .center, spacing: nil, content: { DSFSparklineDotGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource3, graphColor: DSFColor.systemGreen, unsetGraphColor: DSFColor.darkGray.withAlphaComponent(0.2), verticalDotCount: 10) .frame(height: 60) DSFSparklineDotGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource3, graphColor: DSFColor.systemPink, unsetGraphColor: DSFColor.darkGray.withAlphaComponent(0.2), verticalDotCount: 10, upsideDown: true) .frame(height: 60) }) } #if os(macOS) HStack { Picker(selection: $selectedType, label: EmptyView()) { Text("Line").tag(1) Text("Line (Smooth)").tag(2) Text("Bar").tag(3) Text("Dot").tag(4) Text("Stripes").tag(5) }.pickerStyle(RadioGroupPickerStyle()) Group { if self.selectedType == 1 { DSFSparklineLineGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource5, graphColor: DSFColor.systemOrange, showZeroLine: true) } else if self.selectedType == 2 { DSFSparklineLineGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource5, graphColor: DSFColor.systemOrange, interpolated: true, showZeroLine: true) } else if self.selectedType == 3 { DSFSparklineBarGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource5, graphColor: DSFColor.systemOrange, barSpacing: 1, showZeroLine: true) } else if self.selectedType == 4 { DSFSparklineDotGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource5, graphColor: DSFColor.systemOrange) } else { DSFSparklineStripesGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource5, barSpacing: 1, gradient: self.gradient) } } .frame(height: 80) .padding(2) } #else VStack { Picker(selection: $selectedType, label: EmptyView()) { Text("Line").tag(1) Text("Line (Smooth)").tag(2) Text("Bar").tag(3) Text("Dot").tag(4) Text("Stripes").tag(5) } Group { if self.selectedType == 1 { DSFSparklineLineGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource5, graphColor: DSFColor.systemOrange, showZeroLine: true) } else if self.selectedType == 2 { DSFSparklineLineGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource5, graphColor: DSFColor.systemOrange, interpolated: true, showZeroLine: true) } else if self.selectedType == 3 { DSFSparklineBarGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource5, graphColor: DSFColor.systemOrange, barSpacing: 1, showZeroLine: true) } else if self.selectedType == 4 { DSFSparklineDotGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource5, graphColor: DSFColor.systemOrange) } else { DSFSparklineStripesGraphView.SwiftUI(dataSource: dataSource.PreviewGlobalDataSource5, barSpacing: 1, gradient: self.gradient) } } .frame(height: 80) .padding(2) } #endif }.padding(20) .onAppear { self.dataSource.start() } .onDisappear { self.dataSource.stop() } } } let globalSource = DataSource( DSFSparkline.DataSource(windowSize: 30), DSFSparkline.DataSource(range: -1.0 ... 1.0), DSFSparkline.DataSource(range: -100 ... 100), DSFSparkline.DataSource(range: 0 ... 100), DSFSparkline.DataSource(range: 0 ... 100, zeroLineValue: 80) ) class DataSource { let PreviewGlobalDataSource1: DSFSparkline.DataSource let PreviewGlobalDataSource2: DSFSparkline.DataSource let PreviewGlobalDataSource3: DSFSparkline.DataSource let PreviewGlobalDataSource4: DSFSparkline.DataSource let PreviewGlobalDataSource5: DSFSparkline.DataSource var shouldStop: Bool = false init(_ d1: DSFSparkline.DataSource, _ d2: DSFSparkline.DataSource, _ d3: DSFSparkline.DataSource, _ d4: DSFSparkline.DataSource, _ d5: DSFSparkline.DataSource) { self.PreviewGlobalDataSource1 = d1 self.PreviewGlobalDataSource2 = d2 self.PreviewGlobalDataSource3 = d3 self.PreviewGlobalDataSource4 = d4 self.PreviewGlobalDataSource5 = d5 } func start() { self.shouldStop = false self.PreviewGlobalDataSource3.windowSize = 100 self.PreviewGlobalDataSource4.windowSize = 40 _ = self.PreviewGlobalDataSource4.push(value: 50) self.PreviewGlobalDataSource5.windowSize = 50 self.updateWithNewValues() } func stop() { self.shouldStop = true } var sinusoid = 0.00 var lastSource4: CGFloat = 50.0 func updateWithNewValues() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in guard let `self` = self else { return } if self.shouldStop { return } let val = sin(self.sinusoid) self.sinusoid += 0.12 let cr = CGFloat(val) _ = self.PreviewGlobalDataSource1.push(value: cr) let cr2 = CGFloat.random(in: self.PreviewGlobalDataSource2.range!) // -1 ... 1) _ = self.PreviewGlobalDataSource2.push(value: cr2) let cr3 = CGFloat.random(in: self.PreviewGlobalDataSource3.range!) // -100 ... 100) _ = self.PreviewGlobalDataSource3.push(value: cr3) let cr4 = CGFloat.random(in: -20 ... 20) let newVal = min(100, max(0, self.lastSource4 + cr4)) _ = self.PreviewGlobalDataSource4.push(value: newVal) self.lastSource4 = newVal let cr5 = CGFloat.random(in: self.PreviewGlobalDataSource5.range!) _ = self.PreviewGlobalDataSource5.push(value: cr5) self.updateWithNewValues() } } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/ActivityGridView.swift ================================================ // // TestingView.swift // Demos // // Created by Darren Ford on 14/4/21. // import SwiftUI import DSFSparkline struct ActivityGridView: View { @State var currentDate = Date() let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @State var inputData: [Double] = [] let cellStyleLight = DSFSparkline.ActivityGrid.CellStyle(fillScheme: DSFSparkline.ActivityGrid.CellStyle.DefaultLight) let cellStyleDark = DSFSparkline.ActivityGrid.CellStyle(fillScheme: DSFSparkline.ActivityGrid.CellStyle.DefaultDark) let gradient = DSFSparkline.GradientBucket( colors: [ CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1), CGColor(srgbRed: 1, green: 1, blue: 0, alpha: 1), CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1), ] ) let fiveColorGradient = DSFSparkline.GradientBucket( colors: [ CGColor(red: 0.806, green: 0.164, blue: 0.287, alpha: 1.0), CGColor(red: 0.978, green: 0.620, blue: 0.303, alpha: 1.0), CGColor(red: 0.954, green: 0.431, blue: 0.221, alpha: 1.0), CGColor(red: 0.935, green: 0.280, blue: 0.286, alpha: 1.0), CGColor(red: 0.345, green: 0.155, blue: 0.259, alpha: 1.0), ] ) let SmallerFullRange: [Double] = (0 ... 1000).map { _ in Double.random(in: 0 ... 1) } @State var isLightMode: Bool = false var body: some View { VStack(spacing: 4) { Toggle("Use Alternate Palette", isOn: $isLightMode) Text("Github style").font(.title3) DSFSparklineActivityGridView.SwiftUI( values: inputData, range: 0 ... 1, cellStyle: isLightMode ? cellStyleLight : cellStyleDark ) .tooltipStringForCell { index in "Index[\(index)]" } .onReceive(timer) { input in self.inputData.insert(Double.random(in: 0 ... 1), at: 0) } .border(.red) Text("Defrag style").font(.title3) DSFSparklineActivityGridView.SwiftUI( values: inputData, range: 0 ... 1, cellStyle: isLightMode ? cellStyleLight : cellStyleDark, layoutStyle: .defrag ) .border(.red) Divider() .frame(maxWidth: .infinity) VStack { Text("Fixed row/column").font(.title3) HStack { DSFSparklineActivityGridView.SwiftUI( values: inputData, range: 0 ... 1, verticalCellCount: 10, horizontalCellCount: 7, layoutStyle: .defrag, fillScheme: isLightMode ? cellStyleLight.fillScheme : cellStyleDark.fillScheme, borderColor: isLightMode ? .init(gray: 0, alpha: 0.2) : .init(gray: 1, alpha: 0.2), borderWidth: 0.5 ) .border(.red) DSFSparklineActivityGridView.SwiftUI( values: inputData, range: 0 ... 1, verticalCellCount: 10, horizontalCellCount: 7, layoutStyle: .github, fillScheme: isLightMode ? cellStyleLight.fillScheme : cellStyleDark.fillScheme, borderColor: isLightMode ? .init(gray: 0, alpha: 0.2) : .init(gray: 1, alpha: 0.2), borderWidth: 0.5 ) .border(.green) DSFSparklineActivityGridView.SwiftUI( values: SmallerFullRange, range: 0 ... 1, verticalCellCount: 7, horizontalCellCount: 10, cellStyle: .init(fillScheme: .init(gradient: gradient), cellDimension: 6, cellSpacing: 3), layoutStyle: .defrag ) .border(.blue) DSFSparklineActivityGridView.SwiftUI( values: SmallerFullRange, range: 0 ... 1, verticalCellCount: 7, horizontalCellCount: 10, cellStyle: .init(fillScheme: .init(gradient: gradient), cellDimension: 15, cellSpacing: 1), layoutStyle: .github ) .border(.yellow) VStack(spacing: 0) { HStack(spacing: 6) { Text("S").font(.system(.caption, design: .monospaced)) Text("M").font(.system(.caption, design: .monospaced)) Text("T").font(.system(.caption, design: .monospaced)) Text("W").font(.system(.caption, design: .monospaced)) Text("T").font(.system(.caption, design: .monospaced)) Text("F").font(.system(.caption, design: .monospaced)) Text("S").font(.system(.caption, design: .monospaced)) } .padding(3) DSFSparklineActivityGridView.SwiftUI( values: SmallerFullRange, range: 0 ... 1, verticalCellCount: 30, horizontalCellCount: 7, cellStyle: .init(fillScheme: .init(gradient: fiveColorGradient)), layoutStyle: .github ) .tooltipStringForCell { index in "Tooltip[\(SmallerFullRange[index])]" } } } } } .padding() } } struct ActivityGridView_Previews: PreviewProvider { static var previews: some View { ActivityGridView() .frame(height: 1000) } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" }, { "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/AttributedString.swift ================================================ // // AttributedString.swift // Demos // // Appropriated from https://swiftui-lab.com/attributed-strings-with-swiftui/ // An NSTextView wrapped for SwiftUI containing an attributed string. import SwiftUI #if os(macOS) import AppKit struct AttributedText: View { @State var size: CGSize = .zero let attributedString: NSAttributedString init(_ attributedString: NSAttributedString) { self.attributedString = attributedString } var body: some View { AttributedTextRepresentable(attributedString: attributedString, size: $size) .frame(width: size.width, height: size.height) } struct AttributedTextRepresentable: NSViewRepresentable { let attributedString: NSAttributedString @Binding var size: CGSize func makeNSView(context: Context) -> NSTextView { let textView = NSTextView() guard let textContainer = textView.textContainer else { fatalError() } textContainer.widthTracksTextView = false textContainer.containerSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) textView.drawsBackground = false return textView } func updateNSView(_ nsView: NSTextView, context: Context) { nsView.textStorage?.setAttributedString(attributedString) DispatchQueue.main.async { if let textContainer = nsView.textContainer, let layoutManager = textContainer.layoutManager { layoutManager.ensureLayout(for: textContainer) size = layoutManager.usedRect(for: textContainer).size } else { size = .zero } // This doesn't work accurately. //size = nsView.textStorage!.size() } } } } #else import UIKit struct AttributedText: View { @State private var size: CGSize = .zero let attributedString: NSAttributedString init(_ attributedString: NSAttributedString) { self.attributedString = attributedString } var body: some View { AttributedTextRepresentable(attributedString: attributedString, size: $size) .frame(width: size.width, height: size.height) } struct AttributedTextRepresentable: UIViewRepresentable { let attributedString: NSAttributedString @Binding var size: CGSize func makeUIView(context: Context) -> UILabel { let label = UILabel() label.lineBreakMode = .byClipping label.numberOfLines = 0 return label } func updateUIView(_ uiView: UILabel, context: Context) { uiView.attributedText = attributedString DispatchQueue.main.async { size = uiView.sizeThatFits(uiView.superview?.bounds.size ?? .zero) } } } } #endif ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/BarDemoContentView.swift ================================================ // // StackLineDemoContentView.swift // Demos // // Created by Darren Ford on 16/2/21. // import SwiftUI import DSFSparkline struct BarBasic: View { var body: some View { Text("Bar") DSFSparklineBarGraphView.SwiftUI( dataSource: BarDataSource1, graphColor: DSFColor.systemGray, lineWidth: 2, barSpacing: 2 ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct BarZeroLine: View { var body: some View { Text("Bar with zero-line") DSFSparklineBarGraphView.SwiftUI( dataSource: BarDataSource1, graphColor: DSFColor.systemPink, lineWidth: 1, barSpacing: 0, showZeroLine: true ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct BarCenteredZeroLine: View { var body: some View { Text("Bar centered around zero-line") DSFSparklineBarGraphView.SwiftUI( dataSource: BarDataSource1, graphColor: DSFColor.systemBlue, lineWidth: 2, barSpacing: 4, showZeroLine: true, centeredAtZeroLine: true ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct BarCenteredZeroLineColored: View { var body: some View { Text("Bar centered around zero-line, lower color") DSFSparklineBarGraphView.SwiftUI( dataSource: BarDataSource1, graphColor: DSFColor.systemGreen, lineWidth: 1, //showZeroLine: true, centeredAtZeroLine: true, lowerGraphColor: DSFColor.systemRed ) .frame(height: 60.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } private let primaryFill = DSFSparkline.Fill.Gradient(colors: [ CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1), CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1), ]) private let secondaryFill = DSFSparkline.Fill.Gradient(colors: [ CGColor(srgbRed: 1, green: 1, blue: 0, alpha: 1), CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1), ]) struct BarCenteredZeroLineCustomFill: View { var body: some View { Text("Bar centered around zero-line, custom fill") DSFSparklineBarGraphView.SwiftUI( dataSource: BarDataSource1, graphColor: DSFColor.black, lineWidth: 1, showZeroLine: true, centeredAtZeroLine: true, primaryFill: primaryFill, secondaryFill: secondaryFill ) .frame(height: 60.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct BarWithCustomFill: View { var body: some View { Text("Bar, custom fill") DSFSparklineBarGraphView.SwiftUI( dataSource: BarDataSource1, graphColor: DSFColor.black, lineWidth: 1, primaryFill: primaryFill, secondaryFill: secondaryFill ) .frame(height: 60.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct BarRange: View { var body: some View { Text("Bar with range") DSFSparklineBarGraphView.SwiftUI( dataSource: BarDataSource2, graphColor: DSFColor.systemGreen, lineWidth: 1, showZeroLine: true, highlightDefinitions: [ DSFSparkline.HighlightRangeDefinition( range: 20 ..< 80, fillColor: DSFColor.systemGray.withAlphaComponent(0.2).cgColor ) ], gridLines: .init(values: [0, 25, 50, 75, 100]) ) .frame(width: 250.0, height: 59.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct BarNofill: View { var body: some View { Text("Bar no fill") DSFSparklineBarGraphView.SwiftUI( dataSource: BarDataSource3, graphColor: DSFColor.systemYellow, lineWidth: 3 ) .frame(width: 330.0, height: 59.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct BarRange2: View { var body: some View { Text("Bar with range") DSFSparklineBarGraphView.SwiftUI( dataSource: BarDataSource4, graphColor: DSFColor.systemRed, lineWidth: 1, showZeroLine: true, highlightDefinitions: [ DSFSparkline.HighlightRangeDefinition( range: 0.3 ..< 0.7, fillColor: DSFColor.systemPink.withAlphaComponent(0.1).cgColor ) ] ) .frame(width: 330.0, height: 59.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct BarCenteredZeroLine2: View { var body: some View { Text("Bar centered around zero-line") DSFSparklineBarGraphView.SwiftUI( dataSource: BarDataSource4, graphColor: DSFColor.systemGreen, lineWidth: 1, showZeroLine: true, zeroLineDefinition: DSFSparkline.ZeroLineDefinition( color: DSFColor.systemGray, lineWidth: 1, lineDashStyle: []), centeredAtZeroLine: true, lowerGraphColor: DSFColor.systemPink ) .frame(width: 100.0, height: 59.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct BarDemoContentView: View { var body: some View { ScrollView { VStack { BarBasic() BarZeroLine() BarWithCustomFill() BarCenteredZeroLine() BarCenteredZeroLineColored() BarCenteredZeroLineCustomFill() BarRange() BarNofill() BarRange2() BarCenteredZeroLine2() } } } } struct BarDemoContentView_Previews: PreviewProvider { static var previews: some View { BarDemoContentView() } } fileprivate var BarDataSource1: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: 0 ... 1, zeroLineValue: 0.4) d.push(values: [ 0.85, 0.04, 0.24, 0.13, 0.51, 0.93, 0.26, 0.69, 0.16, 0.39, 0.19, 0.12, 0.28, 0.42, 0.42, 0.48, 0.29, 0.05, 0.87, 0.28 ]) return d }() fileprivate var BarDataSource2: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 8, range: 0 ... 100) d.push(values: [100, 0, 25, 50, 75, 10, 10, 88]) return d }() fileprivate var BarDataSource3: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 21, range: -1 ... 1) var sins: CGFloat = 0.0 let r: Range = 0 ..< 21 let vars: [CGFloat] = r.map { sin(CGFloat($0)) } d.push(values: vars) return d }() fileprivate var BarDataSource4: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 21, range: 0 ... 1) d.zeroLineValue = 0.5 d.push(values: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0]) return d }() ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/BitmapGenerationView.swift ================================================ // // BitmapGenerationView.swift // Demos // // Created by Darren Ford on 25/2/21. // import SwiftUI import DSFSparkline fileprivate let b1: DSFSparklineSurface.Bitmap = { let b = DSFSparklineSurface.Bitmap() let dataSource = DSFSparkline.DataSource(values: [1, 5, 3, 4], range: 0 ... 6) let li = DSFSparklineOverlay.GridLines() li.dataSource = dataSource li.floatValues = [1, 3, 5] li.strokeWidth = 0.5 li.dashStyle = [0.5, 0.5] li.strokeColor = DSFColor.gray.withAlphaComponent(0.3).cgColor b.addOverlay(li) let l = DSFSparklineOverlay.Line() l.primaryStrokeColor = DSFColor.primaryTextColor.cgColor l.primaryFill = DSFSparkline.Fill.Gradient(colors: [ DSFColor.systemRed.cgColor, DSFColor.systemBlue.cgColor, ]) l.dataSource = dataSource b.addOverlay(l) return b }() fileprivate let b200: DSFSparklineSurface.Bitmap = { let b = DSFSparklineSurface.Bitmap() let dataSource = DSFSparkline.DataSource(values: [1, 5, 3, 4], range: 0 ... 6) let li = DSFSparklineOverlay.GridLines() li.dataSource = dataSource li.floatValues = [1, 3, 5] li.strokeWidth = 0.5 li.dashStyle = [0.5, 0.5] li.strokeColor = DSFColor.gray.withAlphaComponent(0.3).cgColor b.addOverlay(li) let l = DSFSparklineOverlay.Line() l.primaryStrokeColor = DSFColor.primaryTextColor.cgColor l.primaryFill = DSFSparkline.Fill.Gradient(colors: [ DSFColor.systemRed.cgColor, DSFColor.systemBlue.cgColor, ], isHorizontal: true) l.dataSource = dataSource b.addOverlay(l) return b }() fileprivate let b2: DSFSparklineSurface.Bitmap = { let b = DSFSparklineSurface.Bitmap() let dataSource = DSFSparkline.DataSource(values: [1, 5, 3, 4], range: 0 ... 6) let l = DSFSparklineOverlay.Line() l.primaryStrokeColor = DSFColor.black.cgColor l.strokeWidth = 1.0 l.dataSource = dataSource b.addOverlay(l) return b }() fileprivate let b3: DSFSparklineSurface.Bitmap = { let b = DSFSparklineSurface.Bitmap() let dataSource = DSFSparkline.DataSource(values: [1, 5, 3, 4]) let l = DSFSparklineOverlay.Line() l.primaryStrokeColor = DSFColor.systemPink.cgColor l.strokeWidth = 1.0 l.shadow = NSShadow(blurRadius: 2.0, offset: CGSize(width: 1, height: -1), color: DSFColor.gray.withAlphaComponent(0.5)) l.markerSize = 6 l.dataSource = dataSource b.addOverlay(l) return b }() fileprivate let b4: DSFSparklineSurface.Bitmap = { let b = DSFSparklineSurface.Bitmap() let dataSource = DSFSparkline.DataSource(values: [1, 5, 3, 4]) let l = DSFSparklineOverlay.Line() l.primaryStrokeColor = DSFColor.systemPink.cgColor l.primaryFill = DSFSparkline.Fill.Color(DSFColor.systemPink.withAlphaComponent(0.3).cgColor) l.strokeWidth = 3.0 l.interpolated = true l.markerSize = 6 l.dataSource = dataSource b.addOverlay(l) return b }() fileprivate let b5: DSFSparklineSurface.Bitmap = { let b = DSFSparklineSurface.Bitmap() let dataSource = DSFSparkline.DataSource(values: [4, 2, 9, 9, 0, 9, 0, 4]) let l = DSFSparklineOverlay.Line() l.primaryStrokeColor = DSFColor.systemIndigo.cgColor l.primaryFill = DSFSparkline.Fill.Color(DSFColor.systemIndigo.withAlphaComponent(0.3).cgColor) l.strokeWidth = 3.0 l.interpolated = true l.markerSize = 6 l.dataSource = dataSource b.addOverlay(l) return b }() fileprivate let b6: DSFSparklineSurface.Bitmap = { let b = DSFSparklineSurface.Bitmap() let dataSource = DSFSparkline.DataSource(values: [4, 2, 9, 9, 0, 9, 0, 4]) let l = DSFSparklineOverlay.Line() l.primaryStrokeColor = DSFColor.systemIndigo.cgColor l.primaryFill = DSFSparkline.Fill.Color(DSFColor.systemIndigo.withAlphaComponent(0.3).cgColor) l.strokeWidth = 3.0 l.interpolated = true l.centeredAtZeroLine = true l.markerSize = 6 dataSource.zeroLineValue = 5 l.dataSource = dataSource l.secondaryStrokeColor = DSFColor.systemTeal.cgColor l.secondaryFill = DSFSparkline.Fill.Color(DSFColor.systemTeal.withAlphaComponent(0.3).cgColor) b.addOverlay(l) return b }() fileprivate let tablet1: DSFSparklineSurface.Bitmap = { let b = DSFSparklineSurface.Bitmap() let l = DSFSparklineOverlay.Tablet() l.winStrokeColor = DSFColor.systemGreen.withAlphaComponent(0.5).cgColor l.winFill = DSFSparkline.Fill.Gradient(colors: [ DSFColor.systemGreen.withAlphaComponent(0.8).cgColor, DSFColor.systemGreen.withAlphaComponent(0.2).cgColor ]) l.lossStrokeColor = DSFColor.systemRed.withAlphaComponent(0.5).cgColor l.lossFill = DSFSparkline.Fill.Gradient(colors: [ DSFColor.systemRed.withAlphaComponent(0.2).cgColor, DSFColor.systemRed.withAlphaComponent(0.6).cgColor ]) let dataSource = DSFSparkline.DataSource( values: [-1, 5, 3, -7, -2, 2, 5, -1, 5, 3, -7, -2, 2, 5, -1, 5, 3, -7, -2, 2, 5]) l.dataSource = dataSource b.addOverlay(l) return b }() fileprivate let percentBar: DSFSparklineSurface.Bitmap = { let bitmap = DSFSparklineSurface.Bitmap() let percentbar = DSFSparklineOverlay.PercentBar(value: 0.42) bitmap.addOverlay(percentbar) // Generate an image with retina scale return bitmap }() fileprivate let wiperGauge: DSFSparklineSurface.Bitmap = { let bitmap = DSFSparklineSurface.Bitmap() let wiperGauge = DSFSparklineOverlay.WiperGauge() wiperGauge.value = 0.35 bitmap.addOverlay(wiperGauge) // Generate an image with retina scale return bitmap }() fileprivate let wiperGauge2: DSFSparklineSurface.Bitmap = { let bitmap = DSFSparklineSurface.Bitmap() let wiperGauge = DSFSparklineOverlay.WiperGauge() wiperGauge.value = 0.8 wiperGauge.valueColor = .init(flatColor: CGColor(srgbRed: 0.3, green: 1, blue: 0.3, alpha: 1)) wiperGauge.valueBackgroundColor = CGColor(srgbRed: 1, green: 0.3, blue: 0.3, alpha: 1) wiperGauge.gaugeUpperArcColor = CGColor(gray: 0, alpha: 1) wiperGauge.gaugePointerColor = CGColor(gray: 0.3, alpha: 1) wiperGauge.gaugeBackgroundColor = CGColor(srgbRed: 1, green: 1, blue: 0.4, alpha: 1) bitmap.addOverlay(wiperGauge) // Generate an image with retina scale return bitmap }() struct BitmapGenerationView: View { #if os(macOS) func makeImage(_ nsImage: NSImage) -> Image { return Image(nsImage: nsImage) } func generate(_ bitmap: DSFSparklineSurface.Bitmap, size: CGSize) -> NSImage { guard let image = bitmap.image(size: size, scale: 2) else { return NSImage(systemSymbolName: "exclamationmark.triangle.fill", accessibilityDescription: nil)! } return image } #else func makeImage(_ uiImage: UIImage) -> Image { return Image(uiImage: uiImage) } func generate(_ bitmap: DSFSparklineSurface.Bitmap, size: CGSize) -> UIImage { guard let image = bitmap.image(size: size, scale: 2) else { return UIImage(systemName: "exclamationmark.triangle.fill")! } return image } #endif var body: some View { VStack { Text("Sparkline images created using DSFSparklineSurface.Bitmap") VStack { makeImage(self.generate(b1, size: CGSize(width: 200, height: 40))) .padding(4) .border(Color.gray) makeImage(self.generate(b200, size: CGSize(width: 200, height: 40))) .padding(4) .border(Color.gray) makeImage(self.generate(b2, size: CGSize(width: 200, height: 40))) .padding(4) .border(Color.gray) } Divider() VStack { makeImage(self.generate(b3, size: CGSize(width: 200, height: 40))) .padding(4) .border(Color.gray) makeImage(self.generate(b4, size: CGSize(width: 200, height: 40))) .padding(4) .border(Color.gray) } Divider() HStack { makeImage(self.generate(b5, size: CGSize(width: 100, height: 30))) .padding(4) .border(Color.gray) makeImage(self.generate(b6, size: CGSize(width: 100, height: 30))) .padding(4) .border(Color.gray) } Divider() makeImage(self.generate(tablet1, size: CGSize(width: 400, height: 20))) .padding(4) .border(Color.gray) Divider() HStack { makeImage(self.generate(percentBar, size: CGSize(width: 200, height: 20))) .padding(4) .border(Color.gray) makeImage(self.generate(wiperGauge, size: CGSize(width: 80, height: 40))) .padding(4) .border(Color.gray) makeImage(self.generate(wiperGauge2, size: CGSize(width: 80, height: 40))) .padding(4) .border(Color.gray) } } } } struct BitmapGenerationView_Previews: PreviewProvider { static var previews: some View { BitmapGenerationView() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/CircularGaugeView.swift ================================================ // // TestingView.swift // Demos // // Created by Darren Ford on 14/4/21. // import SwiftUI import DSFSparkline struct TrackStyle { var color: Color = .black var width: Double = 10 var hasShadow: Bool = false var shadowInner: Bool = false var shadowColor: Color = Color(white: 0, opacity: 0.8) var shadowOffset: CGSize = CGSize(width: 2, height: 2) var shadowBlur: Double = 2 var hasStroke: Bool = false var strokeColor: Color = .black var strokeWidth: Double = 0.5 var shadow: DSFSparkline.Shadow? { if hasShadow { return DSFSparkline.Shadow( blurRadius: shadowBlur, offset: shadowOffset, color: shadowColor.cgColor ?? CGColor.black, isInner: shadowInner ) } return nil } var track: DSFSparklineOverlay.CircularGauge.TrackStyle { DSFSparklineOverlay.CircularGauge.TrackStyle( width: width, fillColor: DSFSparkline.Fill.Color(color.cgColor ?? .black), strokeWidth: strokeWidth, strokeColor: hasStroke ? strokeColor.cgColor : nil, shadow: shadow ) } } struct CircularGaugeView: View { @State var value: Double = 0.25 @State var trackStyle = TrackStyle(color: Color(red: 1, green: 0, blue: 0, opacity: 0.1), width: 40) @State var lineStyle = TrackStyle(color: Color(red: 1, green: 0, blue: 0, opacity: 1), width: 20) var body: some View { //Self._printChanges() return VStack { DSFSparklineCircularGaugeView.SwiftUI( value: value, trackStyle: trackStyle.track, lineStyle: lineStyle.track ) .frame(width: 200, height: 200) Slider(value: $value, in: 0 ... 1) { Text("Value") } #if os(macOS) HStack(spacing: 16) { GroupBox("Track Settings") { TrackSettingsView(style: $trackStyle) .padding(8) } GroupBox("Value Settings") { TrackSettingsView(style: $lineStyle) .padding(8) } } #else HStack(spacing: 8) { TrackSettingsView(style: $trackStyle) TrackSettingsView(style: $lineStyle) } .frame(maxHeight: .infinity) #endif Divider() AnimatedGaugeView() } .padding() } } struct TrackSettingsView: View { @Binding var style: TrackStyle var body: some View { //Self._printChanges() return Form { ColorPicker("Color", selection: $style.color) Slider(value: $style.width, in: 0.1 ... 50) { Text("Width") } Divider() Toggle("Shadow", isOn: $style.hasShadow) Toggle("Inner Shadow", isOn: $style.shadowInner) .disabled(!style.hasShadow) ColorPicker("Shadow Color", selection: $style.shadowColor) .disabled(!style.hasShadow) Slider(value: $style.shadowOffset.width, in: -10 ... 10) { Text("X Offset") } .disabled(!style.hasShadow) Slider(value: $style.shadowOffset.height, in: -10 ... 10) { Text("Y Offset") } .disabled(!style.hasShadow) Slider(value: $style.shadowBlur, in: 0.1 ... 10) { Text("Blur") } .disabled(!style.hasShadow) Divider() Toggle("Stroke", isOn: $style.hasStroke) ColorPicker("Stroke Color", selection: $style.strokeColor) .disabled(!style.hasStroke) Slider(value: $style.strokeWidth, in: 0.1 ... 5) { Text("Stroke Width") } .disabled(!style.hasStroke) } } } struct AnimatedGaugeView: View { @State var r1: Double = 0.25 @State var r1tStyle = TrackStyle(color: Color(red: 0, green: 1, blue: 0, opacity: 0.1), width: 15) @State var r1lStyle = TrackStyle(color: Color(red: 0, green: 1, blue: 0, opacity: 1), width: 7) @State var r2: Double = 0.5 @State var r2tStyle = TrackStyle(color: Color(red: 0, green: 0, blue: 1, opacity: 0.1), width: 15) @State var r2lStyle = TrackStyle(color: Color(red: 0, green: 0, blue: 1, opacity: 1), width: 7) @State var duration: Double = 0.25 var body: some View { //Self._printChanges() return HStack { Slider(value: $duration, in: 0.01 ... 2) { Text("Duration") } Button("Animate!") { r1 = Double.random(in: 0 ... 1) r2 = Double.random(in: 0 ... 1) } DSFSparklineCircularGaugeView.SwiftUI( value: r1, animationStyle: .init(duration: duration, function: .linear), trackStyle: r1tStyle.track, lineStyle: r1lStyle.track ) .frame(width: 50, height: 50) DSFSparklineCircularGaugeView.SwiftUI( value: r2, animationStyle: .init(duration: duration, function: .easeInEaseOut), trackStyle: r2tStyle.track, lineStyle: r2lStyle.track ) .frame(width: 50, height: 50) } } } #Preview("All") { CircularGaugeView() .padding() } #Preview("Settings") { TrackSettingsView(style: .constant(TrackStyle())) .padding() } #Preview("Animated") { AnimatedGaugeView() .padding() } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/CircularProgress.swift ================================================ // // CircularProgressView.swift // Demos // // Created by Darren Ford on 16/2/24. // import SwiftUI import DSFSparkline func namedImage(_ name: String) -> CGImage? { #if os(macOS) if #available(macOS 11.0, *) { return NSImage(systemSymbolName: name, accessibilityDescription: nil)?.cgImage(forProposedRect: nil, context: nil, hints: nil) } return nil #else return UIImage(systemName: name)?.cgImage #endif } struct CircularProgressView: View { let g = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 1.0), CGColor(srgbRed: 0.891, green: 0.000, blue: 0.090, alpha: 1.0), ] ) let g1 = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 1.0), CGColor(srgbRed: 0.601, green: 1.000, blue: 0.009, alpha: 1.0), ] ) let g2 = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 1.0), CGColor(srgbRed: 0.015, green: 0.847, blue: 1.000, alpha: 1.0), ] ) let g3 = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.996, green: 0.759, blue: 0.300, alpha: 1.0), CGColor(srgbRed: 0.996, green: 0.459, blue: 0.000, alpha: 1.0), ] ) let i1: CGImage? = namedImage("arrow.right") let i2: CGImage? = namedImage("arrow.up") let i3: CGImage? = namedImage("arrow.triangle.swap") @State var value: Double = 0.25 @State var padding: Double = 2 @State var trackWidth: Double = 20 @State var showIcons: Bool = false @State var trackColor: CGColor? var body: some View { VStack(spacing: 8) { HStack(spacing: 16) { ZStack { DSFSparklineCircularProgressView.SwiftUI( value: value, fillStyle: g, trackWidth: trackWidth, trackColor: CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 0.1), trackIcon: showIcons ? i1 : nil ) DSFSparklineCircularProgressView.SwiftUI( value: value * 0.8, fillStyle: g1, trackWidth: trackWidth, padding: 1 * (trackWidth + padding), trackColor: CGColor(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 0.1), trackIcon: showIcons ? i2 : nil ) DSFSparklineCircularProgressView.SwiftUI( value: value * 0.6, fillStyle: g2, trackWidth: trackWidth, padding: 2 * (trackWidth + padding), trackColor: CGColor(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 0.1), trackIcon: showIcons ? i3 : nil ) } .frame(width: 200, height: 200) ZStack { DSFSparklineCircularProgressView.SwiftUI( value: value, fillStyle: DSFSparkline.Fill.Color(CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1)), trackWidth: trackWidth, trackColor: CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 0.1), trackIcon: showIcons ? i1 : nil ) DSFSparklineCircularProgressView.SwiftUI( value: value * 0.8, fillStyle: DSFSparkline.Fill.Color(CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1)), trackWidth: trackWidth, padding: 1 * (trackWidth + padding), trackColor: CGColor(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 0.1), trackIcon: showIcons ? i2 : nil ) DSFSparklineCircularProgressView.SwiftUI( value: value * 0.6, fillStyle: DSFSparkline.Fill.Color(CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1)), trackWidth: trackWidth, padding: 2 * (trackWidth + padding), trackColor: CGColor(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 0.1), trackIcon: showIcons ? i3 : nil ) } .frame(width: 200, height: 200) } Form { Slider(value: $value, in: 0 ... 2.5) { Text("Value") } minimumValueLabel: { Text("0") } maximumValueLabel: { Text("2.5") } Slider(value: $trackWidth, in: 5 ... 25) { Text("Track Width") } minimumValueLabel: { Text("5") } maximumValueLabel: { Text("25") } Slider(value: $padding, in: 0 ... 10) { Text("Track padding") } minimumValueLabel: { Text("0") } maximumValueLabel: { Text("10") } Toggle(isOn: $showIcons) { Text("Show icons") } } #if os(iOS) .frame(height: 250) #endif Divider() HStack { DSFSparklineCircularProgressView.SwiftUI( value: 0.2, fillStyle: DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 0, alpha: 1), trackWidth: 5, trackColor: CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 0.1) ) DSFSparklineCircularProgressView.SwiftUI( value: 0.80, fillStyle: DSFSparkline.Fill.Color(srgbRed: 1, green: 1, blue: 0, alpha: 1), trackWidth: 5, trackColor: CGColor(srgbRed: 1.000, green: 1.000, blue: 0, alpha: 0.1) ) DSFSparklineCircularProgressView.SwiftUI( value: 0.35, fillStyle: DSFSparkline.Fill.Color(srgbRed: 0, green: 1, blue: 0, alpha: 1), trackWidth: 5, trackColor: CGColor(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 0.1) ) DSFSparklineCircularProgressView.SwiftUI( value: 0.80, fillStyle: DSFSparkline.Fill.Color(srgbRed: 0, green: 0, blue: 1, alpha: 1), trackWidth: 5, trackColor: CGColor(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 0.1) ) DSFSparklineCircularProgressView.SwiftUI( value: 1.3, fillStyle: DSFSparkline.Fill.Color(gray: 1, alpha: 1), trackWidth: 5 ) DSFSparklineCircularProgressView.SwiftUI( value: 0.65, fillStyle: DSFSparkline.Fill.Color(gray: 0, alpha: 1), trackWidth: 5, trackColor: CGColor(gray: 0, alpha: 0.3) ) DSFSparklineCircularProgressView.SwiftUI( value: 0.65, fillStyle: DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 1), trackWidth: 5, trackColor: CGColor(red: 1, green: 1, blue: 0, alpha: 1) ) } .frame(height: 32) } .padding() } } struct CircularProgressView_Previews: PreviewProvider { static var previews: some View { CircularProgressView() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/ContentView.swift ================================================ // // ContentView.swift // Shared // // Created by Darren Ford on 27/1/21. // import DSFSparkline import SwiftUI struct Item: Identifiable { let id: Int let name: String } struct ContentView: View { let items: [Item] = [ Item(id: 0, name: "ReportView"), Item(id: 1, name: "WinLossTie"), Item(id: 2, name: "Tablet"), Item(id: 3, name: "Line"), Item(id: 4, name: "Stackline"), Item(id: 5, name: "Pie Chart"), Item(id: 6, name: "Databar"), Item(id: 7, name: "Dot"), Item(id: 8, name: "Stripes"), Item(id: 9, name: "Active"), Item(id: 10, name: "Bar"), Item(id: 11, name: "Bitmap Testing"), Item(id: 12, name: "SwiftUI Overlays"), Item(id: 13, name: "Percent Bar"), Item(id: 14, name: "Wiper Gauge"), Item(id: 15, name: "Activity Grid"), Item(id: 16, name: "Circular Progress"), Item(id: 17, name: "Circular Gauge"), Item(id: 99, name: "Testing Harness") ] var body: some View { NavigationView { List(items) { item in NavigationLink(destination: DetailView(item: item)) { Text("\(item.name)") } } //.navigationBarTitle("Dynamic List") } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } struct DetailView: View { let item: Item var body: some View { //Self._printChanges() return ScrollView([.vertical]) { switch item.id { case 0: ReportView() case 1: WinLossCreate() case 2: TabletView(shouldAnimate: true) case 3: LineDemoContentView() case 4: StackLineDemoContentView() case 5: PieGraphDemoView() case 6: DataBarGraphContent() case 7: DotGraphView() case 8: StripesDemoView() case 9: MakeActiveView() case 10: BarDemoContentView() case 11: BitmapGenerationView() case 12: SwiftUIView(shouldAnimate: true) case 13: PercentBarView() case 14: WiperGraphDemoView() case 15: ActivityGridView() case 16: CircularProgressView() case 17: CircularGaugeView() case 99: TestingView() default: fatalError() } } // .padding() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/DataBarGraphContent.swift ================================================ // // DataBarGraphContent.swift // Demos // // Created by Darren Ford on 16/2/21. // import SwiftUI import DSFSparkline fileprivate let firstDataSourceDataBar = DSFSparkline.StaticDataSource([10, 40, 25]) fileprivate let secondDataSourceDataBar = DSFSparkline.StaticDataSource([33, 33, 33]) fileprivate let thirdDataSourceDataBar = DSFSparkline.StaticDataSource([85, 10, 19]) fileprivate let fourthDataSourceDataBar = DSFSparkline.StaticDataSource([3, 4, 5]) fileprivate let palette1 = DSFSparkline.Palette([.systemRed, .systemOrange, .systemYellow]) fileprivate let grays = DSFSparkline.Palette([ DSFColor.init(white: 0.80, alpha: 1), DSFColor.init(white: 0.60, alpha: 1), DSFColor.init(white: 0.40, alpha: 1) ]) struct DataBarGraphContent: View { var body: some View { VStack(spacing: 8) { HStack(spacing: 8) { Text("No stroke").frame(width: 70, alignment: Alignment.trailing) DSFSparklineDataBarGraphView.SwiftUI( dataSource: firstDataSourceDataBar, palette: palette1 ) .frame(width: 96, height: 24) DSFSparklineDataBarGraphView.SwiftUI( dataSource: secondDataSourceDataBar, palette: palette1 ) .frame(width: 96, height: 24) DSFSparklineDataBarGraphView.SwiftUI( dataSource: thirdDataSourceDataBar, palette: palette1 ) .frame(width: 96, height: 24) } HStack(spacing: 8) { Text("Stroke").frame(width: 70, alignment: Alignment.trailing) DSFSparklineDataBarGraphView.SwiftUI( dataSource: firstDataSourceDataBar, strokeColor: DSFColor.black, lineWidth: 1 ) .frame(width: 96, height: 24) DSFSparklineDataBarGraphView.SwiftUI( dataSource: secondDataSourceDataBar, strokeColor: DSFColor.black, lineWidth: 1 ) .frame(width: 96, height: 24) DSFSparklineDataBarGraphView.SwiftUI( dataSource: thirdDataSourceDataBar, strokeColor: DSFColor.black, lineWidth: 1 ) .frame(width: 96, height: 24) } HStack(spacing: 8) { DSFSparklineDataBarGraphView.SwiftUI( dataSource: fourthDataSourceDataBar, palette: grays ) .frame(width: 96, height: 36) DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([CGFloat](repeating: 10.0, count: DSFSparkline.Palette.shared.colors.count)), strokeColor: DSFColor.black.withAlphaComponent(0.5), lineWidth: 1 ) .frame(width: 96, height: 36) DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([CGFloat](repeating: 10.0, count: DSFSparkline.Palette.sharedGrays.colors.count)), palette: .sharedGrays, strokeColor: DSFColor.black.withAlphaComponent(0.5), lineWidth: 1 ) .frame(width: 96, height: 36) DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([5, 2, 4, 8]), strokeColor: DSFColor.gray, lineWidth: 1 ) .frame(width: 96, height: 36) } HStack(spacing: 8) { DSFSparklineDataBarGraphView.SwiftUI( dataSource: fourthDataSourceDataBar, palette: grays, animationStyle: DSFSparkline.AnimationStyle(duration: 1, function: .easeInEaseOut) ) .frame(width: 96, height: 36) DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([CGFloat](repeating: 10.0, count: DSFSparkline.Palette.shared.colors.count)), strokeColor: DSFColor.black.withAlphaComponent(0.5), lineWidth: 1, animationStyle: .init(duration: 1, function: .easeInEaseOut) ) .frame(width: 96, height: 36) DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([CGFloat](repeating: 10.0, count: DSFSparkline.Palette.sharedGrays.colors.count)), palette: .sharedGrays, strokeColor: DSFColor.black.withAlphaComponent(0.5), lineWidth: 1, animationStyle: .init(duration: 1, function: .easeInEaseOut) ) .frame(width: 96, height: 36) DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([5, 2, 4, 8]), strokeColor: DSFColor.gray, lineWidth: 1, animationStyle: .init(duration: 1, function: .easeInEaseOut) ) .frame(width: 96, height: 36) } Text("Data Bar with maximum value defined") VStack{ HStack(spacing: 20) { VStack(spacing: 8) { Text("No Background Color") DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([2, 4, 8]), maximumTotalValue: 20, palette: palette1, lineWidth: 1, animationStyle: .init(duration: 0.5, function: .linear) ) .frame(height: 24) .padding(3) .border(Color.gray.opacity(0.3), width: 0.5) DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([5, 10, 5]), maximumTotalValue: 20, palette: palette1, lineWidth: 1, animationStyle: .init(duration: 0.5, function: .linear) ) .frame(height: 24) .padding(3) .border(Color.gray.opacity(0.3), width: 0.5) DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([1, 3, 5]), maximumTotalValue: 20, palette: palette1, lineWidth: 1, animationStyle: .init(duration: 0.5, function: .linear) ) .frame(height: 24) .padding(3) .border(Color.gray.opacity(0.3), width: 0.5) } .frame(width: 192) VStack(spacing: 8) { Text("Background color defined") DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([3, 3, 5]), maximumTotalValue: 14, palette: palette1, unsetColor: DSFColor.black, strokeColor: DSFColor.black, lineWidth: 1, animationStyle: .init(duration: 0.5, function: .easeInEaseOut) ) .frame(height: 24) .padding(3) .border(Color.gray.opacity(0.3), width: 0.5) DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([12, 2, 5]), maximumTotalValue: 20, palette: palette1, unsetColor: DSFColor.black, strokeColor: DSFColor.black, lineWidth: 1, animationStyle: .init(duration: 0.5, function: .easeInEaseOut) ) .frame(height: 24) .padding(3) .border(Color.gray.opacity(0.3), width: 0.5) DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([1, 10, 5]), maximumTotalValue: 20, palette: palette1, unsetColor: DSFColor.black, strokeColor: DSFColor.black, lineWidth: 1, animationStyle: .init(duration: 0.5, function: .easeInEaseOut) ) .frame(height: 24) .padding(3) .border(Color.gray.opacity(0.3), width: 0.5) } .frame(width: 192) } } } .padding() } } struct DataBarGraphContent_Previews: PreviewProvider { static var previews: some View { DataBarGraphContent() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/DotGraphView.swift ================================================ // // DotGraphView.swift // Demos // // Created by Darren Ford on 16/2/21. // import SwiftUI import DSFSparkline fileprivate var data1: DSFSparkline.DataSource = { let e = DSFSparkline.DataSource(windowSize: 100, range: 0 ... 100) let values: [CGFloat] = (0 ..< 100).map { _ in CGFloat.random(in: 0 ... 100) } e.set(values: values) return e }() fileprivate var animData1: DSFSparkline.DataSource = { let e = DSFSparkline.DataSource(windowSize: 100, range: 0 ... 100) return e }() struct DotStandard: View { var body: some View { VStack { Text("Basic Dot") DSFSparklineDotGraphView.SwiftUI( dataSource: data1, graphColor: DSFColor.systemGreen ) .frame(height: 40) .padding(4) Text("Basic Dot Upside Down with unset color") DSFSparklineDotGraphView.SwiftUI( dataSource: data1, graphColor: DSFColor.systemBlue, unsetGraphColor: DSFColor.gray.withAlphaComponent(0.1), upsideDown: true ) .frame(height: 40) .padding(4) } .frame(width: 400) } } struct DotStandardAnim: View { var body: some View { VStack { Text("Animated Dot") DSFSparklineDotGraphView.SwiftUI( dataSource: animData1, graphColor: DSFColor.systemRed, unsetGraphColor: DSFColor.gray.withAlphaComponent(0.1)) .frame(width: 300, height: 40) .padding(4) .onAppear(perform: { startAnim() }) } } func startAnim() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { animData1.push(value: CGFloat.random(in: 0 ... 100)) self.startAnim() } } } struct DotGraphView: View { var body: some View { VStack { DotStandard() DotStandardAnim() } } } struct DotGraphView_Previews: PreviewProvider { static var previews: some View { DotGraphView() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/Extensions.swift ================================================ // // Extensions.swift // Demos // // Created by Darren Ford on 26/2/21. // import CoreGraphics #if !os(macOS) internal extension CGColor { static var black: CGColor { return CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 1])! } static var clear: CGColor { return CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 0])! } } #endif ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/LineDemoContentView.swift ================================================ // // LineDemoContentView.swift // SwiftUI Demo // // Created by Darren Ford on 27/1/21. // import SwiftUI import DSFSparkline var LineSource1: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: 0 ... 1, zeroLineValue: 0.5) d.push(values: [ 0.72, 0.84, 0.15, 0.16, 0.30, 0.58, 0.95, 0.44, 0.02, 0.27, 0.48, 0.16, 0.15, 0.14, 0.81, 0.53, 0.67, 0.52, 0.07, 0.50 ]) return d }() let GraphColor = DSFColor.gray struct LineDemoBasic: View { var body: some View { VStack { Text("Line") HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, lineWidth: 1, lineShading: false, shadowed: true ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, lineWidth: 1, interpolated: true, lineShading: false ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } } } //struct LineDemoBasic_Previews: PreviewProvider { // static var previews: some View { // LineDemoBasic() // } //} struct LineDemoBasicMarkers: View { var body: some View { VStack { Text("Line with markers") HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, lineWidth: 1, lineShading: false, markerSize: 4 ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, lineWidth: 1, interpolated: true, lineShading: false, markerSize: 4 ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } } } //struct LineDemoBasicMarkers_Previews: PreviewProvider { // static var previews: some View { // LineDemoBasicMarkers() // } //} struct LineDemoMarkersAndShadow: View { var body: some View { VStack { Text("Line with markers and shadow") DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, lineWidth: 1, lineShading: false, shadowed: true, markerSize: 4 ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } } //struct LineDemoMarkersAndShadow_Previews: PreviewProvider { // static var previews: some View { // LineDemoMarkersAndShadow() // } //} struct LineDemoBasicZeroLine: View { var body: some View { VStack { Text("Line with zero-line added") HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, lineWidth: 1, lineShading: false, showZeroLine: true ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, lineWidth: 1, interpolated: true, lineShading: false, showZeroLine: true ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } } } //struct LineDemoBasicZeroLine_Previews: PreviewProvider { // static var previews: some View { // LineDemoBasicZeroLine() // } //} struct LineDemoArea: View { var body: some View { VStack { Text("Area") HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, showZeroLine: true ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, interpolated: true, showZeroLine: true, markerSize: 4 ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } } } //struct LineDemoArea_Previews: PreviewProvider { // static var previews: some View { // LineDemoArea() // } //} struct LineDemoAreaCentered: View { var body: some View { VStack { Text("Line centered") HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: DSFColor.systemGreen, lineWidth: 0.5, showZeroLine: true, centeredAtZeroLine: true, lowerGraphColor: DSFColor(red: 0.7, green: 0, blue: 0, alpha: 1.0) ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: DSFColor.systemGreen, interpolated: true, showZeroLine: true, centeredAtZeroLine: true, lowerGraphColor: DSFColor(red: 0.7, green: 0, blue: 0, alpha: 1.0) ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } } } //struct LineDemoAreaCentered_Previews: PreviewProvider { // static var previews: some View { // LineDemoAreaCentered() // } //} struct LineDemoAreaCenteredMarkers: View { var body: some View { VStack { Text("Line centered with markers") HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: DSFColor.systemGreen, lineWidth: 0.5, showZeroLine: true, centeredAtZeroLine: true, lowerGraphColor: DSFColor(red: 0.7, green: 0, blue: 0, alpha: 1.0), markerSize: 4 ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: DSFColor.systemGreen, interpolated: true, showZeroLine: true, centeredAtZeroLine: true, lowerGraphColor: DSFColor(red: 0.7, green: 0, blue: 0, alpha: 1.0), markerSize: 4 ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } } } //struct LineDemoAreaCenteredMarkers_Previews: PreviewProvider { // static var previews: some View { // LineDemoAreaCenteredMarkers() // } //} struct LineDemoAreaCenteredMarkersNoLowerColor: View { var body: some View { VStack { Text("Line centered with markers, single color") HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: DSFColor.systemGreen, lineWidth: 0.5, showZeroLine: true, centeredAtZeroLine: true, markerSize: 4 ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: DSFColor.systemGreen, interpolated: true, showZeroLine: true, centeredAtZeroLine: true, markerSize: 4 ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } } } //struct LineDemoAreaCenteredMarkersNoLowerColor_Previews: PreviewProvider { // static var previews: some View { // LineDemoAreaCenteredMarkersNoLowerColor() // } //} private let primaryFill = DSFSparkline.Fill.Gradient(colors: [ CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1), CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1), ]) private let secondaryFill = DSFSparkline.Fill.Gradient(colors: [ CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1), CGColor(srgbRed: 0.3, green: 0.5, blue: 0.5, alpha: 1), ]) struct LineDemoLineRanges: View { var body: some View { VStack { Text("Line with gradient fill") HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, lineWidth: 0.5, centeredAtZeroLine: true, primaryFill: primaryFill, secondaryFill: secondaryFill ) DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, lineWidth: 0.5, interpolated: true, centeredAtZeroLine: true, primaryFill: primaryFill, secondaryFill: secondaryFill ) } .frame(height: 80.0) Text("Line with ranges") HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, lineShading: false, highlightDefinitions: [ DSFSparkline.HighlightRangeDefinition( range: 0.7 ..< 1.0, fillColor: DSFColor.red.withAlphaComponent(0.15).cgColor ), DSFSparkline.HighlightRangeDefinition( range: 0.3 ..< 0.7, fillColor: DSFColor.yellow.withAlphaComponent(0.15).cgColor ), DSFSparkline.HighlightRangeDefinition( range: 0.0 ..< 0.3, fillColor: DSFColor.green.withAlphaComponent(0.15).cgColor ) ] ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, interpolated: true, lineShading: false, highlightDefinitions: [ DSFSparkline.HighlightRangeDefinition( range: 0.4 ..< 0.6, fillColor: DSFColor.systemGray.withAlphaComponent(0.3).cgColor ) ] ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } Text("Line with grid lines") HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, lineShading: false, gridLines: .init(values: [0, 0.25, 0.5, 0.75, 1.0]) ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) DSFSparklineLineGraphView.SwiftUI( dataSource: LineSource1, graphColor: GraphColor, interpolated: true, lineShading: false, gridLines: .init(values: [0, 0.4, 0.7, 0.9, 1.0]) ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } } } //struct LineDemoLineRanges2_Previews: PreviewProvider { // static var previews: some View { // LineDemoLineRanges2() // } //} struct LineDemoLineRanges2: View { let c0 = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.736, 0.169, 0.264, 1.000])! let c1 = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.927, 0.393, 0.265, 1.000])! let c2 = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.945, 0.593, 0.257, 1.000])! let c3 = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.950, 0.856, 0.373, 1.000])! let c4 = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.605, 0.815, 0.249, 1.000])! let c5 = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.674, 0.938, 0.561, 1.000])! let source: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 12, range: 0 ... 50) d.push(values: [ 12, 12, 3, 16, 18, 22, 11, 26, 22, 45, 13, 10 ]) return d }() var body: some View { HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: source, graphColor: DSFColor.white, lineWidth: 4, interpolated: true, lineShading: false, shadowed: true, highlightDefinitions: [ DSFSparkline.HighlightRangeDefinition( range: 0 ..< 5, fillColor: c0 ), DSFSparkline.HighlightRangeDefinition( range: 5 ..< 10, fillColor: c1 ), DSFSparkline.HighlightRangeDefinition( range: 10 ..< 15, fillColor: c2 ), DSFSparkline.HighlightRangeDefinition( range: 15 ..< 20, fillColor: c3 ), DSFSparkline.HighlightRangeDefinition( range: 20 ..< 25, fillColor: c4 ), DSFSparkline.HighlightRangeDefinition( range: 25 ..< 50, fillColor: c5 ), ] ) .frame(height: 120.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } } struct LineDemoCustomMarkers: View { let source: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: 0 ... 1, zeroLineValue: 0.5) d.push(values: [ 0.72, 0.84, 0.15, 0.16, 0.30, 0.58, 0.87, 0.44, 0.02, 0.27, 0.48, 0.16, 0.15, 0.14, 0.81, 0.53, 0.67, 0.52, 0.07, 0.50 ]) return d }() var drawCurrentValueMarker: DSFSparklineOverlay.Line.MarkerDrawingBlock = { context, markerFrames in let l = markerFrames.last! context.setFillColor(l.isPositiveValue ? DSFColor.systemGreen.cgColor : DSFColor.systemRed.cgColor) context.fill(l.rect) } var customMarkerDrawing: DSFSparklineOverlay.Line.MarkerDrawingBlock = { context, markerFrames in // Get the frames containing the minimum and maximum values if let minMarker = markerFrames.min(by: { (a, b) -> Bool in a.value < b.value }), let maxMarker = markerFrames.min(by: { (a, b) -> Bool in a.value > b.value }) { // Draw minimum marker context.setFillColor(DSFColor.systemRed.cgColor) context.fill(minMarker.rect) context.setLineWidth(0.5) context.setStrokeColor(DSFColor.white.cgColor) context.stroke(minMarker.rect) // Draw maximum marker context.setFillColor(DSFColor.systemGreen.cgColor) context.fill(maxMarker.rect) context.setLineWidth(0.5) context.setStrokeColor(DSFColor.white.cgColor) context.stroke(maxMarker.rect) } } var body: some View { HStack { VStack { DSFSparklineLineGraphView.SwiftUI( dataSource: source, graphColor: DSFColor.systemGreen, lineWidth: 0.5, showZeroLine: true, centeredAtZeroLine: true, lowerGraphColor: DSFColor(red: 0.7, green: 0, blue: 0, alpha: 1.0), markerSize: 4, markerDrawingBlock: self.drawCurrentValueMarker ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) Text("Current value marker").font(.footnote) } VStack { DSFSparklineLineGraphView.SwiftUI( dataSource: source, graphColor: DSFColor.systemGreen, lineWidth: 0.5, showZeroLine: true, centeredAtZeroLine: true, lowerGraphColor: DSFColor(red: 0.7, green: 0, blue: 0, alpha: 1.0), markerSize: 6, markerDrawingBlock: self.customMarkerDrawing ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) Text("Min/Max marker").font(.footnote) } } .onAppear(perform: { startSomething() }) } func startSomething() { if !IsRunningInPreviewPane { updateWithNewValues() } } func updateWithNewValues() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { source.push(value: CGFloat.random(in: 0.0 ... 1.0)) updateWithNewValues() } } } struct LineGraphShowingBug13: View { let source: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, /*range: 0 ... 1,*/ zeroLineValue: 0) d.push(values: [ 0, 5, 4.5, 10, 8, 0, 60, 60, -60, -60, 60, 60, -60, -60, 60, 55 ]) return d }() let source2: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: 0 ... 1, zeroLineValue: 0.25) d.push(values: [ 0.72, 0.84, 0.15, 0.16, 0.30, 0.58, 0.99, 0.99, 0.02, 0.07, 0.48, 0.16, 0.15, 0.14, 0.81, 0.53, 0.67, 0.52, 0.07, 0.50 ]) return d }() var body: some View { VStack { Text("Demonstrating interpolated line clipping") HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: self.source, graphColor: DSFColor.systemIndigo, lineWidth: 1, lineShading: true, shadowed: true, centeredAtZeroLine: true, lowerGraphColor: DSFColor.systemRed, markerSize: 6, gridLines: .init(values: [-60, -30, 0, 30, 60]) ) .border(Color.gray.opacity(0.2), width: 1) .frame(width: 200, height: 100) DSFSparklineLineGraphView.SwiftUI( dataSource: self.source, graphColor: DSFColor.systemIndigo, lineWidth: 1, interpolated: true, lineShading: true, shadowed: true, markerSize: 6, gridLines: .init(values: [-60, -30, 0, 30, 60]) ) .border(Color.gray.opacity(0.2), width: 1) .frame(width: 200, height: 100) DSFSparklineLineGraphView.SwiftUI( dataSource: self.source, graphColor: DSFColor.systemIndigo, lineWidth: 1, interpolated: true, lineShading: true, shadowed: true, centeredAtZeroLine: true, lowerGraphColor: DSFColor.systemRed, markerSize: 6 ) .border(Color.gray.opacity(0.2), width: 1) .frame(width: 200, height: 100) } HStack { DSFSparklineSurface.SwiftUI([ DSFSparklineOverlay.GridLines( dataSource: self.source2, floatValues: [0, 0.25, 0.5, 0.75, 1.0], strokeColor: CGColor(gray: 0.5, alpha: 0.3), strokeWidth: 0.5, dashStyle: [0.5, 0.5] ), DSFSparklineOverlay.RangeHighlight( dataSource: self.source2, range: 0.75 ..< 1.0, fill: DSFSparkline.Fill.Color(CGColor(red: 1, green: 0, blue: 0, alpha: 0.1)) ), DSFSparklineOverlay.RangeHighlight( dataSource: self.source2, range: 0.5 ..< 0.75, fill: DSFSparkline.Fill.Color(CGColor(red: 1, green: 1, blue: 0, alpha: 0.1)) ), DSFSparklineOverlay.RangeHighlight( dataSource: self.source2, range: 0.0 ..< 0.5, fill: DSFSparkline.Fill.Color(CGColor(red: 0, green: 1, blue: 0, alpha: 0.1)) ), DSFSparklineOverlay.ZeroLine( dataSource: self.source2, strokeColor: CGColor(red: 1, green: 0, blue: 1, alpha: 1), strokeWidth: 1 ), { let d = DSFSparklineOverlay.Line() d.dataSource = self.source2 d.interpolated = true d.primaryStrokeColor = CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1) d.markerSize = 3 d.strokeWidth = 1.0 return d }() ]) .frame(width: 200, height: 50) .border(Color.gray.opacity(0.2), width: 1) } } } } struct LineGraphShowingBug13_Previews: PreviewProvider { static var previews: some View { LineGraphShowingBug13() } } struct LineDemoContentView: View { var body: some View { ScrollView([.vertical, .horizontal]) { VStack(spacing: 8) { VStack { LineDemoBasic() LineDemoBasicMarkers() LineDemoMarkersAndShadow() LineDemoBasicZeroLine() LineDemoArea() } VStack { LineDemoAreaCentered() LineDemoAreaCenteredMarkers() LineDemoAreaCenteredMarkersNoLowerColor() LineDemoLineRanges() LineDemoLineRanges2() } VStack { Text("Custom markers").font(.headline) LineDemoCustomMarkers() } VStack { LineGraphShowingBug13() } } .frame(width: 400.0) } } } struct LineDemoContentView_Previews: PreviewProvider { static var previews: some View { LineDemoContentView() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/PercentBarView.swift ================================================ // // PercentBar.swift // Demos // // Created by Darren Ford on 18/3/21. // import SwiftUI import DSFSparkline struct PercentBarView: View { // The actual line graph var percentBarOverlay: DSFSparklineOverlay.PercentBar = { let lineOverlay = DSFSparklineOverlay.PercentBar(value: 0) return lineOverlay }() var percentBarOverlay2: DSFSparklineOverlay.PercentBar = { let lineOverlay = DSFSparklineOverlay.PercentBar(value: 0) return lineOverlay }() var percentBarOverlay3: DSFSparklineOverlay.PercentBar = { let lineOverlay = DSFSparklineOverlay.PercentBar(value: 0) return lineOverlay }() func style() -> DSFSparkline.PercentBar.Style { let s = DSFSparkline.PercentBar.Style() s.barColor = DSFColor.systemYellow.cgColor s.barTextColor = .black s.underBarColor = DSFColor.systemBlue.cgColor s.underBarTextColor = CGColor(gray: 1.0, alpha: 1.0) s.font = DSFFont(name: "Menlo", size: 13)! return s } func style2() -> DSFSparkline.PercentBar.Style { let s = DSFSparkline.PercentBar.Style() s.barColor = DSFColor.systemIndigo.cgColor s.underBarColor = CGColor(gray: 0.0, alpha: 0.1) s.showLabel = false return s } func style3() -> DSFSparkline.PercentBar.Style { let s = DSFSparkline.PercentBar.Style() s.barEdgeInsets = DSFEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) s.barColor = CGColor(gray: 0.0, alpha: 0.2) s.barTextColor = .black s.underBarColor = CGColor(gray: 0.0, alpha: 0.1) s.underBarTextColor = .black s.font = DSFFont(name: "Menlo", size: 13)! return s } func style4(value: CGFloat) -> DSFSparkline.PercentBar.Style { let s = DSFSparkline.PercentBar.Style() s.barEdgeInsets = DSFEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) if value > 0.8 { s.barColor = DSFColor.systemRed.cgColor s.barTextColor = CGColor(gray: 1.0, alpha: 1.0) } else if value > 0.5 { s.barColor = DSFColor.systemYellow.cgColor s.barTextColor = CGColor(gray: 0.0, alpha: 1.0) } else { s.barColor = DSFColor.systemGreen.cgColor s.barTextColor = CGColor(gray: 0.0, alpha: 1.0) } s.underBarColor = CGColor(gray: 0.8, alpha: 1.0) s.underBarTextColor = .black s.font = DSFFont(name: "Menlo Bold", size: 16)! return s } let fixedFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .percent formatter.minimumFractionDigits = 2 formatter.maximumFractionDigits = 2 return formatter }() func fixedFormatted(_ value: CGFloat) -> String { return self.fixedFormatter.string(for: value) ?? "" } func style10() -> DSFSparkline.PercentBar.Style { let s = DSFSparkline.PercentBar.Style() s.barEdgeInsets = DSFEdgeInsets(top: 1, left: 1, bottom: 1, right: 1) s.barColor = CGColor(gray: 0.0, alpha: 1.0) s.barTextColor = CGColor(gray: 1.0, alpha: 1.0) s.underBarColor = CGColor(gray: 0.8, alpha: 1.0) s.underBarTextColor = .black s.font = DSFFont(name: "Menlo", size: 11)! return s } func style11() -> DSFSparkline.PercentBar.Style { let s = DSFSparkline.PercentBar.Style() s.showLabel = false s.barEdgeInsets = DSFEdgeInsets(top: 1, left: 1, bottom: 1, right: 1) s.barColor = CGColor(gray: 0.0, alpha: 1.0) s.barTextColor = CGColor(gray: 1.0, alpha: 1.0) s.underBarColor = CGColor(gray: 0.8, alpha: 1.0) s.underBarTextColor = .black s.font = DSFFont(name: "Menlo", size: 11)! return s } @State var v1: CGFloat = 0.0 @State var v2: CGFloat = 1.0 @State var v3: CGFloat = 0.85 @State var v4: CGFloat = 0.05 @State var v5: CGFloat = 0.66 @State var v6: CGFloat = 0.5 @State var v10: CGFloat = 0.0 @State var v11: CGFloat = 0.2 @State var v12: CGFloat = 0.4 @State var v13: CGFloat = 0.6 @State var v14: CGFloat = 0.8 @State var v15: CGFloat = 1.0 var body: some View { VStack { VStack { Text("Fixed") .font(.subheadline) DSFSparklinePercentBarGraphView.SwiftUI(style: style(), value: Double(v1)) .frame(height: 20) DSFSparklinePercentBarGraphView.SwiftUI(style: style(), value: Double(0.5)) .frame(height: 20) DSFSparklinePercentBarGraphView.SwiftUI(style: style(), value: Double(v2)) .frame(height: 20) Divider() VStack { let animationStyle = DSFSparkline.AnimationStyle(duration: 0.25) HStack { DSFSparklinePercentBarGraphView.SwiftUI(style: style10(), value: Double(v10), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) DSFSparklinePercentBarGraphView.SwiftUI(style: style10(), value: Double(v11), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) DSFSparklinePercentBarGraphView.SwiftUI(style: style10(), value: Double(v12), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) DSFSparklinePercentBarGraphView.SwiftUI(style: style10(), value: Double(v13), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) DSFSparklinePercentBarGraphView.SwiftUI(style: style10(), value: Double(v14), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) DSFSparklinePercentBarGraphView.SwiftUI(style: style10(), value: Double(v15), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) } HStack { DSFSparklinePercentBarGraphView.SwiftUI(style: style11(), value: Double(v10), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) DSFSparklinePercentBarGraphView.SwiftUI(style: style11(), value: Double(v11), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) DSFSparklinePercentBarGraphView.SwiftUI(style: style11(), value: Double(v12), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) DSFSparklinePercentBarGraphView.SwiftUI(style: style11(), value: Double(v13), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) DSFSparklinePercentBarGraphView.SwiftUI(style: style11(), value: Double(v14), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) DSFSparklinePercentBarGraphView.SwiftUI(style: style11(), value: Double(v15), animationStyle: animationStyle) .frame(width: 50, height: 18, alignment: .center) } Button("Random") { self.v10 = CGFloat(drand48()) self.v11 = CGFloat(drand48()) self.v12 = CGFloat(drand48()) self.v13 = CGFloat(drand48()) self.v14 = CGFloat(drand48()) self.v15 = CGFloat(drand48()) } .frame(height: 18, alignment: .center) } Divider() HStack { DSFSparklinePercentBarGraphView.SwiftUI(style: style2(), value: Double(v1)) .frame(height: 20) Text(fixedFormatted(self.v1)) .frame(width: 75, alignment: .trailing) } HStack { DSFSparklinePercentBarGraphView.SwiftUI(style: style2(), value: Double(0.5)) .frame(height: 20) Text(fixedFormatted(0.5)) .frame(width: 75, alignment: .trailing) } HStack { DSFSparklinePercentBarGraphView.SwiftUI(style: style2(), value: Double(v2)) .frame(height: 20) Text(fixedFormatted(self.v2)) .frame(width: 75, alignment: .trailing) } } .frame(width: 320) Divider() VStack { Text("Interactive") .font(.subheadline) DSFSparklinePercentBarGraphView.SwiftUI(style: style3(), value: Double(v6)) .frame(height: 26) HStack(spacing: 8) { Slider(value: $v6, in: 0 ... 1) Text(fixedFormatted(self.v6)) .frame(width: 75, alignment: .trailing) } } .padding(8) Divider() VStack { Text("Animation") .font(.subheadline) DSFSparklinePercentBarGraphView.SwiftUI(style: style4(value: v3), value: Double(v3), animationStyle: .init(duration: 0.5)) .frame(height: 30) DSFSparklinePercentBarGraphView.SwiftUI(style: style4(value: v4), value: Double(v4), animationStyle: .init(duration: 0.5)) .frame(height: 30) DSFSparklinePercentBarGraphView.SwiftUI(style: style4(value: v5), value: Double(v5), animationStyle: .init(duration: 0.5)) .frame(height: 30) Button("Random") { self.v3 = CGFloat(drand48()) self.v4 = CGFloat(drand48()) self.v5 = CGFloat(drand48()) } } .frame(width: 250) } } } struct PercentBar_Previews: PreviewProvider { static var previews: some View { PercentBarView() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/PieGraphDemoView.swift ================================================ // // PieGraphDemoView.swift // Demos // // Created by Darren Ford on 16/2/21. // import SwiftUI import DSFSparkline fileprivate let palette1 = DSFSparkline.Palette([.systemRed, .systemOrange, .systemYellow]) fileprivate let grays = DSFSparkline.Palette([ DSFColor.init(white: 1.00, alpha: 1), DSFColor.init(white: 0.66, alpha: 1), DSFColor.init(white: 0.33, alpha: 1) ]) fileprivate let firstDataSource = DSFSparkline.StaticDataSource([10, 40, 25]) fileprivate let secondDataSource = DSFSparkline.StaticDataSource([33, 33, 33]) fileprivate let thirdDataSource = DSFSparkline.StaticDataSource( [85, 10, 19]) fileprivate let fourthDataSource = DSFSparkline.StaticDataSource([3, 4, 5]) struct PieGraphDemoView: View { var body: some View { VStack(spacing: 8) { HStack(spacing: 8) { Text("No stroke").frame(width: 70, alignment: Alignment.trailing) DSFSparklinePieGraphView.SwiftUI( dataSource: firstDataSource, palette: palette1 ) .frame(width: 24, height: 24) DSFSparklinePieGraphView.SwiftUI( dataSource: secondDataSource, palette: palette1 ) .frame(width: 24, height: 24) DSFSparklinePieGraphView.SwiftUI( dataSource: thirdDataSource, palette: palette1 ) .frame(width: 24, height: 24) } HStack(spacing: 8) { Text("Stroke").frame(width: 70, alignment: Alignment.trailing) DSFSparklinePieGraphView.SwiftUI( dataSource: firstDataSource, strokeColor: DSFColor.black.withAlphaComponent(0.5), lineWidth: 1 ) .frame(width: 24, height: 24) DSFSparklinePieGraphView.SwiftUI( dataSource: secondDataSource, strokeColor: DSFColor.black.withAlphaComponent(0.5), lineWidth: 1 ) .frame(width: 24, height: 24) DSFSparklinePieGraphView.SwiftUI( dataSource: thirdDataSource, strokeColor: DSFColor.black.withAlphaComponent(0.5), lineWidth: 1 ) .frame(width: 24, height: 24) } HStack(spacing: 8) { DSFSparklinePieGraphView.SwiftUI( dataSource: fourthDataSource, palette: grays ) .frame(width: 36, height: 36) DSFSparklinePieGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([CGFloat](repeating: 10.0, count: DSFSparkline.Palette.shared.colors.count)), strokeColor: DSFColor.black.withAlphaComponent(0.5), lineWidth: 1 ) .frame(width: 36, height: 36) DSFSparklinePieGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([CGFloat](repeating: 10.0, count: DSFSparkline.Palette.sharedGrays.colors.count)), palette: .sharedGrays, strokeColor: DSFColor.black.withAlphaComponent(0.5), lineWidth: 1 ) .frame(width: 36, height: 36) DSFSparklinePieGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([5, 2, 4, 8]), strokeColor: DSFColor.gray, lineWidth: 1 ) .frame(width: 36, height: 36) } HStack(spacing: 8) { DSFSparklinePieGraphView.SwiftUI( dataSource: fourthDataSource, palette: grays, animationStyle: .init(duration: 1.0, function: .easeInEaseOut) ) .frame(width: 36, height: 36) DSFSparklinePieGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([CGFloat](repeating: 10.0, count: DSFSparkline.Palette.shared.colors.count)), strokeColor: DSFColor.black, lineWidth: 2, animationStyle: .init(duration: 1.0, function: .easeInEaseOut) ) .frame(width: 36, height: 36) DSFSparklinePieGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([CGFloat](repeating: 10.0, count: DSFSparkline.Palette.sharedGrays.colors.count)), palette: .sharedGrays, strokeColor: DSFColor.black.withAlphaComponent(0.5), lineWidth: 1, animationStyle: .init(duration: 1.0, function: .easeInEaseOut) ) .frame(width: 36, height: 36) DSFSparklinePieGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource([5, 2, 4, 8]), strokeColor: DSFColor.gray, lineWidth: 1, animationStyle: .init(duration: 1.0, function: .easeInEaseOut) ) .frame(width: 36, height: 36) } } .padding() } } struct PieGraphDemoView_Previews: PreviewProvider { static var previews: some View { PieGraphDemoView() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/ReportView.swift ================================================ // // ContentView.swift // Shared // // Created by Darren Ford on 27/1/21. // import DSFSparkline import SwiftUI struct UserResults { let name: String var values: [Int] let dataSource = DSFSparkline.DataSource(windowSize: 5) init(name: String, values: [Int]) { self.name = name self.values = values self.dataSource.set(values: self.values.map { CGFloat($0) }) } } struct SalesResult { let name: String var values: [CGFloat] var maximumTotal: CGFloat init(name: String, values: [CGFloat], maximumTotal: CGFloat) { self.name = name self.values = values self.maximumTotal = maximumTotal } } struct ReportView: View { static func Make() -> ReportView { return ReportView() } var gridItems1: [GridItem] = [ GridItem(.fixed(35), spacing: 2), GridItem(.fixed(35), spacing: 2), GridItem(.fixed(35), spacing: 2), GridItem(.fixed(35), spacing: 2), GridItem(.fixed(35), spacing: 2), GridItem(.fixed(35), spacing: 2), ] let userItems1: [UserResults] = [ UserResults(name: "Aiden", values: [90, 120, 110, 130, 115]), UserResults(name: "Ethan", values: [100, 95, 115, 120, 118]), UserResults(name: "Jackson", values: [120, 125, 140, 130, 135]), UserResults(name: "Lucas", values: [118, 120, 125, 128, 135]), UserResults(name: "Noah", values: [130, 130, 125, 120, 90]), ] var gridItems2: [GridItem] = [ GridItem(.fixed(35), spacing: 2), GridItem(.fixed(35), spacing: 2), GridItem(.fixed(35), spacing: 2), GridItem(.fixed(35), spacing: 2), GridItem(.fixed(35), spacing: 2), GridItem(.fixed(35), spacing: 2), ] let userItems2: [UserResults] = [ UserResults(name: "Aiden", values: [1, 1, -1, 1, -1, 1, 1, 1]), UserResults(name: "Ethan", values: [1, 1, -1, 1, 1, -1, 1, -1]), UserResults(name: "Jackson", values: [-1, 1, 1, -1, -1, 1, 1, 1]), UserResults(name: "Lucas", values: [1, -1, -1, 1, 1, -1, -1, 1]), UserResults(name: "Noah", values: [-1, 1, 1, 1, -1, 1, 1, 1]), ] let salesItems: [SalesResult] = [ SalesResult(name: "Aiden", values: [120, 200, 0, 270], maximumTotal: 1000), SalesResult(name: "Ethan", values: [300, 400, 100, 90], maximumTotal: 1000), SalesResult(name: "Jackson", values: [100, 140, 90, 110], maximumTotal: 1000), SalesResult(name: "Lucas", values: [250, 250, 100, 100], maximumTotal: 1000), SalesResult(name: "Noah", values: [300, 100, 200, 270], maximumTotal: 1000), ] func sparklineAttributedString() -> NSAttributedString { let source = DSFSparkline.DataSource(values: [4, 1, 8, 7, 5, 9, 3], range: 0 ... 10) let baseColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.033, 0.277, 0.650, 1.000])! let primaryStroke = baseColor // (gray: 0.0, alpha: 0.3)) let primaryFill = DSFSparkline.Fill.Color(baseColor.copy(alpha: 0.3)!) let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let line = DSFSparklineOverlay.Line() // Create a line overlay line.strokeWidth = 1 line.primaryStrokeColor = primaryStroke line.primaryFill = primaryFill line.dataSource = source // Assign the datasource to the overlay bitmap.addOverlay(line) // And add the overlay to the surface. let attr = bitmap.attributedString(size: CGSize(width: 40, height: 18), scale: 2)! let message = NSMutableAttributedString(string: "Inlined - ") message.append(attr) message.append(NSAttributedString(string: " - line graph")) message.addAttributes([.foregroundColor: DSFColor.primaryTextColor], range: NSRange(location: 0, length: message.length)) return message } var body: some View { VStack { ReportHeaderView(attrString: self.sparklineAttributedString()) .padding(EdgeInsets(top: 16, leading: 0, bottom: 25, trailing: 0)) ReportExamResults(gridItems1: self.gridItems1, userItems1: self.userItems1) .frame(maxWidth: .infinity) ReportTeamWinLosses(gridItems2: self.gridItems2, userItems2: self.userItems2) .frame(maxWidth: .infinity) ReportTeamSales(gridItems2: self.gridItems2, salesItems: self.salesItems) .frame(maxWidth: .infinity) } } } struct ReportHeaderView: View { let attrString: NSAttributedString var body: some View { VStack { AttributedText(attrString) .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(20) HStack { Text("NASDAQ Feb 2020 -> Feb 2021").frame(width: 300) DSFSparklineLineGraphView.SwiftUI(dataSource: NasdaqFeb2020Feb2021DataSource, graphColor: DSFColor.systemRed, lineWidth: 0.5) .frame(width: 75, height: 20) } HStack { Text("Gold Feb 2020 -> Feb 2021").frame(width: 300) DSFSparklineLineGraphView.SwiftUI(dataSource: GoldFeb2020Feb2021DataSource, graphColor: DSFColor.systemGreen, lineWidth: 0.5, centeredAtZeroLine: true, lowerGraphColor: DSFColor.systemRed) .frame(width: 75, height: 20) } } } } struct ReportExamResults: View { let gridItems1: [GridItem] let userItems1: [UserResults] var body: some View { VStack { Text("Exam Results") LazyHGrid(rows: gridItems1) { ForEach(0 ..< 5, id: \.self) { number in HStack { Text(userItems1[number].name) .frame(width: 80, alignment: .leading) ForEach(0 ..< 5, id: \.self) { count in Text("\(userItems1[number].values[count])") .frame(width: 30, alignment: .center) } DSFSparklineLineGraphView.SwiftUI( dataSource: userItems1[number].dataSource, graphColor: DSFColor.systemBlue, lineShading: false ) .frame(width: 120) } .padding(8) .background(number % 2 == 0 ? Color(Color.RGBColorSpace.sRGB, red: 0, green: 0, blue: 0, opacity: 0.1) : Color.clear) } } } } } struct ReportTeamWinLosses: View { let gridItems2: [GridItem] let userItems2: [UserResults] var body: some View { VStack { Text("Team Wins/Losses") LazyHGrid(rows: gridItems2) { ForEach(0 ..< 5, id: \.self) { number in HStack { Text(userItems2[number].name) .frame(width: 80, alignment: .leading) ForEach(0 ..< 8, id: \.self) { count in Text("\(userItems2[number].values[count])") .frame(width: 25, alignment: .center) } DSFSparklineWinLossGraphView.SwiftUI( dataSource: userItems2[number].dataSource, barSpacing: 3, showZeroLine: true, zeroLineDefinition: .init(color: .systemGray, lineWidth: 1, lineDashStyle: [1, 1]) ) .frame(width: 120) DSFSparklineTabletGraphView.SwiftUI( dataSource: userItems2[number].dataSource, barSpacing: 2 ) .frame(width: 200, height: 12) } .padding(4) .background(number % 2 == 0 ? Color(Color.RGBColorSpace.sRGB, red: 0, green: 0, blue: 0, opacity: 0.1) : Color.clear) } } } } } struct ReportTeamSales: View { let gridItems2: [GridItem] let salesItems: [SalesResult] var body: some View { VStack { Text("Team Sales") LazyHGrid(rows: gridItems2) { ForEach(0 ..< 5, id: \.self) { number in HStack(spacing: 10) { Text(salesItems[number].name) .frame(width: 80, alignment: .leading) ForEach(0 ..< 4, id: \.self) { count in Text("\(salesItems[number].values[count], specifier: "%.2f")") .frame(width: 60, alignment: .trailing) } DSFSparklineLineGraphView.SwiftUI( dataSource: DSFSparkline.DataSource(values: salesItems[number].values), graphColor: DSFColor.systemBlue, lineShading: false, markerSize: 6 ) .frame(width: 120) DSFSparklinePieGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource(salesItems[number].values) ) .frame(width: 28, height: 28) DSFSparklineDataBarGraphView.SwiftUI( dataSource: DSFSparkline.StaticDataSource(salesItems[number].values), maximumTotalValue: salesItems[number].maximumTotal, unsetColor: DSFColor.black.withAlphaComponent(0.2) ) .frame(width: 120) } .padding(4) .background(number % 2 == 0 ? Color(Color.RGBColorSpace.sRGB, red: 0, green: 0, blue: 0, opacity: 0.1) : Color.clear) } } } } } struct ReportView_Previews: PreviewProvider { static var previews: some View { ReportView() } } let NasdaqFeb2020Feb2021DataSource = DSFSparkline.DataSource(values: NasdaqFeb2020Feb2021) let NasdaqFeb2020Feb2021: [CGFloat] = [ 8965.61, 8980.78, 8566.48, 8567.37, 8952.16, 8684.08, 9018.08, 8738.58, 8575.62, 7950.68, 8344.25, 7952.04, 7201.79, 7874.87, 6904.58, 7334.77, 6989.83, 7150.58, 6879.52, 6860.66, 7417.85, 7384.29, 7797.54, 7502.37, 7774.14, 7700.10, 7360.58, 7487.31, 7373.08, 7913.24, 7887.25, 8090.89, 8153.58, 8192.41, 8515.74, 8393.17, 8532.36, 8650.13, 8560.73, 8263.23, 8495.37, 8494.75, 8634.51, 8730.16, 8607.73, 8914.70, 8889.54, 8604.95, 8710.70, 8809.12, 8854.38, 8979.66, 9121.32, 9192.33, 9002.54, 8863.16, 8943.71, 9014.55, 9234.83, 9185.09, 9375.78, 9284.87, 9324.58, 9340.21, 9412.36, 9368.99, 9489.87, 9552.04, 9608.37, 9682.91, 9615.80, 9814.08, 9924.75, 9953.75, 10020.34, 9492.73, 9588.80, 9726.01, 9895.87, 9910.53, 9943.04, 9946.12, 10056.48, 10131.37, 9909.16, 10017.00, 9757.21, 9874.15, 10058.76, 10154.62, 10207.62, 10433.65, 10343.88, 10492.50, 10547.75, 10617.44, 10390.83, 10488.58, 10550.49, 10473.83, 10503.19, 10767.08, 10680.36, 10706.12, 10461.41, 10363.17, 10536.26, 10402.08, 10542.94, 10587.80, 10745.26, 10902.79, 10941.16, 10998.40, 11108.07, 11010.98, 10968.36, 10782.82, 11012.24, 11042.50, 11019.29, 11129.73, 11210.83, 11146.45, 11264.95, 11311.79, 11379.71, 11466.46, 11665.05, 11625.33, 11695.62, 11775.45, 11939.66, 12056.44, 11458.09, 11313.12, 10847.69, 11141.55, 10919.58, 10853.54, 11056.65, 11190.32, 11050.46, 10910.28, 10793.28, 10778.79, 10963.63, 10632.99, 10672.26, 10913.55, 11117.53, 11085.25, 11167.50, 11326.50, 11075.01, 11332.49, 11154.59, 11364.59, 11420.98, 11579.94, 11876.25, 11863.90, 11768.73, 11713.87, 11671.55, 11478.87, 11516.49, 11484.69, 11506.00, 11548.28, 11358.94, 11431.34, 11004.87, 11185.58, 10911.58, 10957.61, 11160.57, 11590.78, 11890.92, 11895.23, 11713.78, 11553.86, 11786.42, 11709.58, 11829.29, 11924.12, 11899.33, 11801.59, 11904.70, 11854.96, 11880.62, 12036.79, 12094.40, 12205.84, 12198.74, 12355.11, 12349.37, 12377.17, 12464.23, 12519.95, 12582.76, 12338.95, 12405.80, 12377.87, 12440.04, 12595.05, 12658.19, 12764.75, 12755.63, 12742.51, 12807.91, 12771.11, 12804.73, 12899.41, 12850.21, 12870.00, 12888.28, 12698.45, 12818.95, 12740.79, 13067.48, 13201.98, 13036.42, 13072.42, 13128.95, 13112.63, 12998.50, 13197.17, 13457.25, 13530.91, 13543.05, 13635.99, 13626.05, 13270.59, 13337.16, 13070.69, 13403.38, 13612.78, 13610.54, 13777.74, 13856.29, 13987.63, 14007.70, 13972.53, 14025.76, 14095.46, 14047.50, 13965.49, 13865.36, 13874.45, 13533.04, 13465.20, 13597.96, 13119.43, ] let GoldFeb2020Feb2021DataSource: DSFSparkline.DataSource = { let s = DSFSparkline.DataSource(zeroLineValue: 1775.4) s.push(values: GoldFeb2020Feb2021) return s }() let GoldFeb2020Feb2021: [CGFloat] = [ 1646.90, 1640.00, 1640.00, 1564.09, 1592.30, 1642.09, 1641.09, 1666.40, 1670.80, 1674.50, 1659.09, 1641.40, 1589.30, 1515.69, 1485.90, 1524.90, 1477.30, 1478.59, 1484.00, 1567.00, 1660.19, 1632.30, 1650.09, 1623.90, 1622.00, 1583.40, 1578.19, 1625.69, 1633.69, 1677.00, 1664.80, 1665.40, 1736.19, 1744.80, 1756.69, 1727.19, 1720.40, 1689.19, 1701.59, 1678.19, 1728.69, 1733.30, 1723.50, 1711.90, 1710.50, 1703.40, 1684.19, 1694.50, 1706.90, 1704.40, 1684.19, 1721.80, 1709.90, 1695.30, 1704.40, 1713.90, 1738.09, 1753.40, 1731.80, 1744.19, 1750.59, 1720.50, 1734.59, 1704.80, 1710.30, 1713.30, 1736.90, 1737.80, 1725.19, 1697.80, 1718.90, 1676.19, 1698.30, 1714.69, 1713.30, 1732.00, 1729.30, 1720.30, 1729.59, 1729.19, 1724.80, 1745.90, 1756.69, 1772.09, 1765.80, 1762.09, 1772.50, 1774.80, 1793.00, 1773.19, 1784.00, 1788.50, 1804.19, 1815.50, 1799.19, 1798.19, 1811.00, 1810.59, 1811.40, 1798.69, 1808.30, 1815.90, 1842.40, 1864.09, 1889.09, 1897.30, 1931.00, 1944.69, 1953.50, 1942.30, 1962.80, 1966.00, 2001.19, 2031.09, 2051.50, 2010.09, 2024.40, 1932.59, 1934.90, 1956.69, 1937.00, 1985.00, 1999.40, 1958.69, 1933.80, 1934.59, 1927.69, 1911.80, 1940.69, 1921.59, 1964.59, 1967.59, 1968.19, 1934.40, 1927.59, 1923.90, 1933.00, 1944.69, 1954.19, 1937.80, 1953.09, 1956.30, 1960.19, 1940.00, 1952.09, 1901.19, 1898.59, 1859.90, 1868.30, 1857.69, 1872.80, 1894.30, 1887.50, 1908.40, 1900.19, 1912.50, 1901.09, 1883.59, 1888.59, 1919.50, 1922.50, 1888.50, 1901.30, 1903.19, 1900.80, 1906.40, 1910.40, 1924.59, 1901.09, 1902.00, 1902.69, 1908.80, 1876.19, 1865.59, 1877.40, 1890.40, 1908.50, 1894.59, 1945.30, 1950.30, 1853.19, 1875.40, 1860.69, 1872.59, 1885.69, 1887.30, 1884.50, 1873.50, 1861.09, 1872.59, 1837.80, 1804.80, 1805.69, 1775.69, 1814.09, 1825.69, 1836.80, 1835.90, 1861.80, 1870.80, 1834.59, 1833.59, 1839.80, 1828.69, 1852.30, 1856.09, 1887.19, 1885.69, 1879.19, 1866.59, 1874.69, 1877.19, 1879.69, 1891.00, 1893.09, 1944.69, 1952.69, 1906.90, 1912.30, 1834.09, 1849.59, 1842.90, 1853.59, 1850.30, 1829.30, 1839.50, 1865.90, 1865.30, 1855.69, 1854.90, 1850.69, 1844.90, 1837.90, 1847.30, 1860.80, 1830.50, 1832.19, 1788.90, 1810.90, 1831.90, 1835.30, 1840.59, 1824.90, 1821.59, 1797.19, 1771.09, 1773.40, 1775.80, 1806.69, 1804.40, 1796.40, 1770.30, ] ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/StackLineDemoContentView.swift ================================================ // // StackLineDemoContentView.swift // Demos // // Created by Darren Ford on 16/2/21. // import DSFSparkline import SwiftUI private let primaryFill = DSFSparkline.Fill.Gradient(colors: [ CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1), CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1), ]) private let secondaryFill = DSFSparkline.Fill.Gradient(colors: [ CGColor(srgbRed: 1, green: 1, blue: 0, alpha: 1), CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1), ]) struct StackLineBasic: View { var body: some View { Text("Stackline") DSFSparklineStackLineGraphView.SwiftUI( dataSource: UpDataSource1, graphColor: DSFColor.systemGray, lineWidth: 1, shadowed: true ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct StackLineZeroLine: View { var body: some View { Text("Stackline with zero-line") DSFSparklineStackLineGraphView.SwiftUI( dataSource: UpDataSource1, graphColor: DSFColor.systemPink, lineWidth: 1, showZeroLine: true ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct StackLineCenteredZeroLine: View { var body: some View { Text("Stackline centered around zero-line") DSFSparklineStackLineGraphView.SwiftUI( dataSource: UpDataSource1, graphColor: DSFColor.systemBlue, lineWidth: 1, showZeroLine: true, centeredAtZeroLine: true ) .frame(height: 40.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct StackLineCenteredZeroLineColored: View { var body: some View { Text("Stackline centered around zero-line, lower color, grid lines") DSFSparklineStackLineGraphView.SwiftUI( dataSource: UpDataSource1, graphColor: DSFColor.systemGreen, lineWidth: 1, shadowed: true, centeredAtZeroLine: true, lowerGraphColor: DSFColor.systemRed, gridLines: .init(width: 0.5, values: [0, 0.1, 0.2, 0.3, 0.4 ,0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) ) .frame(height: 60.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct StackLineRange: View { var body: some View { Text("Stackline with range") DSFSparklineStackLineGraphView.SwiftUI( dataSource: UpDataSource2, graphColor: DSFColor.systemGreen, lineWidth: 1, showZeroLine: true, highlightDefinitions: [ DSFSparkline.HighlightRangeDefinition( range: 20 ..< 80, fillColor: DSFColor.systemGray.withAlphaComponent(0.2).cgColor ), ] ) .frame(width: 250.0, height: 59.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct StackLineNofill: View { var body: some View { Text("Stackline no fill") DSFSparklineStackLineGraphView.SwiftUI( dataSource: UpDataSource3, graphColor: DSFColor.systemYellow, lineWidth: 3, lineShading: false, shadowed: false ) .frame(width: 330.0, height: 59.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct StackLineRange2: View { var body: some View { Text("StackLine with range") DSFSparklineStackLineGraphView.SwiftUI( dataSource: UpDataSource4, graphColor: DSFColor.systemRed, lineWidth: 0.5, shadowed: false, showZeroLine: true, highlightDefinitions: [ DSFSparkline.HighlightRangeDefinition( range: 0.3 ..< 0.7, fillColor: DSFColor.systemPink.withAlphaComponent(0.1).cgColor ), ] ) .frame(width: 330.0, height: 59.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct StackLineCenteredZeroLine2: View { var body: some View { Text("StackLine centered around zero-line") DSFSparklineStackLineGraphView.SwiftUI( dataSource: UpDataSource4, graphColor: DSFColor.systemGreen, lineWidth: 0.5, shadowed: false, showZeroLine: true, zeroLineDefinition: DSFSparkline.ZeroLineDefinition( color: DSFColor.systemGray, lineWidth: 1, lineDashStyle: [] ), centeredAtZeroLine: true, lowerGraphColor: DSFColor.systemPink ) .frame(width: 100.0, height: 59.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct StackLineCenteredZeroLineCustomFill: View { var body: some View { Text("StackLine centered around zero-line, custom fill") DSFSparklineStackLineGraphView.SwiftUI( dataSource: UpDataSource1, graphColor: DSFColor.systemBlue, lineWidth: 1, showZeroLine: true, centeredAtZeroLine: true, primaryFill: primaryFill, secondaryFill: secondaryFill ) .frame(height: 80.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } struct StackLineDemoContentView: View { var body: some View { ScrollView { VStack { StackLineBasic() StackLineZeroLine() StackLineCenteredZeroLine() StackLineCenteredZeroLineColored() StackLineCenteredZeroLineCustomFill() StackLineRange() StackLineNofill() StackLineRange2() StackLineCenteredZeroLine2() } } } } struct StackLineDemoContentView_Previews: PreviewProvider { static var previews: some View { StackLineDemoContentView() } } private var UpDataSource1: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: 0 ... 1, zeroLineValue: 0.4) // d.push(values: [0.0, 0.3, 0.2, 0.1, 0.8, 0.7, 0.5, 0.6, 0.1, 0.9, 1]) d.push(values: [ 0.85, 0.04, 0.24, 0.13, 0.51, 0.93, 0.26, 0.69, 0.16, 0.39, 0.19, 0.12, 0.28, 0.42, 0.42, 0.48, 0.29, 0.05, 0.87, 0.28, ]) return d }() private var UpDataSource2: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 8, range: 0 ... 100) d.push(values: [100, 0, 25, 50, 75, 10, 10, 88]) return d }() private var UpDataSource3: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 21, range: -1 ... 1) var sins: CGFloat = 0.0 let r: Range = 0 ..< 21 let vars: [CGFloat] = r.map { sin(CGFloat($0)) } d.push(values: vars) return d }() private var UpDataSource4: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 21, range: 0 ... 1) d.zeroLineValue = 0.5 d.push(values: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0]) return d }() ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/StripesDemoView.swift ================================================ // // StripesDemoView.swift // Demos // // Created by Darren Ford on 16/2/21. // import SwiftUI import DSFSparkline struct StripesDemoView: View { let gradient = DSFSparkline.GradientBucket( posts: [ DSFSparkline.GradientBucket.Post(color: CGColor(red: 0, green: 0, blue: 1, alpha: 1), location: 0), DSFSparkline.GradientBucket.Post(color: CGColor(red: 1, green: 1, blue: 1, alpha: 1), location: 0.5), DSFSparkline.GradientBucket.Post(color: CGColor(red: 1, green: 0, blue: 0, alpha: 1), location: 1) ] ) let gradientBucketed = DSFSparkline.GradientBucket( posts: [ DSFSparkline.GradientBucket.Post(color: CGColor(red: 0, green: 0, blue: 1, alpha: 1), location: 0), DSFSparkline.GradientBucket.Post(color: CGColor(red: 1, green: 1, blue: 1, alpha: 1), location: 0.5), DSFSparkline.GradientBucket.Post(color: CGColor(red: 1, green: 0, blue: 0, alpha: 1), location: 1) ], bucketCount: 6 ) let gradient2: DSFSparkline.GradientBucket = { let g = DSFSparkline.GradientBucket(posts: [ DSFSparkline.GradientBucket.Post(color: DSFColor.systemYellow.cgColor, location: 0), DSFSparkline.GradientBucket.Post(r: 0.3, g: 0, b: 0.3, location: 1.0) ]) g.bucketCount = 4 return g }() let gradient3: DSFSparkline.GradientBucket = { let g = DSFSparkline.GradientBucket(posts: [ DSFSparkline.GradientBucket.Post(color: DSFColor.systemYellow.cgColor, location: 0), DSFSparkline.GradientBucket.Post(r: 0.3, g: 0, b: 0.3, location: 1.0) ]) g.bucketCount = 5 return g }() let gradient4: DSFSparkline.GradientBucket = { let g = DSFSparkline.GradientBucket(posts: [ DSFSparkline.GradientBucket.Post(r: 0.0, g: 0.0, b: 0.0, location: 0), DSFSparkline.GradientBucket.Post(r: 1.0, g: 1.0, b: 1.0, location: 1.0) ]) g.bucketCount = 8 return g }() var body: some View { VStack { VStack { Text("Global annual mean temperature anomaly") DSFSparklineStripesGraphView.SwiftUI(dataSource: WorldDataSource, barSpacing: 1, gradient: self.gradient) .frame(height: 30) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } VStack { Text("Global annual mean temperature anomaly (6 color buckets)") DSFSparklineStripesGraphView.SwiftUI(dataSource: WorldDataSource, barSpacing: 1, gradient: self.gradientBucketed) .frame(height: 30) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } DSFSparklineLineGraphView.SwiftUI(dataSource: WorldDataSource, graphColor: DSFColor.systemTeal, lineShading: false, showZeroLine: true) .frame(height: 30) .padding(5) .border(Color.gray.opacity(0.2), width: 1) VStack { Text("Global annual mean temperature anomaly overlaid") ZStack { DSFSparklineStripesGraphView.SwiftUI(dataSource: WorldDataSource, gradient: self.gradient) DSFSparklineLineGraphView.SwiftUI(dataSource: WorldDataSource, graphColor: DSFColor.black, lineWidth: 1.5, interpolated: true, lineShading: true) } .frame(height: 40) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } VStack { Text("Global annual mean temperature anomaly overlaid (6 color buckets)") ZStack { DSFSparklineStripesGraphView.SwiftUI(dataSource: WorldDataSource, gradient: self.gradientBucketed) DSFSparklineLineGraphView.SwiftUI(dataSource: WorldDataSource, graphColor: DSFColor.black, lineWidth: 1, interpolated: true, lineShading: true) } .frame(height: 40) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } Text("Australian annual mean temperature anomaly") VStack { HStack { Text("integral").frame(width: 70, alignment: Alignment.trailing) DSFSparklineStripesGraphView.SwiftUI(dataSource: australianAnomaly, integral: true, barSpacing: 2, gradient: self.gradient2) .frame(height: 25) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } HStack { Text("integral").frame(width: 70, alignment: Alignment.trailing) DSFSparklineStripesGraphView.SwiftUI(dataSource: australianAnomaly, integral: true, barSpacing: 0, gradient: self.gradient2) .frame(height: 25) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } HStack { Text("fractional").frame(width: 70, alignment: Alignment.trailing) DSFSparklineStripesGraphView.SwiftUI(dataSource: australianAnomaly, barSpacing: 1, gradient: self.gradient2) .frame(height: 25) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } HStack { Text("fractional").frame(width: 70, alignment: Alignment.trailing) DSFSparklineStripesGraphView.SwiftUI(dataSource: australianAnomaly, gradient: self.gradient2) .frame(height: 25) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } VStack { HStack { VStack { DSFSparklineStripesGraphView.SwiftUI(dataSource: GradientTestDataSource, gradient: self.gradient3) .frame(height: 25) .padding(5) .border(Color.gray.opacity(0.2), width: 1) DSFSparklineBarGraphView.SwiftUI(dataSource: GradientTestDataSource, graphColor: DSFColor(red: 0.5, green: 0, blue: 0.5, alpha: 1.0), centeredAtZeroLine: true, lowerGraphColor: DSFColor.systemYellow) .frame(height: 50) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } VStack { DSFSparklineStripesGraphView.SwiftUI(dataSource: GradientTestDataSource2, barSpacing: 1, gradient: self.gradient4) .frame(height: 50) .padding(5) .border(Color.gray.opacity(0.2), width: 1) } } } } .padding(4) // .frame(width: 400) } } struct StripesDemoView_Previews: PreviewProvider { static var previews: some View { StripesDemoView() } } // MARK: - fileprivate var GradientTestDataSource: DSFSparkline.DataSource = { let e = DSFSparkline.DataSource() e.set(values: [-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) return e }() fileprivate var GradientTestDataSource2: DSFSparkline.DataSource = { let e = DSFSparkline.DataSource() e.set(values: [-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) return e }() // MARK: - World definition let WorldDataSource: DSFSparkline.DataSource = { let e = DSFSparkline.DataSource(windowSize: UInt(WorldRawData.count)) e.set(values: WorldRawData) return e }() // https://www.metoffice.gov.uk/hadobs/hadcrut4/data/current/download.html // https://www.metoffice.gov.uk/hadobs/hadcrut4/data/current/time_series/HadCRUT.4.6.0.0.annual_ns_avg.txt // Data format :- https://www.metoffice.gov.uk/hadobs/hadcrut4/data/current/series_format.html let WorldRawData: [CGFloat] = [ -0.373, -0.218, -0.228, -0.269, -0.248, -0.272, -0.358, -0.461, -0.467, -0.284, -0.343, -0.407, -0.524, -0.278, -0.494, -0.279, -0.251, -0.321, -0.238, -0.262, -0.276, -0.335, -0.227, -0.304, -0.368, -0.395, -0.384, -0.075, 0.035, -0.230, -0.227, -0.200, -0.213, -0.296, -0.409, -0.389, -0.367, -0.418, -0.307, -0.171, -0.416, -0.330, -0.455, -0.473, -0.410, -0.390, -0.186, -0.206, -0.412, -0.289, -0.203, -0.259, -0.402, -0.479, -0.520, -0.377, -0.283, -0.465, -0.511, -0.522, -0.490, -0.544, -0.437, -0.424, -0.244, -0.141, -0.383, -0.468, -0.333, -0.275, -0.247, -0.187, -0.302, -0.276, -0.294, -0.215, -0.108, -0.210, -0.206, -0.350, -0.137, -0.087, -0.137, -0.273, -0.131, -0.178, -0.147, -0.026, -0.006, -0.052, 0.014, 0.020, -0.027, -0.004, 0.144, 0.025, -0.071, -0.038, -0.039, -0.074, -0.173, -0.052, 0.028, 0.097, -0.129, -0.190, -0.267, -0.007, 0.046, 0.017, -0.049, 0.038, 0.014, 0.048, -0.223, -0.140, -0.068, -0.074, -0.113, 0.032, -0.027, -0.186, -0.065, 0.062, -0.214, -0.149, -0.241, 0.047, -0.062, 0.057, 0.092, 0.140, 0.011, 0.194, -0.014, -0.030, 0.045, 0.192, 0.198, 0.118, 0.296, 0.254, 0.105, 0.148, 0.208, 0.325, 0.183, 0.390, 0.539, 0.306, 0.294, 0.441, 0.496, 0.505, 0.447, 0.545, 0.506, 0.491, 0.395, 0.506, 0.560, 0.425, 0.470, 0.514, 0.579, 0.763, 0.797, 0.677, 0.597, 0.736, 0.768 ] // MARK: - Australia Mean Temp Deviation fileprivate var australianAnomaly: DSFSparkline.DataSource = { let e = DSFSparkline.DataSource(windowSize: UInt(AustraliaMeanTempDeviation.count)) e.set(values: AustraliaMeanTempDeviation) return e }() // http://www.bom.gov.au/climate/change/#tabs=Tracker&tracker=timeseries&tQ=graph%3Dtmean%26area%3Daus%26season%3D0112 let AustraliaMeanTempDeviation: [CGFloat] = [ -0.50, -0.68, -0.20, -0.87, 0.12, 0.07, -0.57, -1.24, -0.54, -0.15, -0.53, -0.23, -0.47, -0.38, -0.69, -0.77, -0.17, -0.51, 0.16, -0.87, -0.24, -0.59, -0.42, -0.45, -0.36, -0.50, -0.14, -0.36, 0.19, -0.62, -0.24, -0.55, 0.08, -0.62, -0.40, -0.29, -0.73, -0.25, -0.45, -0.94, -0.61, -0.43, -0.43, -0.45, -0.36, -0.32, -0.92, 0.04, 0.14, 0.24, -0.66, 0.05, -0.11, -0.13, -0.22, 0.25, -0.50, -0.22, -0.39, -0.03, -0.10, -0.22, 0.15, 0.54, -0.70, -0.22, -0.75, -0.04, -0.31, 0.37, 0.74, 0.27, -0.04, 0.33, -0.38, 0.21, 0.22, 0.17, 0.73, -0.02, 0.47, 0.60, 0.12, 0.31, 0.18, 0.16, 0.60, 0.30, 0.97, 0.32, -0.04, 0.05, 0.71, 0.69, 0.54, 1.16, 0.50, 0.76, 0.45, 0.93, 0.33, -0.00, 0.24, 1.33, 1.04, 0.94, 0.99, 1.06, 1.12, 1.52, 1.15 ] ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/SwiftUI-Overlays/OverlayView.swift ================================================ // // OverlayView.swift // Demos // // Created by Darren Ford on 17/2/21. // import SwiftUI import DSFSparkline extension DSFColor { static var primaryTextColor: DSFColor { #if os(macOS) return NSColor.textColor #else return UIColor.label #endif } } fileprivate var dataSource1: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: -55 ... 55, zeroLineValue: 0) d.set(values: [ 18, -5, 11, 12, -21, 48, 41, -19, -28, 3, 28, -27, -21, -45, -48, -39, 33, -4, 35, 37] ) return d }() struct OverlayView: View { var barGraph: DSFSparklineOverlay = { let b = DSFSparklineOverlay.Bar() b.dataSource = dataSource1 b.strokeWidth = 2 b.primaryStrokeColor = DSFColor.systemPink.cgColor b.primaryFill = DSFSparkline.Fill.Color(DSFColor.systemPink.withAlphaComponent(0.5).cgColor) return b }() var lineGraph: DSFSparklineOverlay = { let l = DSFSparklineOverlay.Line() l.dataSource = dataSource1 l.strokeWidth = 1 l.interpolated = true l.primaryStrokeColor = DSFColor.primaryTextColor.cgColor l.primaryFill = DSFSparkline.Fill.Color(DSFColor.primaryTextColor.withAlphaComponent(0.7).cgColor) return l }() var barGraph2: DSFSparklineOverlay = { let b = DSFSparklineOverlay.Bar() b.dataSource = dataSource1 b.strokeWidth = 2 b.primaryStrokeColor = DSFColor.systemPink.cgColor b.primaryFill = DSFSparkline.Fill.Color(DSFColor.systemPink.withAlphaComponent(0.5).cgColor) return b }() var lineGraph2: DSFSparklineOverlay = { let l = DSFSparklineOverlay.Line() l.dataSource = dataSource1 l.strokeWidth = 1 l.interpolated = true l.primaryStrokeColor = DSFColor.primaryTextColor.cgColor l.primaryFill = DSFSparkline.Fill.Color(DSFColor.primaryTextColor.withAlphaComponent(0.7).cgColor) return l }() var body: some View { VStack { Text("Overlay two sparklines using the same data") .font(.headline) HStack { VStack { DSFSparklineSurface.SwiftUI([ barGraph, // bar on the bottom lineGraph // line on the top ]) .frame(height: 40) Text("Line on top").font(.caption) } VStack { DSFSparklineSurface.SwiftUI([ lineGraph2, // line on the bottom barGraph2 // bar on the top ]) .frame(height: 40) Text("Bar on top").font(.caption) } } } .frame(width: 500) .padding() } } struct OverlayView_Previews: PreviewProvider { static var previews: some View { OverlayView() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/SwiftUI-Overlays/StripesOverlaidView.swift ================================================ // // StripesOverlaidView.swift // Demos // // Created by Darren Ford on 28/2/21. // import SwiftUI import DSFSparkline struct StripesOverlaidView: View { static let palette = DSFSparkline.GradientBucket(posts: [ DSFSparkline.GradientBucket.Post(color: DSFColor.systemGreen.cgColor, location: 0), DSFSparkline.GradientBucket.Post(color: DSFColor.systemGreen.cgColor, location: 0.1), DSFSparkline.GradientBucket.Post(color: DSFColor.systemYellow.cgColor, location: 0.5), DSFSparkline.GradientBucket.Post(color: DSFColor.systemRed.cgColor, location: 0.9), DSFSparkline.GradientBucket.Post(color: DSFColor.systemRed.cgColor, location: 1.0) ]) var stripesOverlay: DSFSparklineOverlay = { let s = DSFSparklineOverlay.Stripes() s.dataSource = WorldDataSource s.integral = false s.gradient = StripesOverlaidView.palette return s }() var lineOverlay: DSFSparklineOverlay = { let l = DSFSparklineOverlay.Line() l.dataSource = WorldDataSource l.strokeWidth = 1 l.interpolated = true l.primaryStrokeColor = DSFColor.black.cgColor l.primaryFill = DSFSparkline.Fill.Color(DSFColor.black.withAlphaComponent(0.3).cgColor) return l }() var body: some View { VStack { Text("Overlay two sparklines using the same data") .font(.headline) DSFSparklineSurface.SwiftUI([ stripesOverlay, lineOverlay ]) .frame(height: 40) } } } struct StripesOverlaidView_Previews: PreviewProvider { static var previews: some View { StripesOverlaidView() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/SwiftUI-Overlays/SuperCoolLineSpark.swift ================================================ // // SuperCoolLineSpark.swift // Demos // // Created by Darren Ford on 1/3/21. // import SwiftUI import DSFSparkline fileprivate let SwiftUIDemoDataSource: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: 0 ... 1, zeroLineValue: 0.5) d.push(values: [ 0.72, 0.84, 0.15, 0.16, 0.30, 0.58, 0.87, 0.44, 0.02, 0.27, 0.48, 0.16, 0.15, 0.14, 0.81, 0.53, 0.67, 0.52, 0.07, 0.50 ]) return d }() struct SuperCoolLineSpark: View { // The overlay representing the zero-line for the data source var zeroOverlay: DSFSparklineOverlay = { let zeroLine = DSFSparklineOverlay.ZeroLine() zeroLine.dataSource = SwiftUIDemoDataSource zeroLine.dashStyle = [] return zeroLine }() // The overlay to draw a highlight between range 0 ..< 0.2 var rangeOverlay: DSFSparklineOverlay = { let highlight = DSFSparklineOverlay.RangeHighlight() highlight.dataSource = SwiftUIDemoDataSource highlight.highlightRange = 0.0 ..< 0.2 highlight.fill = DSFSparkline.Fill.Color(DSFColor.gray.withAlphaComponent(0.4).cgColor) return highlight }() // The actual line graph var lineOverlay: DSFSparklineOverlay = { let lineOverlay = DSFSparklineOverlay.Line() lineOverlay.dataSource = SwiftUIDemoDataSource lineOverlay.primaryStrokeColor = DSFColor.systemBlue.cgColor lineOverlay.primaryFill = DSFSparkline.Fill.Color(DSFColor.systemBlue.withAlphaComponent(0.3).cgColor) lineOverlay.secondaryStrokeColor = DSFColor.systemYellow.cgColor lineOverlay.secondaryFill = DSFSparkline.Fill.Color(DSFColor.systemYellow.withAlphaComponent(0.3).cgColor) lineOverlay.strokeWidth = 1 lineOverlay.markerSize = 4 lineOverlay.centeredAtZeroLine = true return lineOverlay }() var body: some View { DSFSparklineSurface.SwiftUI([ rangeOverlay, // range highlight overlay zeroOverlay, // zero-line overlay lineOverlay, // line graph overlay ]) .frame(width: 150, height: 40) } } struct SuperCoolLineSpark_Previews: PreviewProvider { static var previews: some View { SuperCoolLineSpark() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/SwiftUI-Overlays/SwiftUIContentView.swift ================================================ // // SwiftUIView.swift // Demos // // Created by Darren Ford on 26/2/21. // import DSFSparkline import SwiftUI // A shared data source private var lineOverlayDataSource: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: -0.1 ... 1.1, zeroLineValue: 0.5) d.push(values: [ 1.00, 1.00, 0.44, 0.16, 0.30, 0.58, 0.87, 0.44, 0.00, 0.00, 0.38, 0.16, 0.15, 0.14, 0.81, 0.53, 0.67, 0.52, 0.07, 0.50, ]) return d }() struct SwiftUIContentView_LineOnly: View { // The actual line graph fileprivate let lineOverlay1: DSFSparklineOverlay = { let lineOverlay = DSFSparklineOverlay.Line() lineOverlay.dataSource = lineOverlayDataSource lineOverlay.primaryStrokeColor = DSFColor.systemGreen.cgColor lineOverlay.primaryFill = DSFSparkline.Fill.Color(DSFColor.systemGreen.withAlphaComponent(0.3).cgColor) lineOverlay.secondaryStrokeColor = DSFColor.systemRed.cgColor lineOverlay.secondaryFill = DSFSparkline.Fill.Color(DSFColor.systemRed.withAlphaComponent(0.3).cgColor) lineOverlay.strokeWidth = 2 lineOverlay.markerSize = 6 lineOverlay.centeredAtZeroLine = true return lineOverlay }() var body: some View { DSFSparklineSurface.SwiftUI([ lineOverlay1, // overlay 1 - line graph ]) } } struct SwiftUIContentView_LineOnly_Previews: PreviewProvider { static var previews: some View { SwiftUIContentView_LineOnly() } } ////// struct SwiftUIContentView_LineZeroLine: View { // The actual line graph fileprivate let lineOverlay2: DSFSparklineOverlay = { let lineOverlay = DSFSparklineOverlay.Line() lineOverlay.dataSource = lineOverlayDataSource lineOverlay.primaryStrokeColor = DSFColor.systemGreen.cgColor lineOverlay.primaryFill = DSFSparkline.Fill.Color(DSFColor.systemGreen.withAlphaComponent(0.3).cgColor) lineOverlay.secondaryStrokeColor = DSFColor.systemRed.cgColor lineOverlay.secondaryFill = DSFSparkline.Fill.Color(DSFColor.systemRed.withAlphaComponent(0.3).cgColor) lineOverlay.strokeWidth = 2 lineOverlay.markerSize = 6 lineOverlay.centeredAtZeroLine = true return lineOverlay }() // The overlay representing the zero-line for the graph fileprivate let lineZeroOverlay2: DSFSparklineOverlay = { let zeroLine = DSFSparklineOverlay.ZeroLine() zeroLine.dataSource = LineSource1 zeroLine.dashStyle = [] return zeroLine }() var body: some View { DSFSparklineSurface.SwiftUI([ lineZeroOverlay2, // overlay 1 - zero-line lineOverlay2, // overlay 2 - line graph ]) } } struct SwiftUIContentView_LineZeroLine_Previews: PreviewProvider { static var previews: some View { SwiftUIContentView_LineZeroLine() } } ////// struct SwiftUIContentView_LineZeroLineGrid: View { // The actual line graph fileprivate let lineOverlay3: DSFSparklineOverlay = { let lineOverlay = DSFSparklineOverlay.Line() lineOverlay.dataSource = lineOverlayDataSource lineOverlay.primaryStrokeColor = DSFColor.systemGreen.cgColor lineOverlay.primaryFill = DSFSparkline.Fill.Color(DSFColor.systemGreen.withAlphaComponent(0.3).cgColor) lineOverlay.secondaryStrokeColor = DSFColor.systemRed.cgColor lineOverlay.secondaryFill = DSFSparkline.Fill.Color(DSFColor.systemRed.withAlphaComponent(0.3).cgColor) lineOverlay.strokeWidth = 2 lineOverlay.markerSize = 6 lineOverlay.centeredAtZeroLine = true return lineOverlay }() // The overlay representing the zero-line for the graph fileprivate let lineZeroOverlay3: DSFSparklineOverlay = { let zeroLine = DSFSparklineOverlay.ZeroLine() zeroLine.dataSource = LineSource1 zeroLine.dashStyle = [] return zeroLine }() fileprivate let gridOverlay3: DSFSparklineOverlay = { let grid = DSFSparklineOverlay.GridLines() grid.dataSource = lineOverlayDataSource grid.floatValues = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0] grid.strokeColor = DSFColor.systemGray.withAlphaComponent(0.3).cgColor grid.strokeWidth = 0.5 grid.dashStyle = [1, 1] return grid }() var body: some View { DSFSparklineSurface.SwiftUI([ gridOverlay3, // overlay 1 - grid lines lineZeroOverlay3, // overlay 2 - zero-line lineOverlay3, // overlay 3 - line graph ]) } } struct SwiftUIContentView_LineZeroLineGrid_Previews: PreviewProvider { static var previews: some View { SwiftUIContentView_LineZeroLineGrid() } } struct SwiftUIContentView_LineZeroLineGridRanges: View { // The actual line graph fileprivate let lineOverlay4: DSFSparklineOverlay = { let lineOverlay = DSFSparklineOverlay.Line() lineOverlay.dataSource = lineOverlayDataSource lineOverlay.primaryStrokeColor = DSFColor.systemGreen.cgColor lineOverlay.primaryFill = DSFSparkline.Fill.Color(DSFColor.systemGreen.withAlphaComponent(0.3).cgColor) lineOverlay.secondaryStrokeColor = DSFColor.systemRed.cgColor lineOverlay.secondaryFill = DSFSparkline.Fill.Color(DSFColor.systemRed.withAlphaComponent(0.3).cgColor) lineOverlay.strokeWidth = 2 lineOverlay.markerSize = 6 lineOverlay.centeredAtZeroLine = true return lineOverlay }() // The overlay representing the zero-line for the graph fileprivate let lineZeroOverlay4: DSFSparklineOverlay = { let zeroLine = DSFSparklineOverlay.ZeroLine() zeroLine.dataSource = LineSource1 zeroLine.dashStyle = [] return zeroLine }() fileprivate let gridOverlay4: DSFSparklineOverlay = { let grid = DSFSparklineOverlay.GridLines() grid.dataSource = lineOverlayDataSource grid.floatValues = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0] grid.strokeColor = DSFColor.systemGray.withAlphaComponent(0.3).cgColor grid.strokeWidth = 0.5 grid.dashStyle = [1, 1] return grid }() fileprivate let rangeOverlay4: DSFSparklineOverlay = { let highlight = DSFSparklineOverlay.RangeHighlight() highlight.dataSource = lineOverlayDataSource highlight.highlightRange = 0.0 ..< 0.2 highlight.fill = DSFSparkline.Fill.Color(DSFColor.systemRed.withAlphaComponent(0.1).cgColor) return highlight }() fileprivate let rangeOverlay5: DSFSparklineOverlay = { let highlight = DSFSparklineOverlay.RangeHighlight() highlight.dataSource = lineOverlayDataSource highlight.highlightRange = 0.8 ..< 1.0 highlight.fill = DSFSparkline.Fill.Color(DSFColor.systemGreen.withAlphaComponent(0.1).cgColor) return highlight }() var body: some View { DSFSparklineSurface.SwiftUI([ rangeOverlay4, // overlay 1 - lower highlight rangeOverlay5, // overlay 2 - upper highlight lineZeroOverlay4, // overlay 3 - zero-line gridOverlay4, // overlay 4 - grid lineOverlay4, // overlay 5 - line graph ]) } } struct SwiftUIContentView_LineZeroLineGridRanges_Previews: PreviewProvider { static var previews: some View { SwiftUIContentView_LineZeroLineGridRanges() } } ///// struct SwiftUILineGraphContentView: View { let shouldAnimate: Bool fileprivate let animator = Animator() var body: some View { VStack { Text("A SwiftUI view demonstrating overlays") .font(.headline) HStack { VStack { SwiftUIContentView_LineOnly() .frame(width: 150, height: 50) .padding(4) .border(Color.gray.opacity(0.3)) Text("Line only").font(.footnote) } VStack { SwiftUIContentView_LineZeroLine() .frame(width: 150, height: 50) .padding(4) .border(Color.gray.opacity(0.3)) Text("Line, zero-line").font(.footnote) } VStack { SwiftUIContentView_LineZeroLineGrid() .frame(width: 150, height: 50) .padding(4) .border(Color.gray.opacity(0.3)) Text("Line, zero-line, grid").font(.footnote) } VStack { SwiftUIContentView_LineZeroLineGridRanges() .frame(width: 150, height: 50) .padding(4) .border(Color.gray.opacity(0.3)) Text("Line, zero-line, grid, ranges").font(.footnote) } } } .onAppear() { if shouldAnimate { self.animator.updateWithNewValues() } } } } fileprivate class Animator { func updateWithNewValues() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in guard let `self` = self else { return } // push a new value into the graph's data source let cr2 = CGFloat.random(in: lineOverlayDataSource.range!) // -1 ... 1) _ = lineOverlayDataSource.push(value: cr2) self.updateWithNewValues() } } } struct SwiftUIContentView_Previews: PreviewProvider { static var previews: some View { SwiftUILineGraphContentView(shouldAnimate: false) } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/SwiftUI-Overlays/SwiftUIView.swift ================================================ // // SwiftUIView.swift // Demos // // Created by Darren Ford on 28/2/21. // import SwiftUI struct SwiftUIView: View { let shouldAnimate: Bool var body: some View { ScrollView([.vertical, .horizontal]) { VStack { SuperCoolLineSpark() OverlayView() StripesOverlaidView() SwiftUILineGraphContentView(shouldAnimate: shouldAnimate) } } } } struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { SwiftUIView(shouldAnimate: false) } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/SwiftUI_Sparkline_DemoApp.swift ================================================ // // SwiftUI_Sparkline_DemoApp.swift // Shared // // Created by Darren Ford on 27/1/21. // import SwiftUI let IsRunningInPreviewPane: Bool = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" @main struct SwiftUI_Sparkline_DemoApp: App { var body: some Scene { WindowGroup { ContentView() } } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/TabletView.swift ================================================ // // TabletView.swift // Demos // // Created by Darren Ford on 28/2/21. // import SwiftUI import DSFSparkline fileprivate var TabletDataSource2: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(range: -1 ... 1) d.set(values: [1, -1, 1, -1, 1, 0, -1, -1, 1, 0, 1, -1, -1, 0]) return d }() struct TabletView: View { let shouldAnimate: Bool fileprivate let animator = Animator() var TabletDataSource1: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: -1 ... 1) d.push(values: [1, 1, 1, -1, 1, 0, -1, -1, 1, 1, 1, -1, -1, 1, 1, 0, -1, 1, 1, 1]) return d }() var tabletOverlay: DSFSparklineOverlay = { let t = DSFSparklineOverlay.Tablet() t.dataSource = TabletDataSource2 t.winStrokeColor = DSFColor.primaryTextColor.cgColor t.winFill = DSFSparkline.Fill.Color(DSFColor.systemGreen.withAlphaComponent(0.7).cgColor) t.lossStrokeColor = DSFColor.primaryTextColor.cgColor t.lossFill = DSFSparkline.Fill.Color(DSFColor.systemRed.withAlphaComponent(0.1).cgColor) return t }() var body: some View { VStack { Text("Tablets using prebuilt types") VStack { DSFSparklineTabletGraphView.SwiftUI( dataSource: TabletDataSource1, winColor: .systemTeal, lossColor: DSFColor.systemGray.withAlphaComponent(0.2), barSpacing: 2 ) .frame(height: 20) .padding(5) .border(Color.gray.opacity(0.2), width: 1) DSFSparklineTabletGraphView.SwiftUI( dataSource: TabletDataSource1, lineWidth: 0.5, barSpacing: 2 ) .padding(5) .border(Color.gray.opacity(0.2), width: 1) .frame(width: 200, height: 20) } VStack { Text("Tablet using overlays") DSFSparklineSurface.SwiftUI([ self.tabletOverlay ]) .padding(5) .border(Color.gray.opacity(0.2), width: 1) .frame(height: 30) .onAppear { if shouldAnimate { self.animator.updateWithNewValues() } } } .padding(EdgeInsets(top: 16, leading: 0, bottom: 0, trailing: 0)) } } } struct TabletView_Previews: PreviewProvider { static var previews: some View { TabletView(shouldAnimate: false) } } fileprivate class Animator { func updateWithNewValues() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in guard let `self` = self else { return } // push a new value into the graph's data source let cr2 = CGFloat.random(in: TabletDataSource2.range!) // -1 ... 1) _ = TabletDataSource2.push(value: cr2) self.updateWithNewValues() } } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/TestingView.swift ================================================ // // TestingView.swift // Demos // // Created by Darren Ford on 14/4/21. // import SwiftUI import DSFSparkline struct TestingView: View { var body: some View { Text("hello") .padding() } } struct TestingView_Previews: PreviewProvider { static var previews: some View { TestingView() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/WinLossGraphContentView.swift ================================================ // // WinLossGraphContentView.swift // SwiftUI Demo // // Created by Darren Ford on 26/1/21. // import SwiftUI import DSFSparkline func WinLossCreate() -> some View { return WinLossGraphContentView( leftDataSource: WinLossDataSource1, rightDataSource: WinLossDataSource2, upDataSource: WinLossDataSource3) } struct ProductSales: Identifiable { var id: String { product } static let formatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .percent formatter.minimumIntegerDigits = 1 formatter.maximumIntegerDigits = 3 formatter.maximumFractionDigits = 3 return formatter }() let product: String let q1: Double var q1s: String { ProductSales.formatter.string(for: q1 / 100) ?? "" } let q2: Double var q2s: String { ProductSales.formatter.string(for: q2 / 100) ?? "" } let q3: Double var q3s: String { ProductSales.formatter.string(for: q3 / 100) ?? "" } let q4: Double var q4s: String { ProductSales.formatter.string(for: q4 / 100) ?? "" } var wl: [CGFloat] { [q1, q2, q3, q4] } } struct WinLossGraphContentView: View { let leftDataSource: DSFSparkline.DataSource let rightDataSource: DSFSparkline.DataSource let upDataSource: DSFSparkline.DataSource @State private var sales: [ProductSales] = [ ProductSales(product: "AAA-001", q1: 6, q2: -4, q3: 9.80, q4: 10.20), ProductSales(product: "BBB-002", q1: 12, q2: -11, q3: -10.6, q4: 5.8), ProductSales(product: "CCC-003", q1: -9, q2: 7, q3: 5.20, q4: 6.70), ProductSales(product: "DDD-004", q1: 5, q2: -9, q3: 1.80, q4: -5.90), ] var body: some View { VStack(spacing: 8) { Text("Win/Loss") .font(.title2).bold() DSFSparklineWinLossGraphView.SwiftUI( dataSource: leftDataSource ) .frame(height: 60) .padding(5) .border(Color.gray.opacity(0.2), width: 1) Text("Win/Loss/Tie") .font(.title2).bold() DSFSparklineWinLossGraphView.SwiftUI( dataSource: rightDataSource, winColor: .systemIndigo, lossColor: .systemTeal, tieColor: DSFColor.systemGray.withAlphaComponent(0.1), lineWidth: 3, barSpacing: 6 ) .frame(height: 60) .padding(5) .border(Color.gray.opacity(0.2), width: 1) Text("Win/Loss/Tie with center-line") .font(.title2).bold() DSFSparklineWinLossGraphView.SwiftUI( dataSource: upDataSource, winColor: .systemGreen, lossColor: .systemRed, tieColor: DSFColor.systemYellow.withAlphaComponent(0.2), barSpacing: 3, showZeroLine: true, zeroLineDefinition: DSFSparkline.ZeroLineDefinition(color: .systemGray) ) .frame(width: 330.0, height: 34.0) .padding(5) .border(Color.gray.opacity(0.2), width: 1) Text("Product quarter") .font(.title2).bold() Table(sales) { TableColumn("Product", value: \.product) .alignment(.leading) TableColumn("Quarter 1") { product in Text(product.q1s) } .width(75) .alignment(.trailing) TableColumn("Quarter 2") { product in Text(product.q2s) } .width(75) .alignment(.trailing) TableColumn("Quarter 3") { product in Text(product.q3s) } .width(75) .alignment(.trailing) TableColumn("Quarter 4") { product in Text(product.q4s) } .width(75) .alignment(.trailing) TableColumn("win-loss") { product in DSFSparklineWinLossGraphView.SwiftUI( dataSource: .init(values: product.wl) ) .frame(height: 25) .frame(minWidth: 150) } .width(150) .alignment(.center) } .frame(height: 250) } .padding() } } var WinLossDataSource1: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 10, range: -1.0 ... 1) d.push(values: [1, -1, 0, 1, -1, -1, 1, -1, 0, 1]) return d }() var WinLossDataSource2: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 10, range: -1.0 ... 1.0) d.push(values: [20, 10, 0, -10, -20, -30, 40, 50, 0, 70]) return d }() var WinLossDataSource3: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: -1 ... 1) d.push(values: [1, 1, 1, -1, 1, 0, -1, -1, 1, 1, 1, -1, -1, 1, 1, 0, -1, 1, 1, 1]) return d }() struct WinLossGraphContentView_Previews: PreviewProvider { static var previews: some View { WinLossGraphContentView(leftDataSource: WinLossDataSource1, rightDataSource: WinLossDataSource2, upDataSource: WinLossDataSource3) } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/Shared/WiperGaugeDemoView.swift ================================================ // // PieGraphDemoView.swift // Demos // // Created by Darren Ford on 16/2/21. // import SwiftUI import DSFSparkline fileprivate let palette1 = DSFSparkline.Palette([.systemRed, .systemOrange, .systemYellow]) fileprivate let palette2 = DSFSparkline.Palette.shared fileprivate let grays = DSFSparkline.Palette([.systemGreen, .systemRed]) fileprivate let coldHot = DSFSparkline.GradientBucket(posts: [ DSFSparkline.GradientBucket.Post(color: CGColor(red: 0, green: 0, blue: 1, alpha: 1), location: 0), DSFSparkline.GradientBucket.Post(color: CGColor(red: 0, green: 0, blue: 1, alpha: 1), location: 0.3), DSFSparkline.GradientBucket.Post(color: CGColor(red: 1, green: 0.581, blue: 0, alpha: 1), location: 0.6), DSFSparkline.GradientBucket.Post(color: CGColor(red: 1, green: 0, blue: 0, alpha: 1), location: 0.8), DSFSparkline.GradientBucket.Post(color: CGColor(red: 1, green: 0, blue: 0, alpha: 1), location: 1) ]) struct WiperGraphDemoView: View { @State var randomValue: CGFloat = CGFloat.random(in: 0...1) @State var sliderValue: CGFloat = 0.75 @State var animationDuration: Double = 0.5 var body: some View { ScrollView { VStack(spacing: 8) { Text("Palette colors").font(.title2).bold() HStack(spacing: 8) { ForEach(0 ..< 7) { DSFSparklineWiperGaugeGraphView.SwiftUI(valueColor: DSFSparkline.ValueBasedFill(palette: palette2), value: Double($0) / 7.0 + 0.1) .frame(width: 48, height: 24) } } Divider() Text("Solid colors").font(.title2).bold() HStack(spacing: 8) { ForEach(0 ..< 6) { DSFSparklineWiperGaugeGraphView.SwiftUI( valueColor: DSFSparkline.ValueBasedFill(flatColor: CGColor(srgbRed: 0.1, green: 0.4, blue: 1.0, alpha: 1)), value: Double($0) / 6.0 + 0.1 ) .frame(width: 48, height: 24) } } Divider() VStack { Text("Palette colors (discrete)").font(.title2).bold() HStack { VStack { DSFSparklineWiperGaugeGraphView.SwiftUI(valueColor: DSFSparkline.ValueBasedFill(palette: palette2), value: randomValue) .frame(width: 64, height: 32) Text("default") } VStack { DSFSparklineWiperGaugeGraphView.SwiftUI( valueColor: DSFSparkline.ValueBasedFill(palette: palette2), value: randomValue, animationStyle: .init(duration: animationDuration) ) .frame(width: 64, height: 32) Text("animated") } Divider().frame(height: 60) VStack { HStack { Button("Random") { randomValue = CGFloat.random(in: 0...1) } Button("Min") { randomValue = 0 } Button("Max") { randomValue = 1 } Slider(value: $animationDuration, in: 0 ... 2) .frame(width: 100) } Text("\(randomValue)") .font(.custom("FontNameMono", fixedSize: 12)) } } HStack { VStack { DSFSparklineWiperGaugeGraphView.SwiftUI( valueColor: DSFSparkline.ValueBasedFill(palette: palette2), value: randomValue ) .frame(width: 128, height: 64) Text("default") } VStack { DSFSparklineWiperGaugeGraphView.SwiftUI( valueColor: DSFSparkline.ValueBasedFill(palette: palette2), value: randomValue, animationStyle: .init(duration: animationDuration) ) .frame(width: 128, height: 64) Text("animated") } } Divider() VStack { Text("Value color types").font(.title2).bold() HStack { VStack { DSFSparklineWiperGaugeGraphView.SwiftUI(valueColor: DSFSparkline.ValueBasedFill(flatColor: CGColor(srgbRed: 0.1, green: 0.4, blue: 0.8, alpha: 1)), value: sliderValue) .frame(width: 200, height: 100) Text("Solid color") } VStack { DSFSparklineWiperGaugeGraphView.SwiftUI(valueColor: DSFSparkline.ValueBasedFill(gradient: coldHot), value: sliderValue) .frame(width: 200, height: 100) Text("Smooth gradient") } VStack { DSFSparklineWiperGaugeGraphView.SwiftUI(valueColor: DSFSparkline.ValueBasedFill.sharedPalette, value: sliderValue) .frame(width: 200, height: 100) Text("Bucket Color") } } Slider(value: $sliderValue) .frame(width: 200) } Divider() VStack { Text("Color components").font(.title2).bold() DSFSparklineWiperGaugeGraphView.SwiftUI( valueColor: DSFColor(red: 0.1, green: 0.4, blue: 0.8, alpha: 1), value: sliderValue, valueBackgroundColor: DSFColor.red, upperArcColor: DSFColor.yellow, pointerColor: DSFColor.green, backgroundColor: DSFColor.purple ) .frame(width: 200, height: 100) } } } } } } struct WiperGraphDemoView_Previews: PreviewProvider { static var previews: some View { WiperGraphDemoView() } } ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/iOS/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UIApplicationSupportsIndirectInputEvents UILaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/macOS/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) ================================================ FILE: Demos/Samples/SwiftUI Sparkline Crossplatform/macOS/macOS.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.files.user-selected.read-only ================================================ FILE: Demos/Samples/iOS Sparkline Demo/AppDelegate.swift ================================================ // // AppDelegate.swift // iOS Sparkline Demo // // Created by Darren Ford on 23/12/19. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } } ================================================ FILE: Demos/Samples/iOS Sparkline Demo/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "1x" }, { "idiom" : "ipad", "size" : "20x20", "scale" : "2x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "1x" }, { "idiom" : "ipad", "size" : "29x29", "scale" : "2x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "1x" }, { "idiom" : "ipad", "size" : "40x40", "scale" : "2x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "1x" }, { "idiom" : "ipad", "size" : "76x76", "scale" : "2x" }, { "idiom" : "ipad", "size" : "83.5x83.5", "scale" : "2x" }, { "idiom" : "ios-marketing", "size" : "1024x1024", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/iOS Sparkline Demo/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/iOS Sparkline Demo/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Demos/Samples/iOS Sparkline Demo/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Demos/Samples/iOS Sparkline Demo/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UISceneConfigurations UIWindowSceneSessionRoleApplication UISceneConfigurationName Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate UISceneStoryboardFile Main UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Demos/Samples/iOS Sparkline Demo/SceneDelegate.swift ================================================ // // SceneDelegate.swift // iOS Sparkline Demo // // Created by Darren Ford on 23/12/19. // import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let _ = (scene as? UIWindowScene) else { return } } func sceneDidDisconnect(_ scene: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). } func sceneDidBecomeActive(_ scene: UIScene) { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } func sceneWillResignActive(_ scene: UIScene) { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). } func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. } func sceneDidEnterBackground(_ scene: UIScene) { // Called as the scene transitions from the foreground to the background. // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. } } ================================================ FILE: Demos/Samples/iOS Sparkline Demo/ViewController.swift ================================================ // // ViewController.swift // iOS Sparkline Demo // // Created by Darren Ford on 23/12/19. // import UIKit import DSFSparkline class ViewController: UIViewController { @IBOutlet var spark1: DSFSparklineLineGraphView! @IBOutlet var spark12: DSFSparklineLineGraphView! var spark1ds = DSFSparkline.DataSource(windowSize: 50, range: -10 ... 30) @IBOutlet var spark2: DSFSparklineLineGraphView! var spark2ds = DSFSparkline.DataSource(windowSize: 50, range: -12 ... 12) @IBOutlet var spark3: DSFSparklineBarGraphView! var spark3ds = DSFSparkline.DataSource(windowSize: 30, range: -10 ... 10) @IBOutlet var spark4: DSFSparklineBarGraphView! var spark4ds = DSFSparkline.DataSource(windowSize: 30, range: -10 ... 10) @IBOutlet var dot1: DSFSparklineDotGraphView! var dot1ds = DSFSparkline.DataSource(windowSize: 80, range: -10 ... 10) @IBOutlet var s1: DSFSparklineBarGraphView! var ds1 = DSFSparkline.DataSource(range: -10 ... 10) @IBOutlet var s2: DSFSparklineBarGraphView! var ds2 = DSFSparkline.DataSource(range: -10 ... 10) @IBOutlet var s3: DSFSparklineBarGraphView! var ds3 = DSFSparkline.DataSource(range: -10 ... 10) @IBOutlet var s4: DSFSparklineBarGraphView! var ds4 = DSFSparkline.DataSource(range: -10 ... 10) @IBOutlet var winLoss1: DSFSparklineWinLossGraphView! var wl1 = DSFSparkline.DataSource(range: -1 ... 1) @IBOutlet var winLoss2: DSFSparklineWinLossGraphView! @IBOutlet var tabletView1: DSFSparklineTabletGraphView! @IBOutlet var tabletView2: DSFSparklineTabletGraphView! @IBOutlet var stacklineGraphView: DSFSparklineStackLineGraphView! var sl1 = DSFSparkline.DataSource(range: 0 ... 10) @IBOutlet var stacklineGraphView2: DSFSparklineStackLineGraphView! @IBOutlet var pie0: DSFSparklinePieGraphView! @IBOutlet var pie1: DSFSparklinePieGraphView! @IBOutlet var pie2: DSFSparklinePieGraphView! @IBOutlet var pie3: DSFSparklinePieGraphView! @IBOutlet var pie4: DSFSparklinePieGraphView! @IBOutlet weak var attributedStringSupportLabel: UILabel! @IBOutlet weak var percentBar1: DSFSparklinePercentBarGraphView! @IBOutlet weak var percentBar2: DSFSparklinePercentBarGraphView! @IBOutlet weak var percentBar3: DSFSparklinePercentBarGraphView! override func viewDidLoad() { super.viewDidLoad() // Setup the inline attributed string self.configureAttributedString() self.spark1.dataSource = self.spark1ds self.spark12.dataSource = self.spark1ds self.spark2.dataSource = self.spark2ds self.spark2ds.zeroLineValue = 0 self.spark3.dataSource = self.spark3ds self.spark4.dataSource = self.spark4ds self.spark4ds.zeroLineValue = -5 self.dot1.dataSource = self.dot1ds self.s1.dataSource = self.ds1 self.s2.dataSource = self.ds2 self.s3.dataSource = self.ds3 self.s4.dataSource = self.ds4 self.winLoss1.dataSource = self.wl1 self.winLoss2.dataSource = self.wl1 self.tabletView1.dataSource = self.wl1 self.tabletView2.dataSource = self.wl1 self.sl1.zeroLineValue = 5 self.stacklineGraphView.dataSource = self.sl1 self.stacklineGraphView2.dataSource = self.sl1 let palette = DSFSparkline.Palette([ DSFColor(red: 0.6, green: 0.6, blue: 1, alpha: 1), DSFColor(red: 0.4, green: 0.4, blue: 1, alpha: 1), DSFColor(red: 0.2, green: 0.2, blue: 1, alpha: 1), DSFColor(red: 0.0, green: 0.0, blue: 1, alpha: 1), ]) self.pie0.palette = .sharedGrays self.pie0.dataSource = DSFSparkline.StaticDataSource([5, 5, 5, 5, 5]) self.pie1.palette = palette self.pie2.palette = palette self.percentBar1.animationStyle = DSFSparkline.AnimationStyle() self.percentBar2.animationStyle = DSFSparkline.AnimationStyle() self.percentBar3.animationStyle = DSFSparkline.AnimationStyle() self.updateValues(self) self.addNewValue2() } var sinusoid: CGFloat = 0.00 func addNewValue2() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in guard let `self` = self else { return } _ = self.spark1ds.push(value: CGFloat.random(in: self.spark1ds.range!)) _ = self.spark2ds.push(value: CGFloat.random(in: -10.0 ... 10.0)) _ = self.spark3ds.push(value: CGFloat.random(in: -10.0 ... 10.0)) let val = sin(self.sinusoid) self.sinusoid += 0.3 _ = self.spark4ds.push(value: val * 10.0) _ = self.dot1ds.push(value: CGFloat.random(in: -10.0 ... 10.0)) _ = self.ds1.push(value: CGFloat.random(in: -10.0 ... 10.0)) _ = self.ds2.push(value: CGFloat.random(in: -10.0 ... 10.0)) _ = self.ds3.push(value: CGFloat.random(in: -10.0 ... 10.0)) _ = self.ds4.push(value: CGFloat.random(in: -10.0 ... 10.0)) _ = self.wl1.push(value: CGFloat(Int.random(in: -1 ... 1))) _ = self.sl1.push(value: CGFloat(Int.random(in: 0 ... 10))) self.addNewValue2() } } func configureAttributedString() { let source = DSFSparkline.DataSource(values: [4, 1, 8, 7, 5, 9, 3], range: 0 ... 10) let baseColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.033, 0.277, 0.650, 1.000])! let primaryStroke = baseColor // (gray: 0.0, alpha: 0.3)) let primaryFill = DSFSparkline.Fill.Color(baseColor.copy(alpha: 0.3)!) let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let line = DSFSparklineOverlay.Line() // Create a line overlay line.strokeWidth = 1 line.primaryStrokeColor = primaryStroke line.primaryFill = primaryFill line.dataSource = source // Assign the datasource to the overlay bitmap.addOverlay(line) // And add the overlay to the surface. let attr = bitmap.attributedString(size: CGSize(width: 40, height: 18), scale: 2)! let message = NSMutableAttributedString(string: "Inlined ") message.append(attr) message.append(NSAttributedString(string: " line graph")) self.attributedStringSupportLabel.attributedText = message } @IBAction func updateValues(_: Any) { let v1 = UInt.random(count: 4, range: 1 ... 10).map { CGFloat($0) } self.pie1.dataSource = DSFSparkline.StaticDataSource(v1) self.pie2.dataSource = DSFSparkline.StaticDataSource(v1) let v2 = UInt.random(count: 4, range: 1 ... 10).map { CGFloat($0) } self.pie3.dataSource = DSFSparkline.StaticDataSource(v2) self.pie4.dataSource = DSFSparkline.StaticDataSource(v2) } @IBAction func newValuesForPercentBar(_ sender: Any) { self.percentBar1.value = CGFloat(drand48()) self.percentBar2.value = CGFloat(drand48()) self.percentBar3.value = CGFloat(drand48()) } } extension CGFloat { static func random(count: UInt, range: ClosedRange) -> [CGFloat] { return (0 ..< count).map { _ in CGFloat.random(in: range) } } } extension UInt { static func random(count: UInt, range: ClosedRange) -> [UInt] { return (0 ..< count).map { _ in UInt.random(in: range) } } } ================================================ FILE: Demos/Samples/iOS Sparkline Demo/iOS Sparkline Demo.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.network.client ================================================ FILE: Demos/Samples/macOS Sparkline Demo/AppDelegate.swift ================================================ // // AppDelegate.swift // macOS Sparkline Demo // // Created by Darren Ford on 23/12/19. // import Cocoa import DSFSparkline @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { } } ================================================ FILE: Demos/Samples/macOS Sparkline Demo/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "size" : "16x16", "scale" : "1x" }, { "idiom" : "mac", "size" : "16x16", "scale" : "2x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "1x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "2x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "1x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "2x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "1x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "2x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "1x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/macOS Sparkline Demo/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/macOS Sparkline Demo/Base.lproj/Main.storyboard ================================================ Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: Demos/Samples/macOS Sparkline Demo/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSMainStoryboardFile Main NSPrincipalClass NSApplication NSSupportsAutomaticTermination NSSupportsSuddenTermination ================================================ FILE: Demos/Samples/macOS Sparkline Demo/ViewController.swift ================================================ // // ViewController.swift // macOS Sparkline Demo // // Created by Darren Ford on 23/12/19. // import Cocoa import DSFSparkline class ViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } @IBOutlet weak var sparkCrash: DSFSparklineDataSourceView! var sparkCrashDatasource = DSFSparkline.DataSource(range: -10 ... 10) @IBOutlet weak var sparkCrash2: DSFSparklineDataSourceView! var sparkCrash2Datasource = DSFSparkline.DataSource(range: -13 ... 13) @IBOutlet weak var sparkDeprecation: DSFSparklineDataSourceView! var sparkDeprecationDatasource = DSFSparkline.DataSource(range: -10 ... 10) @IBOutlet weak var sparkIntervention: DSFSparklineDataSourceView! var sparkInterventionDatasource = DSFSparkline.DataSource() @IBOutlet weak var sparkNetworkError: DSFSparklineDataSourceView! var sparkNetworkErrorDatasource = DSFSparkline.DataSource(range: -10 ... 30) @IBOutlet weak var sparkTransmissionError: DSFSparklineDataSourceView! var sparkTransmissionErrorDatasource = DSFSparkline.DataSource(range: 0 ... 1) @IBOutlet weak var sparkPacketRejection: DSFSparklineWinLossGraphView! var sparkPacketRejectionDatasource = DSFSparkline.DataSource(windowSize: 10, range: -1 ... 1) @IBOutlet weak var stackLineView: DSFSparklineStackLineGraphView! var stackLineViewDatasource = DSFSparkline.DataSource(range: 0 ... 10) @IBOutlet weak var fakeSparkCpu1: DSFSparklineDataSourceView! var fakeSparkCpu1Datasource = DSFSparkline.DataSource(range: 0 ... 1, zeroLineValue: 0.8) @IBOutlet weak var fakeSparkCpu2: DSFSparklineDataSourceView! var fakeSparkCpu2Datasource = DSFSparkline.DataSource(range: 0 ... 1, zeroLineValue: 0.8) @IBOutlet weak var fakeSparkCpu3: DSFSparklineDataSourceView! var fakeSparkCpu3Datasource = DSFSparkline.DataSource(range: 0 ... 1, zeroLineValue: 0.8) @IBOutlet weak var fakeSparkCpu4: DSFSparklineDataSourceView! var fakeSparkCpu4Datasource = DSFSparkline.DataSource(range: 0 ... 1, zeroLineValue: 0.8) @IBOutlet weak var fakeSparkCpu5: DSFSparklineDataSourceView! var fakeSparkCpu5Datasource = DSFSparkline.DataSource(range: 0 ... 1, zeroLineValue: 0.8) @IBOutlet weak var sparkStaticData: DSFSparklineLineGraphView! var sparkStaticDatasource = DSFSparkline.DataSource(range: -20 ... 50) @IBOutlet weak var cpuStack: NSStackView! @IBOutlet weak var cpuDotView: DSFSparklineDotGraphView! var cpuDotViewDatasource = DSFSparkline.DataSource(windowSize: 100, range: 0 ... 100) @IBOutlet weak var cpu2DotView: DSFSparklineDotGraphView! var cpu2DotViewDatasource = DSFSparkline.DataSource(windowSize: 100, range: 0 ... 100) let cpuUsage = MyCpuUsage() @IBOutlet weak var attributedStringTextField: NSTextField! override func viewWillAppear() { super.viewWillAppear() self.configureAttributedString() self.cpuStack.translatesAutoresizingMaskIntoConstraints = false cpuUsage.delegate = self sparkCrash.dataSource = sparkCrashDatasource sparkCrash2.dataSource = sparkCrash2Datasource sparkCrash2Datasource.zeroLineValue = -5 sparkDeprecation.dataSource = sparkDeprecationDatasource sparkIntervention.dataSource = sparkInterventionDatasource sparkNetworkError.dataSource = sparkNetworkErrorDatasource sparkTransmissionError.dataSource = sparkTransmissionErrorDatasource sparkPacketRejectionDatasource.windowSize = 20 sparkPacketRejection.dataSource = sparkPacketRejectionDatasource stackLineView.dataSource = stackLineViewDatasource //stackLineView.highlightRangeDefinition?.range = 3 ..< 7 stackLineView.highlightRangeVisible = true fakeSparkCpu1.dataSource = fakeSparkCpu1Datasource fakeSparkCpu2.dataSource = fakeSparkCpu2Datasource fakeSparkCpu3.dataSource = fakeSparkCpu3Datasource fakeSparkCpu4.dataSource = fakeSparkCpu4Datasource fakeSparkCpu5.dataSource = fakeSparkCpu5Datasource cpuDotView.dataSource = cpuDotViewDatasource cpu2DotView.dataSource = cpu2DotViewDatasource sparkStaticData.dataSource = sparkStaticDatasource sparkStaticDatasource.set(values: (0 ... 10).map { _ in CGFloat.random(in: -20.0 ... 50.0) }) self.updateWithNewValues() cpuUsage.updateInfo(Timer()) } func updateWithNewValues() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in guard let `self` = self else { return } let cr = CGFloat.random(in: -10.0 ... 10.0) _ = self.sparkCrashDatasource.push(value: cr) _ = self.sparkCrash2Datasource.push(value: cr) _ = self.sparkDeprecationDatasource.push(value: CGFloat.random(in: -10.0 ... 10.0)) _ = self.sparkInterventionDatasource.push(value: CGFloat.random(in: -10.0 ... 10.0)) _ = self.sparkNetworkErrorDatasource.push(value: CGFloat.random(in: -10.0 ... 30.0)) _ = self.sparkTransmissionErrorDatasource.push(value: CGFloat.random(in: 0 ... 1)) _ = self.sparkPacketRejectionDatasource.push(value: CGFloat(Int.random(in: -1 ... 1))) _ = self.stackLineViewDatasource.push(value: CGFloat(Int.random(in: 0 ... 10))) ///// _ = self.fakeSparkCpu1Datasource.push(value: CGFloat.random(in: 0 ... 1)) _ = self.fakeSparkCpu2Datasource.push(value: CGFloat.random(in: 0 ... 1)) _ = self.fakeSparkCpu3Datasource.push(value: CGFloat.random(in: 0 ... 1)) _ = self.fakeSparkCpu4Datasource.push(value: CGFloat.random(in: 0 ... 1)) _ = self.fakeSparkCpu5Datasource.push(value: CGFloat.random(in: 0 ... 1)) _ = self.cpuDotViewDatasource.push(value: CGFloat.random(in: 0 ... 100)) _ = self.cpu2DotViewDatasource.push(value: CGFloat.random(in: 0 ... 100)) self.updateWithNewValues() } } var cpuDataSources: [DSFSparkline.DataSource] = [] func configureAttributedString() { let source = DSFSparkline.DataSource(values: [4, 1, 8, 7, 5, 9, 3], range: 0 ... 10) let baseColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.033, 0.277, 0.650, 1.000])! let primaryStroke = baseColor // (gray: 0.0, alpha: 0.3)) let primaryFill = DSFSparkline.Fill.Color(baseColor.copy(alpha: 0.3)!) let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let line = DSFSparklineOverlay.Line() // Create a line overlay line.strokeWidth = 1 line.primaryStrokeColor = primaryStroke line.primaryFill = primaryFill line.dataSource = source // Assign the datasource to the overlay bitmap.addOverlay(line) // And add the overlay to the surface. let attr = bitmap.attributedString(size: CGSize(width: 40, height: 18), scale: 2)! let message = NSMutableAttributedString(string: "Inlined ") message.append(attr) message.append(NSAttributedString(string: " line graph")) self.attributedStringTextField.attributedStringValue = message } } extension ViewController: CPUDelegate { func cpuUsage(usage: [Float]) { DispatchQueue.main.async { [weak self] in self?.updateOnMainThread(usage: usage) } } func updateOnMainThread(usage: [Float]) { if cpuDataSources.count == 0 { self.cpuStack.arrangedSubviews.forEach { $0.removeFromSuperview() } cpuDataSources.removeAll() usage.forEach { _ in let cpu = DSFSparklineBarGraphView(frame: CGRect(x: 0, y: 0, width: cpuStack.frame.width, height: 32)) let ds = DSFSparkline.DataSource(range: 0 ... 100) ds.windowSize = 30 cpu.dataSource = ds self.cpuDataSources.append(ds) cpu.translatesAutoresizingMaskIntoConstraints = false cpu.addConstraint(NSLayoutConstraint(item: cpu, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 48)) cpu.graphColor = .systemBlue cpu.isHidden = false cpu.barSpacing = 1 self.cpuStack.addArrangedSubview(cpu) let box = NSBox(frame: NSRect(x: 0, y: 0, width: 20, height: 1)) box.translatesAutoresizingMaskIntoConstraints = false box.boxType = .separator self.cpuStack.addArrangedSubview(box) } } usage.enumerated().forEach { (offset, element) in _ = cpuDataSources[offset].push(value: CGFloat(element * 100.0)) } } } ================================================ FILE: Demos/Samples/macOS Sparkline Demo/cpuUsage.swift ================================================ // // cpuUsage.swift // DSFSparklines Demo // // Created by Darren Ford on 3/7/19. // Copyright © 2019 Darren Ford. All rights reserved. // import Foundation // CPU usage credit VenoMKO: https://stackoverflow.com/a/6795612/1033581 protocol CPUDelegate { func cpuUsage(usage: [Float]) } class MyCpuUsage { var cpuInfo: processor_info_array_t! var prevCpuInfo: processor_info_array_t? var numCpuInfo: mach_msg_type_number_t = 0 var numPrevCpuInfo: mach_msg_type_number_t = 0 var numCPUs: uint = 0 var updateTimer: Timer! let CPUUsageLock: NSLock = NSLock() init() { let mibKeys: [Int32] = [ CTL_HW, HW_NCPU ] // sysctl Swift usage credit Matt Gallagher: https://github.com/mattgallagher/CwlUtils/blob/master/Sources/CwlUtils/CwlSysctl.swift mibKeys.withUnsafeBufferPointer() { mib in var sizeOfNumCPUs: size_t = MemoryLayout.size let status = sysctl(processor_info_array_t(mutating: mib.baseAddress), 2, &numCPUs, &sizeOfNumCPUs, nil, 0) if status != 0 { numCPUs = 1 } updateTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(updateInfo), userInfo: nil, repeats: true) } } var delegate: CPUDelegate? @objc func updateInfo(_ timer: Timer) { var numCPUsU: natural_t = 0 let err: kern_return_t = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numCPUsU, &cpuInfo, &numCpuInfo); if err == KERN_SUCCESS { CPUUsageLock.lock() var result = [Float]() for i in 0 ..< Int32(numCPUs) { var inUse: Int32 var total: Int32 if let prevCpuInfo = prevCpuInfo { inUse = cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)] - prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)] + cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)] - prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)] + cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)] - prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)] total = inUse + (cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)] - prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)]) } else { inUse = cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)] + cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)] + cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)] total = inUse + cpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)] } result.append(Float(inUse) / Float(total)) //print(String(format: "Core: %u Usage: %f", i, Float(inUse) / Float(total) * 100)) } CPUUsageLock.unlock() if let prevCpuInfo = prevCpuInfo { // vm_deallocate Swift usage credit rsfinn: https://stackoverflow.com/a/48630296/1033581 let prevCpuInfoSize: size_t = MemoryLayout.stride * Int(numPrevCpuInfo) vm_deallocate(mach_task_self_, vm_address_t(bitPattern: prevCpuInfo), vm_size_t(prevCpuInfoSize)) } prevCpuInfo = cpuInfo numPrevCpuInfo = numCpuInfo cpuInfo = nil numCpuInfo = 0 self.delegate?.cpuUsage(usage: result) } else { print("Error!") } } } ================================================ FILE: Demos/Samples/macOS Sparkline Demo/macOS_Sparkline_Demo.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.files.user-selected.read-only ================================================ FILE: Demos/Samples/macOS Sparkline Demo Objc/AppDelegate.h ================================================ // // AppDelegate.h // macOS Sparkline Demo Objc // // Created by Darren Ford on 23/12/19. // #import @interface AppDelegate : NSObject @end ================================================ FILE: Demos/Samples/macOS Sparkline Demo Objc/AppDelegate.m ================================================ // // AppDelegate.m // macOS Sparkline Demo Objc // // Created by Darren Ford on 23/12/19. // #import "AppDelegate.h" @interface AppDelegate () @end @implementation AppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Insert code here to initialize your application } - (void)applicationWillTerminate:(NSNotification *)aNotification { // Insert code here to tear down your application } @end ================================================ FILE: Demos/Samples/macOS Sparkline Demo Objc/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "size" : "16x16", "scale" : "1x" }, { "idiom" : "mac", "size" : "16x16", "scale" : "2x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "1x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "2x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "1x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "2x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "1x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "2x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "1x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/macOS Sparkline Demo Objc/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/macOS Sparkline Demo Objc/Base.lproj/Main.storyboard ================================================ Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: Demos/Samples/macOS Sparkline Demo Objc/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSMainStoryboardFile Main NSPrincipalClass NSApplication NSSupportsAutomaticTermination NSSupportsSuddenTermination ================================================ FILE: Demos/Samples/macOS Sparkline Demo Objc/ViewController.h ================================================ // // ViewController.h // macOS Sparkline Demo Objc // // Created by Darren Ford on 23/12/19. // #import @interface ViewController : NSViewController @end ================================================ FILE: Demos/Samples/macOS Sparkline Demo Objc/ViewController.m ================================================ // // ViewController.m // macOS Sparkline Demo Objc // // Created by Darren Ford on 23/12/19. // #import "ViewController.h" #import @import DSFSparkline; @interface ViewController () @property (nonatomic, weak) IBOutlet DSFSparklineLineGraphView *lineGraph; @property (nonatomic, strong) DSFSparklineDataSource* dataSource; @property (weak) IBOutlet DSFSparklineBarGraphView* barGraph; @property (nonatomic, strong) DSFSparklineDataSource* barDataSource; @property (weak) IBOutlet DSFSparklineBarGraphView* centeredBarGraph; @property (nonatomic, strong) DSFSparklineDataSource* centeredBarDataSource; @property (weak) IBOutlet DSFSparklineDotGraphView *receiveGraph; @property (nonatomic, strong) DSFSparklineDataSource* receiveDataSource; @property (weak) IBOutlet DSFSparklineDotGraphView *sendGraph; @property (nonatomic, strong) DSFSparklineDataSource* sendDataSource; @property (weak) IBOutlet DSFSparklinePercentBarGraphView *percentBarThroughput; @property (weak) IBOutlet DSFSparklineWiperGaugeGraphView *wiperGauge; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; srand48(time(0)); _dataSource = [[DSFSparklineDataSource alloc] init]; _barDataSource = [[DSFSparklineDataSource alloc] init]; _centeredBarDataSource = [[DSFSparklineDataSource alloc] init]; _receiveDataSource = [[DSFSparklineDataSource alloc] init]; _sendDataSource = [[DSFSparklineDataSource alloc] init]; [_wiperGauge setAnimationStyle:[[AnimationStyle alloc] initWithDuration:0.2 function: AnimatorFunctionTypeLinear]]; // Add a custom marker drawing function [_lineGraph setMarkerDrawingBlock:^(CGContextRef context, NSArray * markers) { // Just draw the markers for the 4 most recent values id ms = [markers subarrayWithRange:NSMakeRange([markers count] - 4, 4)]; for (DSFSparklineOverlayLineMarker* m in ms) { CGContextSetFillColorWithColor(context, NSColor.whiteColor.CGColor); CGContextSetShadowWithColor(context, CGSizeMake(1, 1), 4, NSColor.linkColor.CGColor); CGContextFillRect(context, [m rect]); } }]; [[self lineGraph] setDataSource:[self dataSource]]; [[self dataSource] setRangeWithLowerBound:-1.0 upperBound:1.0]; [[self lineGraph] setZeroLineDashStyleString: @"3,3"]; [[self barGraph] setDataSource:[self barDataSource]]; [[self barDataSource] setRangeWithLowerBound:0.0 upperBound:1.0]; [[self barDataSource] setZeroLineValue:0.2]; [[self barDataSource] setWindowSize:30]; CGColorRef blue = CGColorCreateGenericRGB(0, 0, 1, 0.5); id high = [[DSFSparklineHighlightRangeDefinition alloc] initWithLowerBound:0.3 upperBound:0.7 fillColor:blue]; [[self barGraph] setHighlightRangeDefinition:@[high]]; [[self centeredBarGraph] setDataSource:[self centeredBarDataSource]]; [[self centeredBarDataSource] setRangeWithLowerBound:-1.0 upperBound:1.0]; //[[self centeredBarDataSource] setZeroLineValue:-0.2]; [[self centeredBarDataSource] setWindowSize:22]; [[self centeredBarDataSource] setWithValues: @[@(0.0), @(0.1), @(0.2), @(0.3), @(0.4), @(0.5), @(0.6), @(0.7), @(0.8), @(0.9), @(1), @(0.0), @(-0.1), @(-0.2), @(-0.3), @(-0.4), @(-0.5), @(-0.6), @(-0.7), @(-0.8), @(-0.9), @(-1)]]; [[self receiveGraph] setDataSource:[self receiveDataSource]]; [[self receiveDataSource] setRangeWithLowerBound:0.0 upperBound:1.0]; [[self sendGraph] setDataSource:[self sendDataSource]]; [[self sendDataSource] setRangeWithLowerBound:0.0 upperBound:1.0]; DSFSparklinePercentBarStyle* style = [[self percentBarThroughput] displayStyle]; [style setBarEdgeInsets: NSEdgeInsetsMake(1, 1, 1, 1)]; [self performUpdate]; } - (void)performUpdate { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ CGFloat v1 = drand48(); [[self wiperGauge] setValue:v1]; BOOL result = [[self dataSource] pushWithValue: (v1 * 2) - 1]; [[self percentBarThroughput] setValue:v1]; result = [[self barDataSource] pushWithValue: v1]; // v1 = drand48(); // result = [[self centeredBarDataSource] pushWithValue:(v1 * 2) - 1]; v1 = drand48(); result = [[self receiveDataSource] pushWithValue:v1]; v1 = drand48(); result = [[self sendDataSource] pushWithValue:v1]; [self performUpdate]; }); } - (void)setRepresentedObject:(id)representedObject { [super setRepresentedObject:representedObject]; // Update the view, if already loaded. } @end ================================================ FILE: Demos/Samples/macOS Sparkline Demo Objc/macOS_Sparkline_Demo_Objc.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.files.user-selected.read-only ================================================ FILE: Demos/Samples/macOS Sparkline Demo Objc/main.m ================================================ // // main.m // macOS Sparkline Demo Objc // // Created by Darren Ford on 23/12/19. // #import int main(int argc, const char * argv[]) { @autoreleasepool { // Setup code that might create autoreleased objects goes here. } return NSApplicationMain(argc, argv); } ================================================ FILE: Demos/Samples/macOS Table Demo/AppDelegate.swift ================================================ // // AppDelegate.swift // macOS Table Demo // // Created by Darren Ford on 27/12/19. // import Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { // Insert code here to initialize your application } func applicationWillTerminate(_ aNotification: Notification) { // Insert code here to tear down your application } } ================================================ FILE: Demos/Samples/macOS Table Demo/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "size" : "16x16", "scale" : "1x" }, { "idiom" : "mac", "size" : "16x16", "scale" : "2x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "1x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "2x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "1x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "2x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "1x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "2x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "1x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/macOS Table Demo/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/macOS Table Demo/Base.lproj/Main.storyboard ================================================ Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: Demos/Samples/macOS Table Demo/GridViewController.swift ================================================ // // GridViewController.swift // macOS Table Demo // // Created by Darren Ford on 27/12/19. // import Cocoa import DSFSparkline class GridViewController: NSViewController { @IBOutlet weak var grid: NSGridView! let sz = 32 var dataSources: [[DSFSparkline.DataSource]] = [] override func viewDidLoad() { super.viewDidLoad() // Do view setup here. grid.removeColumn(at: 0) grid.removeColumn(at: 0) grid.removeRow(at: 0) grid.removeRow(at: 0) grid.removeRow(at: 0) (0 ... sz).forEach { row in let item = (0 ... sz).map { _ in DSFSparkline.DataSource(windowSize: 30, range: -1 ... 1) } dataSources.append(item) } (0 ... sz).forEach { row in let vs = (0 ... sz).map { column -> DSFSparklineLineGraphView in let i = DSFSparklineLineGraphView() i.zeroLineVisible = false i.graphColor = self.color(row: row, col: column) i.lineWidth = 0.5 i.translatesAutoresizingMaskIntoConstraints = false i.addConstraint(NSLayoutConstraint(item: i, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 32)) i.addConstraint(NSLayoutConstraint(item: i, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 32)) i.dataSource = self.dataSources[row][column] let ranr: [CGFloat] = RandomArray(count: 30, range: -1 ... 1) self.dataSources[row][column].push(values: ranr) return i } self.grid.addColumn(with: vs) } self.updateWithNewValues() } private func lighter(_ color: NSColor) -> NSColor { var h: CGFloat = 0 var s: CGFloat = 0 var b: CGFloat = 0 var a: CGFloat = 0 let cl = color.usingColorSpace(.genericRGB)! cl.getHue(&h, saturation: &s, brightness: &b, alpha: &a) return NSColor(calibratedHue: h, saturation: max(s + 0.3, 1.0), brightness: max(b + 0.3, 1.0), alpha: a) } func color(row: Int, col: Int) -> NSColor { // CMYK let xpos = CGFloat(col) / CGFloat(sz) let ypos = CGFloat(row) / CGFloat(sz) let c = hypot(xpos, ypos) / 1.414 let m = hypot(1.0 - xpos, ypos) / 1.414 let y = hypot(1.0 - xpos, 1.0 - ypos) / 1.414 let k = hypot(xpos, 1.0 - ypos) / 1.414 let color = NSColor(deviceCyan: c, magenta: m, yellow: y, black: k, alpha: 1.0) return self.lighter(color) } func updateWithNewValues() { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in guard let `self` = self else { return } (0 ... self.sz).forEach { row in (0 ... self.sz).forEach { column in _ = self.dataSources[row][column].push(value: CGFloat.random(in: -1 ... 1)) } } self.updateWithNewValues() } } } /// Generate an array of random values for Swift value types that support random value generation /// - Parameters: /// - count: The number of random numbers to generate /// - range: The range of values to generate /// /// Example: /// /// ```swift /// let tenVals: [CGFloat] = RandomArray(count: 10, range: -10 ... 10) /// ``` /// /// Note that the `[CGFloat]` in the definition is required, as explicit generic function instantiation isn't supported /// in Swift. func RandomArray(count: UInt, range: ClosedRange) -> Array where T: BinaryFloatingPoint, T.RawSignificand : FixedWidthInteger { return (0 ..< count).map { _ in T.random(in: range) } } ================================================ FILE: Demos/Samples/macOS Table Demo/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSMainStoryboardFile Main NSPrincipalClass NSApplication NSSupportsAutomaticTermination NSSupportsSuddenTermination ================================================ FILE: Demos/Samples/macOS Table Demo/ViewController.swift ================================================ // // ViewController.swift // macOS Table Demo // // Created by Darren Ford on 27/12/19. // import Cocoa import DSFSparkline class ViewController: NSViewController { @IBOutlet weak var tableView: NSTableView! let count = 200 var inDataSources: [DSFSparkline.DataSource] = [] var outDataSources: [DSFSparkline.DataSource] = [] var cpuDataSources: [DSFSparkline.DataSource] = [] override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. (0 ..< count).forEach { _ in inDataSources.append(DSFSparkline.DataSource(windowSize: 50, range: -1.5 ... 1.5)) outDataSources.append(DSFSparkline.DataSource(windowSize: 50, range: -1.5 ... 1.5)) cpuDataSources.append(DSFSparkline.DataSource(windowSize: 100, range: 0 ... 100)) } self.tableView.reloadData() self.updateWithNewValues() } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } func updateWithNewValues() { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in guard let `self` = self else { return } self.inDataSources.forEach { _ = $0.push(value: CGFloat.random(in: -1 ... 1)) } self.outDataSources.forEach { _ = $0.push(value: CGFloat.random(in: -1 ... 1)) } self.cpuDataSources.forEach { _ = $0.push(value: CGFloat.random(in: 0 ... 100)) } self.updateWithNewValues() } } } class Graphico: NSTableCellView { @IBOutlet weak var sparkline: DSFSparklineLineGraphView! } class Graphicodot: NSTableCellView { @IBOutlet weak var sparkline: DSFSparklineDotGraphView! } extension ViewController: NSTableViewDataSource, NSTableViewDelegate { func numberOfRows(in tableView: NSTableView) -> Int { return count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { if tableColumn?.identifier == NSUserInterfaceItemIdentifier("ident"), let tcv = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("identifier"), owner: self) as? NSTableCellView { tcv.textField?.stringValue = "Input \(row)" return tcv } else if tableColumn?.identifier == NSUserInterfaceItemIdentifier("ingraph"), let tcv = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("ingraph"), owner: self) as? Graphico { tcv.sparkline.dataSource = inDataSources[row] return tcv } else if tableColumn?.identifier == NSUserInterfaceItemIdentifier("outgraph"), let tcv = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("outgraph"), owner: self) as? Graphico { tcv.sparkline.dataSource = outDataSources[row] return tcv } else if tableColumn?.identifier == NSUserInterfaceItemIdentifier("cpuGraph"), let tcv = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier("cpuGraph"), owner: self) as? Graphicodot { tcv.sparkline.dataSource = cpuDataSources[row] return tcv } return nil } } ================================================ FILE: Demos/Samples/macOS Table Demo/macOS_Table_Demo.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.files.user-selected.read-only ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/AppDelegate.swift ================================================ // // AppDelegate.swift // tvOS Sparkline Demo // // Created by Darren Ford on 23/12/19. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } func applicationWillResignActive(_ application: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. } func applicationDidEnterBackground(_ application: UIApplication) { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. } func applicationWillEnterForeground(_ application: UIApplication) { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. } func applicationDidBecomeActive(_ application: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json ================================================ { "layers" : [ { "filename" : "Front.imagestacklayer" }, { "filename" : "Middle.imagestacklayer" }, { "filename" : "Back.imagestacklayer" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json ================================================ { "layers" : [ { "filename" : "Front.imagestacklayer" }, { "filename" : "Middle.imagestacklayer" }, { "filename" : "Back.imagestacklayer" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json ================================================ { "assets" : [ { "size" : "1280x768", "idiom" : "tv", "filename" : "App Icon - App Store.imagestack", "role" : "primary-app-icon" }, { "size" : "400x240", "idiom" : "tv", "filename" : "App Icon.imagestack", "role" : "primary-app-icon" }, { "size" : "2320x720", "idiom" : "tv", "filename" : "Top Shelf Image Wide.imageset", "role" : "top-shelf-image-wide" }, { "size" : "1920x720", "idiom" : "tv", "filename" : "Top Shelf Image.imageset", "role" : "top-shelf-image" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" }, { "idiom" : "tv-marketing", "scale" : "1x" }, { "idiom" : "tv-marketing", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" }, { "idiom" : "tv", "scale" : "2x" }, { "idiom" : "tv-marketing", "scale" : "1x" }, { "idiom" : "tv-marketing", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities arm64 UIUserInterfaceStyle Automatic ================================================ FILE: Demos/Samples/tvOS Sparkline Demo/ViewController.swift ================================================ // // ViewController.swift // tvOS Sparkline Demo // // Created by Darren Ford on 23/12/19. // import UIKit import DSFSparkline class ViewController: UIViewController { @IBOutlet var green: DSFSparklineDataSourceView! var greenDataSource = DSFSparkline.DataSource(range: -11 ... 11) @IBOutlet var red: DSFSparklineLineGraphView! var redDataSource = DSFSparkline.DataSource(range: -45 ... 15) @IBOutlet weak var centeredRedDataSource: DSFSparklineLineGraphView! @IBOutlet var purple: DSFSparklineDotGraphView! var purpleDataSource = DSFSparkline.DataSource(range: 0 ... 50) @IBOutlet var orange: DSFSparklineDotGraphView! var orangeDataSource = DSFSparkline.DataSource(range: 0 ... 50) @IBOutlet var winLoss: DSFSparklineWinLossGraphView! var wlSource = DSFSparkline.DataSource(range: -1 ... 1) @IBOutlet var pie1: DSFSparklinePieGraphView! @IBOutlet var pie2: DSFSparklinePieGraphView! @IBOutlet var pie3: DSFSparklinePieGraphView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. self.winLoss.dataSource = self.wlSource let randWinLoss: [CGFloat] = (0 ..< 50).map { _ in CGFloat(Int.random(in: -1 ... 1)) } self.wlSource.set(values: randWinLoss) self.green.dataSource = self.greenDataSource self.red.dataSource = self.redDataSource self.centeredRedDataSource.dataSource = self.redDataSource self.purple.dataSource = self.purpleDataSource self.orange.dataSource = self.orangeDataSource self.pie1.animationStyle = DSFSparkline.AnimationStyle(duration: 2.0) self.pie1.dataSource = DSFSparkline.StaticDataSource([1, 2, 3]) self.pie2.animationStyle = DSFSparkline.AnimationStyle(duration: 2.0) self.pie2.dataSource = DSFSparkline.StaticDataSource([3, 2, 1]) self.pie3.animationStyle = DSFSparkline.AnimationStyle(duration: 2.0) self.pie3.dataSource = DSFSparkline.StaticDataSource([1, 7, 4, 9]) self.updateWithNewValues() } func updateWithNewValues() { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in guard let `self` = self else { return } _ = self.greenDataSource.push(value: CGFloat.random(in: self.greenDataSource.range!)) _ = self.redDataSource.push(value: CGFloat.random(in: -40 ... 10)) _ = self.purpleDataSource.push(value: CGFloat.random(in: self.purpleDataSource.range!)) _ = self.orangeDataSource.push(value: CGFloat.random(in: self.orangeDataSource.range!)) _ = self.wlSource.push(value: CGFloat(Int.random(in: -1 ... 1))) self.updateWithNewValues() } } } ================================================ FILE: Demos/Simple Wiper Gauge/Simple Wiper Gauge/AppDelegate.swift ================================================ // // AppDelegate.swift // Simple Wiper Gauge // // Created by Darren Ford on 4/1/23. // import Cocoa @main class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { // Insert code here to initialize your application } func applicationWillTerminate(_ aNotification: Notification) { // Insert code here to tear down your application } } ================================================ FILE: Demos/Simple Wiper Gauge/Simple Wiper Gauge/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Demos/Simple Wiper Gauge/Simple Wiper Gauge/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Demos/Simple Wiper Gauge/Simple Wiper Gauge/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Demos/Simple Wiper Gauge/Simple Wiper Gauge/Base.lproj/Main.storyboard ================================================ Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: Demos/Simple Wiper Gauge/Simple Wiper Gauge/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSMainStoryboardFile Main NSPrincipalClass NSApplication ================================================ FILE: Demos/Simple Wiper Gauge/Simple Wiper Gauge/Simple_Wiper_Gauge.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.files.user-selected.read-only ================================================ FILE: Demos/Simple Wiper Gauge/Simple Wiper Gauge/ViewController.swift ================================================ // // ViewController.swift // Simple Wiper Gauge // // Created by Darren Ford on 4/1/23. // import Cocoa import DSFSparkline class ViewController: NSViewController { @IBOutlet weak var wiperGauge: DSFSparklineWiperGaugeGraphView! override func viewDidLoad() { super.viewDidLoad() wiperGauge.value = 0.65 // Do any additional setup after loading the view. } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } @IBAction func valueDidUpdate(_ sender: NSSlider) { wiperGauge.value = CGFloat(sender.doubleValue) } } ================================================ FILE: Demos/Simple Wiper Gauge/Simple Wiper Gauge.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 2354FF5829651D9600376C3C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2354FF5729651D9600376C3C /* AppDelegate.swift */; }; 2354FF5A29651D9600376C3C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2354FF5929651D9600376C3C /* ViewController.swift */; }; 2354FF5C29651D9700376C3C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2354FF5B29651D9700376C3C /* Assets.xcassets */; }; 2354FF5F29651D9700376C3C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2354FF5D29651D9700376C3C /* Main.storyboard */; }; 2354FF702965202600376C3C /* DSFSparkline-static in Frameworks */ = {isa = PBXBuildFile; productRef = 2354FF6F2965202600376C3C /* DSFSparkline-static */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 2354FF5429651D9600376C3C /* Simple Wiper Gauge.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Simple Wiper Gauge.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 2354FF5729651D9600376C3C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 2354FF5929651D9600376C3C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 2354FF5B29651D9700376C3C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2354FF5E29651D9700376C3C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 2354FF6029651D9700376C3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2354FF6129651D9700376C3C /* Simple_Wiper_Gauge.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Simple_Wiper_Gauge.entitlements; sourceTree = ""; }; 2354FF6829651DA400376C3C /* DSFSparkline */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DSFSparkline; path = ../..; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 2354FF5129651D9600376C3C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 2354FF702965202600376C3C /* DSFSparkline-static in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 2354FF4B29651D9600376C3C = { isa = PBXGroup; children = ( 2354FF6829651DA400376C3C /* DSFSparkline */, 2354FF5629651D9600376C3C /* Simple Wiper Gauge */, 2354FF5529651D9600376C3C /* Products */, 2354FF6B29651DC300376C3C /* Frameworks */, ); sourceTree = ""; }; 2354FF5529651D9600376C3C /* Products */ = { isa = PBXGroup; children = ( 2354FF5429651D9600376C3C /* Simple Wiper Gauge.app */, ); name = Products; sourceTree = ""; }; 2354FF5629651D9600376C3C /* Simple Wiper Gauge */ = { isa = PBXGroup; children = ( 2354FF5729651D9600376C3C /* AppDelegate.swift */, 2354FF5929651D9600376C3C /* ViewController.swift */, 2354FF5B29651D9700376C3C /* Assets.xcassets */, 2354FF5D29651D9700376C3C /* Main.storyboard */, 2354FF6029651D9700376C3C /* Info.plist */, 2354FF6129651D9700376C3C /* Simple_Wiper_Gauge.entitlements */, ); path = "Simple Wiper Gauge"; sourceTree = ""; }; 2354FF6B29651DC300376C3C /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 2354FF5329651D9600376C3C /* Simple Wiper Gauge */ = { isa = PBXNativeTarget; buildConfigurationList = 2354FF6429651D9700376C3C /* Build configuration list for PBXNativeTarget "Simple Wiper Gauge" */; buildPhases = ( 2354FF5029651D9600376C3C /* Sources */, 2354FF5129651D9600376C3C /* Frameworks */, 2354FF5229651D9600376C3C /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Simple Wiper Gauge"; packageProductDependencies = ( 2354FF6F2965202600376C3C /* DSFSparkline-static */, ); productName = "Simple Wiper Gauge"; productReference = 2354FF5429651D9600376C3C /* Simple Wiper Gauge.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 2354FF4C29651D9600376C3C /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1230; LastUpgradeCheck = 1230; TargetAttributes = { 2354FF5329651D9600376C3C = { CreatedOnToolsVersion = 12.3; }; }; }; buildConfigurationList = 2354FF4F29651D9600376C3C /* Build configuration list for PBXProject "Simple Wiper Gauge" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 2354FF4B29651D9600376C3C; productRefGroup = 2354FF5529651D9600376C3C /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 2354FF5329651D9600376C3C /* Simple Wiper Gauge */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 2354FF5229651D9600376C3C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 2354FF5C29651D9700376C3C /* Assets.xcassets in Resources */, 2354FF5F29651D9700376C3C /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 2354FF5029651D9600376C3C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 2354FF5A29651D9600376C3C /* ViewController.swift in Sources */, 2354FF5829651D9600376C3C /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 2354FF5D29651D9700376C3C /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 2354FF5E29651D9700376C3C /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 2354FF6229651D9700376C3C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 2354FF6329651D9700376C3C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 2354FF6529651D9700376C3C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "Simple Wiper Gauge/Simple_Wiper_Gauge.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 3L6RK3LGGW; INFOPLIST_FILE = "Simple Wiper Gauge/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = "org.dagronf.dsfsparkline.Simple-Wiper-Gauge"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Debug; }; 2354FF6629651D9700376C3C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "Simple Wiper Gauge/Simple_Wiper_Gauge.entitlements"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = 3L6RK3LGGW; INFOPLIST_FILE = "Simple Wiper Gauge/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = "org.dagronf.dsfsparkline.Simple-Wiper-Gauge"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 2354FF4F29651D9600376C3C /* Build configuration list for PBXProject "Simple Wiper Gauge" */ = { isa = XCConfigurationList; buildConfigurations = ( 2354FF6229651D9700376C3C /* Debug */, 2354FF6329651D9700376C3C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 2354FF6429651D9700376C3C /* Build configuration list for PBXNativeTarget "Simple Wiper Gauge" */ = { isa = XCConfigurationList; buildConfigurations = ( 2354FF6529651D9700376C3C /* Debug */, 2354FF6629651D9700376C3C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ 2354FF6F2965202600376C3C /* DSFSparkline-static */ = { isa = XCSwiftPackageProductDependency; productName = "DSFSparkline-static"; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 2354FF4C29651D9600376C3C /* Project object */; } ================================================ FILE: Demos/Simple Wiper Gauge/Simple Wiper Gauge.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Demos/Simple Wiper Gauge/Simple Wiper Gauge.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "object": { "pins": [ { "package": "SwiftImageReadWrite", "repositoryURL": "https://github.com/dagronf/SwiftImageReadWrite", "state": { "branch": null, "revision": "42ace2412279f18bc264bc306e96b51c36e12a33", "version": "1.9.2" } } ] }, "version": 1 } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 Darren Ford Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.resolved ================================================ { "object": { "pins": [ { "package": "SwiftImageReadWrite", "repositoryURL": "https://github.com/dagronf/SwiftImageReadWrite", "state": { "branch": null, "revision": "42ace2412279f18bc264bc306e96b51c36e12a33", "version": "1.9.2" } } ] }, "version": 1 } ================================================ FILE: Package.swift ================================================ // swift-tools-version: 5.4 import PackageDescription let package = Package( name: "DSFSparkline", platforms: [ .macOS(.v10_13), .iOS(.v14), .tvOS(.v14) ], products: [ .library(name: "DSFSparkline", targets: ["DSFSparkline"]), .library(name: "DSFSparkline-static", type: .static, targets: ["DSFSparkline"]), .library(name: "DSFSparkline-shared", type: .dynamic, targets: ["DSFSparkline"]), ], dependencies: [ .package(url: "https://github.com/dagronf/SwiftImageReadWrite", from: "1.9.2"), ], targets: [ .target( name: "DSFSparkline", dependencies: []), .testTarget( name: "DSFSparklineTests", dependencies: ["DSFSparkline", "SwiftImageReadWrite"]), ] ) ================================================ FILE: README.md ================================================ # Sparklines for macOS, iOS and tvOS A lightweight sparkline component, supporting Swift, SwiftUI, macCatalyst and Objective-C.

CocoaPods Swift Package Manager

## What is a sparkline? A sparkline is a very small chart, typically drawn without axes or coordinates. It presents the general shape of the variation (typically over time) in some measurement, such as temperature or stock market price, in a simple and highly condensed way. Sparklines are small enough to be embedded in text, or several sparklines may be grouped together as elements of a small multiple. Whereas the typical chart is designed to show as much data as possible, and is set off from the flow of text, sparklines are intended to be succinct, memorable, and located where they are discussed. [Source: Wikipedia](https://en.wikipedia.org/wiki/Sparkline) ## What ISN'T a sparkline? `DSFSparkline` **IS NOT** designed to be a full-featured graphing library. It was built to be lightweight, to create small, memorable charts within an app. If you need features like labelling, real-time updating, axis-labelling, interactivity, legends or beautiful charts at larger sizes, you might be better served by the [Charts library](https://github.com/danielgindi/Charts), [Core plot](https://github.com/core-plot/core-plot) or [SciChart (paid)](https://www.scichart.com/ios-chart-features/). You can find a whole lot more [here](https://iosexample.com/tag/charts/). ## Features * Multiple graph styles support, such as line, bar, tablet etc. * Support for sparkline customizations, such as zero-line, grid lines, highlighting. * Prebuilt NSView/UIView/SwiftUI types for quick integration * Independently scalable for sparklines at any size * y-range can automatically grow or shrink to encompass the full y-range of data. * y-range can be fixed and the sparkline will truncate to the specified range * SwiftUI support for all sparkline types * NSAttributedString support * `IBDesignable` support for prebuilt types so you can see and configure your sparklines in interface builder. * Optional drawing of a 'zero line' on the bar and line graphs (thanks [Tito Ciuro](https://github.com/tciuro)) * Playground support ## TL;DR - Show me something!
Create a retina-scale (144dpi) bitmap with a simple line overlay graph ```swift // A datasource with a simple set of data let source = DSFSparkline.DataSource(values: [4, 1, 8, 7, 5, 9, 3], range: 0 ... 10) let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let stack = DSFSparklineOverlay.Line() // Create a line overlay stack.dataSource = source // Assign the datasource to the overlay bitmap.addOverlay(stack) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 50, height: 25, scale: 2) // Embed a sparkline in an NSAttributedString let attributedString = bitmap.attributedString(size: CGSize(width: 40, height: 18), scale: 2) ```
Create a Swift-UI line graph sparkline with zero-line and highlight range overlays ```swift fileprivate let SwiftUIDemoDataSource: DSFSparkline.DataSource = { let d = DSFSparkline.DataSource(windowSize: 20, range: 0 ... 1, zeroLineValue: 0.5) d.push(values: [ 0.72, 0.84, 0.15, 0.16, 0.30, 0.58, 0.87, 0.44, 0.02, 0.27, 0.48, 0.16, 0.15, 0.14, 0.81, 0.53, 0.67, 0.52, 0.07, 0.50 ]) return d }() struct SuperCoolLineSpark: View { // The overlay representing the zero-line for the data source var zeroOverlay: DSFSparklineOverlay = { let zeroLine = DSFSparklineOverlay.ZeroLine() zeroLine.dataSource = SwiftUIDemoDataSource zeroLine.dashStyle = [] return zeroLine }() // The overlay to draw a highlight between range 0 ..< 0.2 var rangeOverlay: DSFSparklineOverlay = { let highlight = DSFSparklineOverlay.RangeHighlight() highlight.dataSource = SwiftUIDemoDataSource highlight.highlightRange = 0.0 ..< 0.2 highlight.fill = DSFSparkline.Fill.Color(DSFColor.gray.withAlphaComponent(0.4).cgColor) return highlight }() // The actual line graph var lineOverlay: DSFSparklineOverlay = { let lineOverlay = DSFSparklineOverlay.Line() lineOverlay.dataSource = SwiftUIDemoDataSource lineOverlay.primaryStrokeColor = DSFColor.systemBlue.cgColor lineOverlay.primaryFill = DSFSparkline.Fill.Color(DSFColor.systemBlue.withAlphaComponent(0.3).cgColor) lineOverlay.secondaryStrokeColor = DSFColor.systemYellow.cgColor lineOverlay.secondaryFill = DSFSparkline.Fill.Color(DSFColor.systemYellow.withAlphaComponent(0.3).cgColor) lineOverlay.strokeWidth = 1 lineOverlay.markerSize = 4 lineOverlay.centeredAtZeroLine = true return lineOverlay }() var body: some View { DSFSparklineSurface.SwiftUI([ rangeOverlay, // range highlight overlay zeroOverlay, // zero-line overlay lineOverlay, // line graph overlay ]) .frame(width: 150, height: 40) } } ```
## Integration Use Swift Package Manager to integrate `DSFSparkline` into your project Add `https://github.com/dagronf/DSFSparkline` to your project. ### Note When adding `DSFSparkline` to one of your projects, you need to choose a **SINGLE** 'package product' to link to your target. Choosing multiple package products for your project will result in weird, inconsistent link errors. | Package Product | Description | |:-----------------------|:--------------------------------| | `DSFSparkline` | The default style. If you are just trying this library out, this is the one to choose | `DSFSparkline-static` | Add `DSFSparkline` as a STATIC library, meaning that all DSFSparkline code is linked directly into your target | | `DSFSparkline-shared` | Add `DSFSparkline` as a SHARED library, meaning that you can use a single shared framework between multiple targets in your project to save space | ## Available graph types ### Line A simple line sparkline. The line can be centered around a zero line to indicate positive and negative values. You can also add (optional) markers to the data points. You can custom-draw the markers by supplying a drawing callback block (`markerDrawingBlock`) where you can customize which markers are drawn (for example, just the min and max values) and how they are drawn. | Standard | Centered | |------------|------------| ||| | Interpolated | Interpolated Centered | |------------|------------| ||| | Standard Markers | Interpolated Markers | |------------|------------| ||| | Custom Markers (min/max only) | Custom Markers (Last 5 values) | |------------|------------| |||
Swift example using custom markers ```swift // // A custom marker drawing function that draws the maximum value in green, the minimum value in red // self.myLineView.markerDrawingBlock = { context, markerFrames in // Get the frames containing the minimum and maximum values if let minMarker = markerFrames.min(by: { (a, b) -> Bool in a.value < b.value }), let maxMarker = markerFrames.min(by: { (a, b) -> Bool in a.value > b.value }) { // Draw minimum marker context.setFillColor(DSFColor.systemRed.cgColor) context.fill(minMarker.rect) context.setLineWidth(0.5) context.setStrokeColor(DSFColor.white.cgColor) context.stroke(minMarker.rect) // Draw maximum marker context.setFillColor(DSFColor.systemGreen.cgColor) context.fill(maxMarker.rect) context.setLineWidth(0.5) context.setStrokeColor(DSFColor.white.cgColor) context.stroke(maxMarker.rect) } } ```
### Bar A simple barchart sparkline. The bar can be centered around a zero line to indicate positive and negative values. | Standard | Centered | |------------|------------| ||| ### Stackline A stackline sparkline is similar to a bar chart except that it doesn't separate out individual columns. The stackline can be centered around a zero line to indicate positive and negative values. | Standard | Centered | |------------|------------| ||| ### Stripes A stripes graph. A good example of a stripes graph is the 'warming stripes' [climate graph](https://www.climatecentral.org/showyourstripes). Values from the datasource are mapped to a supplied gradient | Standard | Integral (pixel boundaries) | |------------|-------------------------------| ||| ### Dot A dot sparkline reminiscent of Activity Viewer's CPU history graphs | Standard | Inverted | |------------|------------| ||| ### Win/Loss/Tie A win-loss graph, where positive values in the datasource are represented as a 'win', negative values represented as a 'loss', and zero values are a 'tie'. | Win/Loss | Win/Loss/Tie | |------------|------------| ||| ### Tablet A tablet graph, where positive values in the datasource are represented as a filled circle, negative values represented as an unfilled circle. The behaviour is identical to the win/loss graph. | Standard | |------------| || ### Pie A basic pie sparkline | Standard | |------------| || ### DataBar A databar sparkline. Supports percentage and totals types. | Percent | Total | |------------|------------| ||| ### PercentBar A percentbar sparkline takes a single value (0 ... 1) and draws a horizontal bar chart containing the value with an optional text label. | | | |------------|------------| ||| ### WiperGauge A simple gauge with a single value (0 ... 1). The color displayed in the gauge can be one of * a single flat color * a color mapped from a gradient to the value * a color mapped from a color bucket to the value | Standard | |------------| || ### Activity Grid A Github-style activity grid. Each cell represents a discrete value in a progression with a color * Configurable horizontal/vertical cell count * Configurable cell spacing * Configurable fill schemes | | | |------------|------------| ||| ### Circular Progress A circular progress graph * Configurable track width * Optional track background color * Configurable track padding to allow overlaying multiple progress graphs to replicate the health rings. * Configurable fill schemes | Simulated health rings | |------------------------| |   | ### Circular Gauge A circular gauge * Configurable track/line width * Configurable fill/stroke schemes * Supports shadows/inner shadows * Animatable changes | | | | | |------|-------|-------|-------| | | | | | # Demos You can find a lot of examples of sparklines in projects in the `Demos/Samples` subfolder, providing examples for macOS (swift and objc), iO, tvOS, macCatalyst and SwiftUI. There's also a simple Xcode Playground available in the `Demos/Playground` subfolder where you can play with the different sparkline types # Building your sparkline ### Using prebuilt views A prebuilt view is useful to quickly add a sparkline using SwiftUI or via Interface Builder. These views a slightly more limited set of styling and customizations but are much quicker to implement than using overlays directly. For the most part, the prebuilt types will be all you need. If you've used `DSFSparklines` prior to v4, these are the original view types that you used to display your sparklines. * **A datasource** - the set of values to draw * **A prebuilt view type** - the NSView/UIView/SwiftUI view to draw your sparkline ### Using overlays There are three fundamental building blocks for an overlay sparkline. Overlay sparklines are more flexible and configurable than the pre-built views, but are more complex to set up. * **A surface** - where it will draw * **A datasource** - the set of values to draw * **One or more overlays** - the 'layers' which render different components of the sparkline ## Surface A surface represents a destination for a sparkline. This library provides a number of built-in surfaces * `DSFSparklineSurfaceView` - An `NSView`/`UIView` surface for displaying a sparkline * `DSFSparklineSurface.SwiftUI` - A SwiftUI `View` surface. * `DSFSparklineSurface.Bitmap` - A `NSImage`/`UIImage`/`CGImage`/`NSAttributedString` surface for creating a bitmap from a sparkline. ## DataSource A data source provides data for a sparkline. A datasource can be shared between multiple overlays or prebuilt types (see below) to provide different 'views' of the data contained within the source. And if a `DataSource` is updated, all sparkline ovelays observing that source will be automatically re-rendered. There are currently two types of datasource available ### DSFSparkline.DataSource A DataSource that contains values that can be updated by pushing new values into the source.
More details #### WindowSize The DataSource defines a 'windowSize' - the maximum number of values to be drawn on the overlay. As values are pushed into the DataSource, any values that no longer `fit` within the DataSource window are discarded. * If the window size is reduced, stored data is truncated. * If the window size is increased, the data store is padded with zeros
Code example ```swift /// Swift dataSource.windowSize = 30 assert(dataSource.windowSize == 30) ``` ```objective-c /// Objective-C [dataSource setWindowSize:30]; assert([dataSource windowSize] == 30); ```
#### Y-range The range defines the upper and lower values to be displayed in the sparkline. Any values pushed into the datasource will be capped when drawn to this range. If the range is not set (ie nil), then any overlays will automatically resize to fit the entire range of values within the source. For example, with values as [1, 2, 3, 4] the range is implicitly set as 1 ... 4. If the values are [-10, 100, 33] the range is implicitly set as -10 ... 100
Code example ```swift /// Swift dataSource.range = -1.0 ... 1.0 ``` ```objective-c /// Objective-C [dataSource setRangeWithLowerBound:-1.0 upperBound:1.0]; ```
#### Zero-line value The zero-line defines the point the sparkline overlays should consider to be 'zero'. For example, graphs that can be centered (line, bar and stackline) use the zero-line value to define where the graph is centered around. The zero-line value defaults to zero. You can draw a zero-line for a sparkline by adding a `DSFSparklineOverlay.ZeroLine` to your surface.
Code example ```swift /// Swift dataSource.zeroLineValue = 0.2 ``` ```objc /// Objective-C [dataSource setZeroLineValue:0.2]; ```
#### Adding values You can push new values into the datasource using the `push` functions. Values in the datasource older than the datasource's `windowSize` are discarded. As values are pushed into the datasource, any overlays assigned this datasource will automatically update.
Code example ```swift /// Swift dataSource.push(value: 4.5) dataSource.push(values: [6, 7, 8]) ``` ```objective-c /// Objective-C [dataSource pushWithValue:@(4.5)]; ```
You replace all the values in a datasource the `set` functions. The set function also changes the `windowSize` for the datasource to the size of the values array passed in. Any overlays assigned this datasource will automatically update.
Code example ```swift /// Swift datasource.set(values: [ 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 0.0, -0.1, -0.2, -0.3, -0.4, -0.5, -0.6, -0.7, -0.8, -0.9, -1.0 ]) ``` ```objc /// Objective-C [datasource setWithValues: @[@(0.0), @(0.1), @(0.2), @(0.3), @(0.4), @(0.5), @(0.6), @(0.7), @(0.8), @(0.9), @(1), @(0.0), @(-0.1), @(-0.2), @(-0.3), @(-0.4), @(-0.5), @(-0.6), @(-0.7), @(-0.8), @(-0.9), @(-1)]]; ```
### DSFSparkline.StaticDataSource A datasource that contains a static set of values. Some types of sparkline use a single 'set' of data, providing no historical context.
More details ```swift /// Swift let dataSource = DSFSparkline.StaticDataSource([1, 2, 3]) ``` ```objc /// Objective-C DSFSparklineStaticDataSource* dataSource = [[DSFSparklineStaticDataSource alloc] init: @[@(1), @(2), @(3)]]; ```
## Overlays Overlays represent the individual visual components of a sparkline. You can add as many or as few to your surface in any order. For example, you could overlay two different graph types onto the same surface using the same. And as overlays can share their datasource, all overlays using the same source will automatically update if the data changes (for example, in reponse to a `push`) For example, there is an overlay that highlights a y-range of data. Or, if you want some grid lines, you can add them using the gridlines overlay. You can add different instances of an overlay to the same sparkline. For example, if you want to add multiple range highlights you add multiple 'highlight' overlays to the sparkline surface. The order in which the overlays are added determine where in the z-order that they appear in the sparkline. For example, you can choose to draw the grid on top of the graph if you want by adding the graph overlay BEFORE you add the grid overlay
The overlay allows your sparkline to be as complex or as simple as you want. ### Graph types #### Dynamic A dynamic graph automatically updates its overlays as values are 'pushed' onto its datasource. As data is added the assigned overlay is automatically updated to reflect the new data. If more data is added via a push or set the data is added to the datasource, the associated view will automatically update to reflect the new data. Older data that no longer falls within the datasource window is discarded. This provides the ability to show a historical data set over the breadth of the graph.
`DSFSparklineOverlay.Line` ```swift let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let line = DSFSparklineOverlay.Line() // Create a line overlay line.strokeWidth = 1 line.primaryFill = primaryFill line.dataSource = source // Assign the datasource to the overlay bitmap.addOverlay(line) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 50, height: 25, scale: 2)! // Do something with 'image' ```
`DSFSparklineOverlay.StackLine` ```swift let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let stack = DSFSparklineOverlay.Stackline() // Create a stackline overlay stack.dataSource = source // Assign the datasource to the overlay stack.strokeWidth = 1 stack.primaryFill = primaryFill bitmap.addOverlay(stack) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 50, height: 25, scale: 2)! // Do something with 'image' ```
`DSFSparklineOverlay.Bar` ```swift let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let bar = DSFSparklineOverlay.Bar() // Create a bar overlay bar.dataSource = source // Assign the datasource to the overlay bar.primaryFill = primaryFill bitmap.addOverlay(bar) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 50, height: 25, scale: 2)! // Do something with 'image' ```
`DSFSparklineOverlay.Dot` ```swift let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let dot = DSFSparklineOverlay.Dot() // Create a dot graph overlay dot = biggersource // Assign the datasource to the overlay bitmap.addOverlay(dot) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 50, height: 32, scale: 2)! // Do something with 'image' ```
`DSFSparklineOverlay.WinLossTie` ```swift let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let winLossTie = DSFSparklineOverlay.WinLossTie() // Create a win-loss-tie overlay winLossTie.dataSource = winloss // Assign the datasource bitmap.addOverlay(winLossTie) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 75, height: 12, scale: 2)! // Do something with 'image' ```
`DSFSparklineOverlay.Tablet` ```swift let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let stack = DSFSparklineOverlay.Tablet() // Create a tablet overlay stack.dataSource = winloss // Assign a datasource to the overlay bitmap.addOverlay(stack) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 90, height: 16, scale: 2)! // Do something with 'image' ```
`DSFSparklineOverlay.Stripes` ```swift let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let stack = DSFSparklineOverlay.Stripes() // Create a stripes overlay stack.dataSource = .init(values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) bitmap.addOverlay(stack) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 90, height: 16, scale: 2) // Do something with 'image' ```
#### Static A static graph has a fixed set of values (for example, a pie chart). The overlays update when a new static data source is assigned to it.
`DSFSparklineOverlay.Pie` ```swift let bitmap = DSFSparklineSurface.Bitmap() let pie = DSFSparklineOverlay.Pie() pie.dataSource = DSFSparkline.StaticDataSource([10, 55, 20]) pie.lineWidth = 0.5 pie.strokeColor = CGColor.black bitmap.addOverlay(pie) // Generate an image with retina scale let image = bitmap.image(width: 18, height: 18, scale: 2)! // Do something with 'image' ```
`DSFSparklineOverlay.DataBar` ```swift let bitmap = DSFSparklineSurface.Bitmap() let stack = DSFSparklineOverlay.DataBar() stack.dataSource = DSFSparkline.StaticDataSource([10, 20, 30]) stack.lineWidth = 0.5 stack.strokeColor = CGColor.black bitmap.addOverlay(stack) // Generate an image with retina scale let image = bitmap.image(width: 50, height: 18, scale: 2)! // Do something with 'image' ```
`DSFSparklineOverlay.PercentBar` ```swift let bitmap = DSFSparklineSurface.Bitmap() let percentBar = DSFSparklineOverlay.PercentBar(value: 0.3) bitmap.addOverlay(percentBar) // Generate an image with retina scale let image = bitmap.image(width: 50, height: 18, scale: 2)! // Do something with 'image' ```
`DSFSparklineOverlay.WiperGauge` ```swift let bitmap = DSFSparklineSurface.Bitmap() let wiperGauge = DSFSparklineOverlay.WiperGauge() wiperGauge.value = 0.75 bitmap.addOverlay(wiperGauge) // Generate an image of the wiper gauge with retina scale let image = bitmap.image(width: 50, height: 25, scale: 2) // Do something with 'image' ```
`DSFSparklineOverlay.ActivityGrid` ```swift let bitmap = DSFSparklineSurface.Bitmap() let activityGrid = DSFSparklineOverlay.ActivityGrid() activityGrid.dataSource = DSFSparkline.StaticDataSource(values: [...]) activityGrid.verticalCellCount = 1 bitmap.addOverlay(activityGrid) // Generate an image of the wiper gauge with retina scale let image = bitmap.image(width: 200, height: 14, scale: 2) // Do something with 'image' ```
`DSFSparklineOverlay.CircularGauge` ```swift let bitmap = DSFSparklineSurface.Bitmap() let gauge = DSFSparklineOverlay.CircularGauge() gauge.value = 0.66 bitmap.addOverlay(gauge) // Generate an image of the wiper gauge with retina scale let image = bitmap.image(width: 40, height: 40, scale: 2) // Do something with 'image' ```
### Component types A component represents an overlay that isn't a graph in itself. Examples are grid lines, zero-lines, highlights etc. A component uses the same datasource so that it aligns with the graph it is associated with. | Name | Description | |------------|------------| | `DSFSparklineOverlay.ZeroLine` | Draw a horizontal line at the 'zero-line' position of the sparkline. The zero-line is defined by the datasource and is by default zero, however this can be changed. | | `DSFSparklineOverlay.RangeHighlight` | Highlight a range of y-values on the sparkline | | `DSFSparklineOverlay.GridLines` | Draw lines at specified y-values on the sparkline | ## Using prebuilt views DSFSparkline has a number of 'prebuilt' sparkline views available with a more limited scope, designed to be quicker to add to your project. Every prebuilt sparkline view has a SwiftUI companion view. ### Available prebuilt types * `DSFSparklineLineGraphView` / `DSFSparklineLineGraphView.SwiftUI` * `DSFSparklineStackLineGraphView` / `DSFSparklineLineGraphView.SwiftUI` * `DSFSparklineBarGraphView` / `DSFSparklineBarGraphView.SwiftUI` * `DSFSparklineStripesGraphView` / `DSFSparklineStripesGraphView.SwiftUI` * `DSFSparklineDotGraphView` / `DSFSparklineDotGraphView.SwiftUI` * `DSFSparklineWinLossGraphView` / `DSFSparklineWinLossGraphView.SwiftUI` * `DSFSparklineTabletGraphView` / `DSFSparklineTabletGraphView.SwiftUI` * `DSFSparklinePieGraphView` / `DSFSparklinePieGraphView.SwiftUI` * `DSFSparklineDataBarGraphView` / `DSFSparklineDataBarGraphView.SwiftUI` * `DSFSparklinePercentBarGraphView` / `DSFSparklinePercentBarGraphView.SwiftUI` * `DSFSparklineWiperGaugeGraphView` / `DSFSparklineWiperGaugeGraphView.SwiftUI` * `DSFSparklineActivityGridView` / `DSFSparklineActivityGridView.SwiftUI` * `DSFSparklineCircularGaugeView` / `DSFSparklineCircularGaugeView.SwiftUI`
Sample Swift code ```swift // Create the view let sparklineView = DSFSparklineLineGraphView(…) sparklineView.graphColor = UIColor.blue sparklineView.showZeroLine = true // Create the datasource and assign to the view let sparklineDataSource = DSFSparklineDataSource(windowSize: 30, range: -1.0 ... 1.0) sparklineView.dataSource = sparklineDataSource … // Add a single new data element to the sparkline sparklineDataSource.push(value: 0.7) // view automatically updates with new data // Add a set of data to the sparkline sparklineDataSource.push(values: [0.3, -0.2, 1.0]) // view automatically updates with new data // Completely replace the sparkline data with a new set of data sparklineDataSource.set(values: [0.2, -0.2, 0.0, 0.9, 0.8]) // view automatically resets to new data ```
Sample SwiftUI code ```swift struct SparklineView: View { let leftDataSource: DSFSparkline.DataSource let rightDataSource: DSFSparkline.DataSource let BigCyanZeroLine = DSFSparkline.ZeroLineDefinition( color: .cyan, lineWidth: 3, lineDashStyle: [4,1,2,1] ) var body: some View { HStack { DSFSparklineLineGraphView.SwiftUI( dataSource: leftDataSource, graphColor: DSFColor.red, interpolated: true) DSFSparklineBarGraphView.SwiftUI( dataSource: rightDataSource, graphColor: DSFColor.blue, lineWidth: 2, showZeroLine: true, zeroLineDefinition: BigCyanZeroLine) } } } ```
Prebuilt customizations ## View Types and settings Represents the viewable settings and display. The current view types available are :- ### Common display customizations | Setting | Type | Description | |-----------------------|------------------------|---------------------------------------------------------| | `graphColor` | `NSColor`
`UIColor` | The color to use when drawing the sparkline | #### Common elements for graphs that can display a zero line (Line/Bar/Stackline) | Setting | Type | Description | |-----------------------|-------------------------|---------------------------------------------------------| | `dataSource` | `DSFDataSource` | The source of data for the graph | | `showZeroLine` | `Bool` | Draw a dotted line at the zero line point on the y-axis | | `zeroLineColor` | `NSColor`
`UIColor` | The color of the 'zero line' on the y-axis. | | `zeroLineWidth` | `CGFloat` | The width of the 'zero line' on the y-axis | | `zeroLineDashStyle` | `[CGFloat]` | The dash pattern to use when drawing the zero line | #### Common elements for graphs that can be centered around the zero-line (Line/Bar/Stackline) | Setting | Type | Description | |-----------------------|-------------------------|-------------------------------------------------------------------------------------------| | `centeredAtZeroLine` | `Bool` | Should the graph be centered at the zero line? | | `lowerGraphColor` | `NSColor`
`UIColor` | The color used to draw values below the zero line. If nil, is the same as the graph color | ### Line graph customizations (`DSFSparklineLineGraphView`) | Setting | Type | Description | |-------------------|-----------------|----------------------------------| | `dataSource` | `DSFDataSource` | The source of data for the graph | | `lineWidth` | `CGFloat` | The width of the line | | `interpolation` | `Bool` | Interpolate a curve between the points | | `lineShading` | `Bool` | Shade the area under the line | | `shadowed` | `Bool` | Draw a shadow under the line | | `markerSize` | `CGFloat` | (optional) Draw a marker of the specified size at every data point in the graph using the line color at that point | | `markerSize` | `CGFloat` | (optional) Draw a marker of the specified size at every data point in the graph using the line color at that point | | `markerDrawingBlock` | | A callback block to allow custom drawing of markers (if `markerSize` is > 1) | ### Bar graph customizations (`DSFSparklineBarGraphView`) | Setting | Type | Description | |--------------|-----------------|----------------------------------| | `dataSource` | `DSFDataSource` | The source of data for the graph | | `lineWidth` | `CGFloat` | The width of the line | | `barSpacing` | `CGFloat` | The spacing between each bar | ### Stripes graph customizations (`DSFSparklineStripesGraphView`) | Setting | Type | Description | |--------------|-----------------|---------------------------------------------------------------------| | `dataSource` | `DSFDataSource` | The source of data for the graph | | `integral` | `Bool` | If true, draws the bars on pixel boundaries to get nice clean lines | | `gradient` | `DSFGradient` | The color gradient to use when mapping datasource values to colors. | ### Dot graph customizations (`DSFSparklineDotGraphView`) | Setting | Type | Description | |-------------------|------------------------|----------------------------------------------------| | `dataSource` | `DSFDataSource` | The source of data for the graph | | `upsideDown` | `Bool` | If true, draws from the top of the graph downwards | | `unsetGraphColor` | `NSColor`
`UIColor` | The color to use when drawing the background | ### Win/Loss graph customizations (`DSFSparklineWinLossGraphView`) | Setting | Type | Description | |----------------|------------------------|-----------------------------------| | `dataSource` | `DSFDataSource` | The source of data for the graph | | `lineWidth` | `CGFloat` | The line width for the stroke | | `barSpacing` | `CGFloat` | The spacing between each bar | | `winColor` | `NSColor`
`UIColor` | The color to use for a 'win' | | `lossColor` | `NSColor`
`UIColor` | The color to use for a 'loss' | | `tieColor` | `NSColor`
`UIColor` | *(optional)* The color to use for a 'tie'. If nil, tie (0) values are not drawn
By default, 'tie' values are not drawn. | ### Tablet graph customizations (`DSFSparklineTabletGraphView`) | Setting | Type | Description | |--------------|------------------------|--------------------------------------------------| | `dataSource` | `DSFDataSource` | The source of data for the graph | | `lineWidth` | `CGFloat` | The line width for the stroke | | `barSpacing` | `CGFloat` | The spacing between each bar | | `winColor` | `NSColor`
`UIColor` | The color to draw the filled circle for a 'win' | | `lossColor` | `NSColor`
`UIColor` | The color to draw the filled circle for a 'loss' | ### Pie graph customizations (`DSFSparklinePieGraphView`) | Setting | Type | Description | |---------------------|------------------------|-----------------------------------------------------------------------------------------| | `dataSource` | `[CGFloat]` | The data to display in the pie chart | | `palette` | `DSFSparklinePalette` | The palette to use when drawing the chart | | `strokeColor` | `NSColor`
`UIColor` | (optional) The color of the line to draw between each segment. If nil, no line is drawn | | `lineWidth` | `CGFloat` | The width of the lines to draw between each segment | | `animated` | `Bool` | If true, when the data source is set the segments animate into view | | `animationDuration` | `CGFloat ` | The duration of the animation | ### Databar graph customizations (`DSFSparklineDataBarGraphView`) | Setting | Type | Description | |---------------------|------------------------|------------------------------------------------------------------------------------------------------------------------| | `dataSource` | `[CGFloat]` | The data to display in the pie chart | | `maximumTotalValue` | `CGFloat` | If <= 0 the data represents a percentage of the total, if > 0 represents the rightmost value to the databar | | `palette` | `DSFSparklinePalette` | The palette to use when drawing. | | `strokeColor` | `NSColor`
`UIColor` | (optional) The color of the line to draw between each segment. If nil, no line is drawn | | `lineWidth` | `CGFloat` | The width of the lines to draw between each segment | | `unsetColor` | `NSColor`
`UIColor` | (optional) If the maximum value is set, if the segments don't fit the total this color is used as the background color | | `animated` | `Bool` | If true, when the data source is set the segments animate into view | | `animationDuration` | `CGFloat` | The duration of the animation | ### Percent Bar graph customizations (`DSFSparklinePercentBarGraphView`) | Setting | Type | Description | |---------------------|------------------------|--------------------------------------------------------| | `value` | `CGFloat` | The value to display in the pie chart | | `displayStyle` | `Style` | The style to apply when drawing the graph | | `cornerRadius` | `CGFloat` | The corner radius for the bar. | | `showLabel` | `Bool` | Should we draw a text label on the percent bar? | | `underBarColor` | `NSColor`
`UIColor` | The background of the bar color for the percent bar chart | | `underBarTextColor` | `NSColor`
`UIColor` | The color for text displayed on the background | | `barColor` | `NSColor`
`UIColor` | The bar color for the percent bar chart | | `barTextColor` | `NSColor`
`UIColor` | The color for text displayed on the bar | | `fontName` | `String` | The name of the label font | | `fontSize` | `CGFloat` | The size of the label font | | `shouldAnimate` | `Bool` | If true, when the data source is set the segments animate into view | | `animationDuration` | `CGFloat` | The duration of the animation | | `leftInset`. | `CGFloat` | The bar's inset from the left of the control | | `topInset` | `CGFloat` | The bar's inset from the top of the control | | `rightInset` | `CGFloat` | The bar's inset from the right of the control | | `bottomInset` | `CGFloat` | The bar's inset from the bottom of the control |
## Screenshots ### In app | macOS dark | macOS light | iOS | |----|----|----| | | | | ### Interface Builder | macOS | tvOS | |----|----| ||| ### SwiftUI ### NSAttributedString support ### Animated ![](https://github.com/dagronf/dagronf.github.io/raw/master/art/projects/DSFSparkline/DSFSparkline_lots.gif) ## Changes ### `7.0.0` * Removed support for macOS 10.11, 10.12 * Removed support for iOS 13, tvOS 13 * Removed CocoaPods support (if you need Cocoapods, stick with v6). ### `6.0.0` * Added CircularGauge, CircularProgress sparkline types * Removed support for `@IBDesignable` and `@IBInspectable` from the NSView/UIView implementations, as Xcode has indicated that it will be dropping support in the near future ### `5.2.0` * Added grid-lines support for pre-built views (line, bar, stack) (NSView/UIView/SwiftUI) ### `5.1.0` * Added Activity Grid ### `5.0.0` * Fixed [occasional clipping](https://github.com/dagronf/DSFSparkline/issues/13) on interpolated line graphs. ### `4.6.0` * Added WiperGauge sparkline type ### `4.3.0` * Added the ability to custom-draw markers for the 'line' sparkline type [(raised issue)](https://github.com/dagronf/DSFSparkline/issues/6) * Fixed a minor issue where insetting certain graph types to remove clipping (line with markers, bar) would mean that zeroline, highlight overlays and grids would be appear to be very slightly off. ### `4.2.0` * Added 'percent bar' sparkline type. ### `4.1.2` * Fixed compile issue [Cannot find type 'NSTextAttachment' in scope" crashes when using SwiftPackage](https://github.com/dagronf/DSFSparkline/issues/5) ### `4.1.1` * Fixed gradient bucket count issue. * Added AttributedString SwiftUI demo to the ReportView demo. Demo uses [The SwiftUI Lab Attributed String](https://swiftui-lab.com/attributed-strings-with-swiftui/) with AppKit bug fix for width calculation. ### `4.1.0` * Embed sparklines in NSAttributedString. ### `4.0.0` Substantial re-architect of the drawing code (that used to be directly in the views) into overlays and surfaces that are far more flexible (for example, being able to draw a sparkline bitmap without having to create a view) The previous view/swiftui types are still available - they have been rebuilt on using the new overlay scheme and are referred to in documentation as 'prebuilt' types. This allowed backwards compatibility with previous versions of the library. Note however that given that the prebuilt views have been re-written there is a possibility of slight visual differences. ### `3.7.0` * Added stripe graph ### `3.6.1` * Fixed animations on iOS/tvOS ### `3.6.0` * Added pie chart, databar chart. * Added ability to show data markers for line graphs ### `3.5.2` * Fixed Objective-C Demo app * Added `snapshot` method to the base sparkline view class to produce an NSImage/UIImage version of the sparkline for embedded sparklines in text etc. ### `3.5.1` * Fixed version in podspec ### `3.5.0` * Added stackline sparkline type * Added win/loss/tie sparkline type * Added tablet sparkline type ### `3.4.0` * Added support for centering line and bar graphs around their zero-line value. ### `3.3.0` * Fixed issue where iOS background wasn't being drawn correctly in some cases. * Fixed rare crash where a line graph with < 2 points would crash. ### `3.2.0` * Changed the zero-line definition class to `DSFSparklineZeroLineDefinition` for clarity. * More documentation, especially around SwiftUI. Attempted to make the documentation clearer around drawing parameters. ### `3.1.0` * Add the ability to customize the zero-line display ([Tito Ciuro](https://github.com/tciuro)) * Changed `showZero` to `showZeroLine` for consistency with the new zero-line display values ### `3.0.0` * Add the ability to set the 'zero' line value. Defaults to zero for backwards compatibility. You can set where the 'zero' line draws via the `zeroLineValue` on the datasource. ### `2.0.0` * The primary views have been renamed with a `View` postfix. So `DSFSparklineLineGraph` -> `DSFSparklineLineGraphView` `DSFSparklineBarGraph` -> `DSFSparklineBarGraphView` `DSFSparklineDotGraph` -> `DSFSparklineDotGraphView` * Renamed `SLColor` and `SLView` to `DSFColor` and `DSFView` for module naming consistency. * I removed `windowSize` from the core `DSFSparklineDataSourceView`. `windowSize` is related to data, and should never have been part of the UI definition. I've provided a replacement purely for `IBDesignable` support called `graphWindowSize` which should only be called from Interface Builder. If you want to set the windowSize from your xib file, set the `graphWindowSize` inspectable. If you see warnings in the log like `2020-12-07 18:22:51.619867+1100 iOS Sparkline Demo[75174:1459637] Failed to set (windowSize) user defined inspected property on (DSFSparkline.DSFSparklineBarGraphView): [ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key windowSize. ` it means that you have a `windowSize` value set in your .xib file. Remove it and set the `graphWindowSize` value instead. * For the Bar type, `lineWidth` and `barSpacing` now represent the pixel spacing between bars and the pixel width for the line. You may find that your line spacing and bar spacing are now incorrect if you have set fractional values for these in the past (for example, if you set lineWidth = 0.5). The reason for this change is to aid drawing lines on pixel boundaries and avoid antialiasing. * Fix for zero line being upside-down ## License MIT. Use it for anything you want, just attribute my work. Let me know if you do use it somewhere, I'd love to hear about it! ``` MIT License Copyright (c) 2025 Darren Ford Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ================================================ FILE: Sources/DSFSparkline/DSFSparklines.h ================================================ // // DSFSparklines.h // DSFSparklines // // Created by Darren Ford on 20/6/19. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #import //! Project version number for DSFSparklines. FOUNDATION_EXPORT double DSFSparklinesVersionNumber; //! Project version string for DSFSparklines. FOUNDATION_EXPORT const unsigned char DSFSparklinesVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import ================================================ FILE: Sources/DSFSparkline/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright Copyright © 2019 Darren Ford. All rights reserved. ================================================ FILE: Sources/DSFSparkline/datasource/DSFSparkline+DataSource.swift ================================================ // // DSFSparkline+DataSource.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics public extension DSFSparkline { /// A datasource for a sparkline @objc(DSFSparklineDataSource) class DataSource: NSObject { public static let DefaultWindowSize: UInt = 10 /// Notification sent when the content of the data source changes @objc(DSFSparklineDataSourceDataChangedNotification) static let DataChangedNotification = NSNotification.Name("DSFSparklineDataSource.DataChanged") private let sparkline: SparklineWindow @objc public override init() { self.sparkline = SparklineWindow(windowSize: DataSource.DefaultWindowSize) super.init() } /// Create a data source /// - Parameters: /// - windowSize: The size of the window to use /// - range: (optional) the clamped y-range of data to display. Values outside this range are clamped /// - zeroLineValue: the zero-line value for the source. Defaults to zero (0) public init(windowSize: UInt = DataSource.DefaultWindowSize, range: ClosedRange? = nil, zeroLineValue: CGFloat = 0) { self.sparkline = SparklineWindow(windowSize: windowSize) self.sparkline.yRange = range self.sparkline.zeroLineValue = zeroLineValue super.init() } /// Create a datasource from a series of values /// - Parameters: /// - values: The values to initially assign to the datasource /// - range: (optional) The maximum range to represent. public convenience init(values: [CGFloat], range: ClosedRange? = nil) { self.init(windowSize: UInt(values.count), range: range) self.set(values: values) } } } public extension DSFSparkline.DataSource { override var description: String { """ DataSource: windowSize: \(sparkline.windowSize), zeroLine: \(sparkline.zeroLineValue), range: \(String(describing: sparkline.yRange)), emptyCount: \(sparkline.emptyValueCount)) data: \(sparkline.raw) norm: \(sparkline.normalized) """ } } // MARK: - Value handling public extension DSFSparkline.DataSource { /// The series of data points with the most recent being the last array entry @objc var data: [CGFloat] { self.sparkline.raw } /// Return the raw values in the data source scaled from 0.0 -> 1.0 @objc var normalized: [CGFloat] { self.sparkline.normalized } /// The number of data points to display in the sparkline @objc var windowSize: UInt { get { self.sparkline.windowSize } set { self.sparkline.windowSize = newValue self.notifyDataChange() } } /// The number of unfilled buckets in the sparkline @objc var emptyValueCount: UInt { return self.sparkline.emptyValueCount } /// The 'zero' line for drawing the horizontal line. Should be in the range lowerBound ..< upperBound @objc var zeroLineValue: CGFloat { get { self.sparkline.zeroLineValue } set { self.sparkline.zeroLineValue = newValue self.notifyDataChange() } } // Is the value at the specified offset in the data source BELOW the zero-line value? @inlinable func valueAtOffsetIsBelowZeroline(_ offset: Int) -> Bool { return self.data[offset] < self.zeroLineValue } } public extension DSFSparkline.DataSource { /// Add a new value. If there are more values than the window size, the oldest value is discarded @discardableResult @objc func push(value: CGFloat) -> Bool { defer { self.notifyDataChange() } return self.sparkline.push(value: value) } /// Add a vector of new values. Equivalent to push(values[0]), push(values[1]), push(values[2]) etc. @objc func push(values: [CGFloat]) { self.sparkline.push(values: values) self.notifyDataChange() } /// Set the sparkline data to the specified values. The window size is changed to reflect the extent of the input data @objc func set(values: [CGFloat]) { self.sparkline.set(values: values) self.notifyDataChange() } /// Reset the data to the lower bound for all data points in the window @objc func reset() { self.sparkline.reset() self.notifyDataChange() } } // MARK: - Range support (Swift) public extension DSFSparkline.DataSource { /// The current minimum/maximum range for the values, or nil if there is no range specified var range: ClosedRange? { get { self.sparkline.yRange } set { self.sparkline.yRange = newValue self.notifyDataChange() } } } // MARK: - Range support (Objc) public extension DSFSparkline.DataSource { /// Returns the lower bound for the current set of values. If no values are present, returns CGFloat.greatestFiniteMagnitude @objc var lowerBound: CGFloat { return self.sparkline.yRange?.lowerBound ?? CGFloat.greatestFiniteMagnitude } /// Returns the upper bound for the current set of values. If no values are present, returns CGFloat.greatestFiniteMagnitude @objc var upperBound: CGFloat { return self.sparkline.yRange?.upperBound ?? CGFloat.greatestFiniteMagnitude } /// Set a range using discrete upper and lower bounds @objc func setRange(lowerBound: CGFloat, upperBound: CGFloat) { assert(lowerBound < upperBound) self.sparkline.yRange = lowerBound ... upperBound self.notifyDataChange() } /// Set a range using discrete upper and lower bounds, drawing a line at the 'zero' point within the range @objc func setRange(lowerBound: CGFloat, upperBound: CGFloat, zeroLinePoint: CGFloat) { assert(lowerBound <= zeroLinePoint && zeroLinePoint <= upperBound) self.sparkline.yRange = lowerBound ... upperBound self.sparkline.zeroLineValue = zeroLinePoint self.notifyDataChange() } /// Remove the range restrictions for the data source @objc func resetRange() { self.range = nil self.notifyDataChange() } } // MARK: - Internal extension DSFSparkline.DataSource { /// The number of items added since last reset var counter: UInt { return self.sparkline.counter } // Normalize the specified value within 0.0 ... 1.0 for the current data func normalize(value: CGFloat) -> CGFloat { return self.sparkline.normalize(value: value) } // Normalize the zero-line value within 0.0 ... 1.0 for the current data var normalizedZeroLineValue: CGFloat { return self.normalize(value: self.zeroLineValue) } fileprivate func notifyDataChange() { NotificationCenter.default.post(name: DSFSparkline.DataSource.DataChangedNotification, object: self) } } public extension DSFSparkline.DataSource { /// Return the vertical fractional position within the data window that represents /// zero for the current set of data. func fractionalZeroPosition() -> CGFloat { return fractionalPosition(for: 0.0) } /// Return the vertical fractional position within the data window that represents /// the zero line value for the current set of data. func fractionalPosition(for value: CGFloat) -> CGFloat { let result: CGFloat if let r = self.range { // If a fixed range is specified, calculate the zero line from the specified range let full = r.upperBound - r.lowerBound // full range width result = abs(value - r.lowerBound) / full } else { // If no fixed range is specified, calculate the zero line position using the current range of the data. result = self.normalize(value: value) } // Clamp to 0.0 -> 1.0 return min(max(result, 0.0), 1.0) } } // In order to clean up some of the code, I've moved the declaration of the data source into a // DSFSparkline 'namespace' to match with the new DSFSparkline.StaticDataSource type // // A simple name change (DSFSparklineDataSource -> DSFSparkline.DataSource) in your code will fix this issue @available(*, deprecated, message: "Move to using DSFSparkline.DataSource instead (simple name change)") public typealias DSFSparklineDataSource = DSFSparkline.DataSource ================================================ FILE: Sources/DSFSparkline/datasource/DSFSparkline+StaticDataSource.swift ================================================ // // DSFSparkline+StaticDataSource.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import CoreGraphics import Foundation public extension DSFSparkline { /// A simple data source containing an array of values. @objc(DSFSparklineStaticDataSource) class StaticDataSource: NSObject { /// The datasource values let values: [CGFloat] /// The minimum value in the data source, excluding .infinite values let minValue: CGFloat /// The maximum value in the data source, excluding .infinite values let maxValue: CGFloat /// The total of all the values within the datasource, excluding .infinite values @objc public let total: CGFloat /// The allowable range of values for this source. If nil, there are no bounds for the source public let valueBounds: ClosedRange? /// Create an empty data source @objc override public convenience init() { self.init([]) } /// Create a data source with the specified values /// - Parameter values: The datasource values @objc public init(_ values: [CGFloat]) { self.values = values self.valueBounds = nil let nonInfiniteValues = values.filter { !$0.isInfinite } self.minValue = nonInfiniteValues.min() ?? 0.0 self.maxValue = nonInfiniteValues.max() ?? 1.0 self.total = nonInfiniteValues.reduce(0) { $0 + $1 } super.init() } /// Create a static data source with values and upper/lower bounds values /// - Parameters: /// - values: The values to be displayed /// - lowerBound: The lower bounds of the data /// - upperBound: The upper bounds of the data @objc public convenience init(_ values: [CGFloat], lowerBound: CGFloat, upperBound: CGFloat) { assert(lowerBound < upperBound) self.init(values, range: lowerBound ... upperBound) } /// Create a static data source with values and upper/lower bounds values /// - Parameters: /// - values: The values to be displayed /// - range: The allowable range for each value public init(_ values: [CGFloat], range: ClosedRange) { self.valueBounds = range let clampedValues = values.map { $0.isInfinite ? .infinity : $0.clamped(to: range) } self.values = clampedValues let nonInfiniteValues = clampedValues.filter { !$0.isInfinite } self.total = nonInfiniteValues.reduce(0) { $0 + $1 } self.minValue = nonInfiniteValues.min() ?? 0.0 self.maxValue = nonInfiniteValues.max() ?? 1.0 super.init() } /// Return the fractional (0 ... 1) value for the specified value, or .infinity if the value is infinite /// - Parameter value: The value to convert to a fractional value within the range of the datasource /// - Returns: A fractional value, or nil when value == .infinity @objc public func fractionalValue(for value: CGFloat) -> CGFloat { if value == .infinity { return .infinity } if let r = valueBounds { let v = value.clamped(to: r) return (v - r.lowerBound) / (r.upperBound - r.lowerBound) } else { return (value - self.minValue) / (self.maxValue - self.minValue) } } /// Return the fractional (0 ... 1) value for the specified value /// - Parameter index: The indexed offset of the datasource to retrieve the fractional value for /// - Returns: /// * If the index is outside the scope of the data source, returns `.nan` /// * If the value at the index is infinite, returns `.infinity` /// * If the value at the index is not infinite, returns the fractional value public func fractionalValue(at index: Int) -> CGFloat { guard index < self.values.count else { return .nan } return self.fractionalValue(for: self.values[index]) } } } public extension DSFSparkline.StaticDataSource { override var description: String { """ StaticDataSource: count: \(values.count), totalValue: \(self.total) values: \(self.values) """ } } ================================================ FILE: Sources/DSFSparkline/datasource/data-core/SparklineData.swift ================================================ // // SparklineData.swift // DSFSparklines // // Created by Darren Ford on 22/12/19. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation class SparklineData where T: BinaryFloatingPoint { var yRange: ClosedRange? = nil { didSet { self.normalized = self.normalize() } } var yMin: T? { return self.yRange?.lowerBound ?? self.yData.min() } var yMax: T? { return self.yRange?.upperBound ?? self.yData.max() } var yData: [T] = [] { didSet { self.normalized = self.normalize() } } public private(set) var normalized: [T] = [] init(range: ClosedRange? = nil) { self.yRange = range } /// Return a normalized (scaled 0.0 -> 1.0) of the supplied value func normalize(value: T) -> T { guard let min = yMin, let max = self.yMax else { return 0.0 } let diff = (max - min) if diff == 0 { return 0.0 } return ((value - min) / diff) } /// Return a normalized (scaled 0.0 -> 1.0) of the current set of data private func normalize() -> [T] { guard let min = yMin, let max = self.yMax else { return [] } // Move the data to 0.0 -> 1.0 scale let diff = (max - min) if diff == 0 { return Array(repeating: 0.0, count: yData.count) } return yData.map { val in ((val - min) / diff) } } } ================================================ FILE: Sources/DSFSparkline/datasource/data-core/SparklineWindow.swift ================================================ // // SparklineWindow.swift // DSFSparklines // // Created by Darren Ford on 20/12/19. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation /// The sparkline 'window' represents the data window to be displayed in the graph /// It is meant to represent the _data_ for the graph, not the way that the graph draws. class SparklineWindow where T: BinaryFloatingPoint { private let NormalizedRange: ClosedRange = 0.0 ... 1.0 private let data: SparklineData /// Optional fixed range for the data being displayed public var yRange: ClosedRange? { get { self.data.yRange } set { self.data.yRange = newValue } } /// The number of items added since last reset var counter: UInt = 0 /// Raw values var raw: [T] { return self.data.yData } /// The normalized data (range 0.0 -> 1.0) depending on the range var normalized: [T] { return self.data.normalized } /// The size of the sparkline data window var windowSize: UInt { get { return UInt(self.data.yData.count) } set { let sz = self.data.yData.count if newValue > sz { let inserter = Array(repeating: self.data.yRange?.lowerBound ?? 0.0, count: Int(newValue) - sz) self.data.yData.insert(contentsOf: inserter, at: 0) } else if newValue < sz { self.data.yData.removeSubrange(0 ..< sz - Int(newValue)) } } } /// The number if of 'empty' buckets in the current window var emptyValueCount: UInt { if self.counter > self.windowSize { return 0 } return self.windowSize - self.counter } /// The 'zero' line for drawing the horizontal line. var zeroLineValue: T = 0.0 /// Create a sparkline data window with an optional data range /// - Parameters: /// - windowSize: The number of data points to be kept in the spark line /// - dataRange: The max/min values init(windowSize: UInt, dataRange: ClosedRange? = nil) { assert(windowSize > 0) self.data = SparklineData(range: dataRange) self.data.yData = Array(repeating: dataRange?.lowerBound ?? 0.0, count: Int(windowSize)) } /// Normalize a value to the current range of the sparkline /// - Parameter value: the value to normalize /// - Return a value 0.0 -> 1.0 public func normalize(value: T) -> T { return self.data.normalize(value: value).clamped(to: NormalizedRange) } /// Push a new value into the sparkline. If the value is outside a specified range, then /// - Parameter value: The value to push into the sparkline /// - Return whether the @discardableResult public func push(value: T) -> Bool { let plotValue: T if let r = self.data.yRange, !r.contains(value) { plotValue = min(r.upperBound, max(r.lowerBound, value)) Swift.print("WARN: Value \(value) outside specified range \(r), truncating to \(plotValue)") } else { plotValue = value } var temp = self.data.yData temp.removeFirst() temp.append(plotValue) self.data.yData = temp self.counter += 1 return true } public func push(values: [T]) { self.counter += UInt(values.count) var temp = self.data.yData // The difference between let diff = temp.count - values.count if diff < 0 { // There are more data points than the window size temp = values temp.removeFirst(abs(diff)) } else if diff == 0 { temp = values } else { temp.removeFirst(min(values.count, temp.count)) temp.append(contentsOf: values) } self.data.yData = temp } public func set(values: [T]) { if let yRange = self.data.yRange { self.data.yData = values.map { $0.clamped(to: yRange) } } else { self.data.yData = values } self.counter = UInt(values.count) } public func reset() { self.counter = 0 self.data.yData = Array(repeating: self.data.yRange?.lowerBound ?? 0.0, count: Int(self.windowSize)) } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/core/DSFSparklineOverlay+Centerable.swift ================================================ // // DSFSparklineOverlay+Centerable.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore public extension DSFSparklineOverlay { /// A graph that can be centered around the datasource's zero-line. /// /// You don't generally create this class yourself, you inherit from it if your overlay type can be /// centered around the zero-line of the data. @objc(DSFSparklineOverlayCenterableGraph) class Centerable: DSFSparklineOverlay.DataSource { /// Should the graph be centered at the zero line defined in the datasource? @objc public var centeredAtZeroLine: Bool = false { didSet { self.setNeedsDisplay() } } // MARK: - Primary /// The primary color for the sparkline @objc public var primaryStrokeColor: CGColor? = .black { didSet { self.setNeedsDisplay() } } /// The primary fill color for the sparkline @objc public var primaryFill: DSFSparklineFillable? { didSet { self.setNeedsDisplay() } } // MARK: - Secondary /// The color used to draw lines below the zero-line (if centeredAtZeroLine=true) @objc public var secondaryStrokeColor: CGColor? { didSet { self.setNeedsDisplay() } } /// The fill color to use for parts of the graph below the zero-line (if centeredAtZeroLine=true) @objc public var secondaryFill: DSFSparklineFillable? { didSet { self.setNeedsDisplay() } } // MARK: - Initializers @objc public init() { super.init() } override public init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.centeredAtZeroLine = orig.centeredAtZeroLine self.primaryStrokeColor = orig.primaryStrokeColor self.primaryFill = orig.primaryFill?.copyFill() self.secondaryStrokeColor = orig.secondaryStrokeColor self.secondaryFill = orig.secondaryFill?.copyFill() super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/core/DSFSparklineOverlay+DataSource.swift ================================================ // // DSFSparklineOverlay+DataSource.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore public extension DSFSparklineOverlay { /// An overlay that contains a DSFSparklineDataSource. /// /// Generally you don't create one of these yourself, you subclass it @objc(DSFSparklineDataSourceOverlay) class DataSource : DSFSparklineOverlay { /// The datasource for displaying the overlay @objc public var dataSource: DSFSparkline.DataSource? { didSet { // Update our observer to detect changes to data in this new DataSource self.updateDataObserver() // And tell any listeners that the datasource was changed self.dataSourceContentDidChange() } } // Listen for changes in the data and update appropriately private var dataObserver: NSObjectProtocol? // Update the observer to point to a new datasource private func updateDataObserver() { self.dataObserver = nil if let datasource = self.dataSource { self.dataObserver = NotificationCenter.default.addObserver( forName: DSFSparkline.DataSource.DataChangedNotification, object: datasource, queue: nil, using: { [weak self] _ in self?.dataSourceContentDidChange() } ) } } /// Overridable to allow overlays to be notified when the content of the data source changes internal func dataSourceContentDidChange() { self.setNeedsDisplay() } // MARK: Initializers @objc public init(dataSource: DSFSparkline.DataSource? = nil) { self.dataSource = dataSource super.init() } override public init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.dataObserver = orig.dataObserver self.dataSource = orig.dataSource super.init(layer: layer) } required init?(coder: NSCoder) { super.init(coder: coder) } deinit { self.dataObserver = nil self.dataSource = nil } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/core/DSFSparklineOverlay+StaticDataSource.swift ================================================ // // DSFSparklineOverlay+StaticDataSource.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore public extension DSFSparklineOverlay { /// A data source that displays a static set of data, like for a pie chart @objc(DSFSparklineOverlayStaticDataSource) class StaticDataSource: DSFSparklineOverlay { /// The data to be displayed in this graph /// /// The values become a percentage of the total value stored within the /// dataStore, and as such each value ends up being drawn as a fraction of the total. /// So for example, if you want the pie chart to represent the number of red cars vs. number of /// blue cars, you just set the values directly. @objc public var dataSource = DSFSparkline.StaticDataSource() { didSet { self.staticDataSourceDidChange() } } /// Datasource values @objc public var values: [CGFloat] { get { self.dataSource.values } set { self.dataSource = .init(newValue) } } @objc public override init() { super.init() } override public init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.dataSource = orig.dataSource super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// Overridable to allow overlays to be notified when the data source is changed func staticDataSourceDidChange() { // Do nothing } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/core/DSFSparklineOverlay.swift ================================================ // // DSFSparklineOverlay.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore /// The core sparkline overlay class. /// /// All sparkline renderers must inherit from this class @objc public class DSFSparklineOverlay: CALayer { override public init() { super.init() self.configure() } override public init(layer: Any) { super.init(layer: layer) self.configure() } required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } private func configure() { self.anchorPoint = CGPoint(x: 0, y: 0) self.isOpaque = false // Disable the implicit animations on the layer to stop the fade when data changes let newActions = [ "onOrderIn": NSNull(), "onOrderOut": NSNull(), "sublayers": NSNull(), "contents": NSNull(), "bounds": NSNull(), ] self.actions = newActions } /// Return the minimum edge insets required to fit the graph within the specified rect /// /// An example of this is a line graph with markers - if a data point is at zero, the marker will be clipped when drawn. /// Overriding this var allows a graph to inset the drawing when required to handle clipping edge cases, while making /// sure that grid lines, highlights etc. arrange themselves correctly draw within the range of the reduced graph size. internal func edgeInsets(for rect: CGRect) -> DSFEdgeInsets { return .zero } /// To be overridden by sub-classes to draw their content into the provided context /// - Parameters: /// - context: The context to draw into /// - bounds: The bounds in which to draw (which may NOT be the same as the bounds for the context!) /// - scale: The scale for the current drawing context (eg. retina == 2) internal func drawGraph(context _: CGContext, bounds _: CGRect, scale _: CGFloat) { fatalError("must be implemented in overridden classes") } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-components/DSFSparklineOverlay+GridLines.swift ================================================ // // DSFSparklineOverlay+GridLines.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparklineOverlay { /// An overlay that draws grid lines at specified vertical points on the sparkline @objc(DSFSparklineOverlayGridLines) class GridLines: DSFSparklineOverlay.DataSource { /// The color of the dotted line at the zero point on the y-axis @objc public var strokeColor: CGColor { didSet { self.setNeedsDisplay() } } /// The width of the dotted line at the zero point on the y-axis @objc public var strokeWidth: CGFloat { didSet { self.setNeedsDisplay() } } /// The line style for the dotted line. Use [] to specify a solid line. @objc public var dashStyle: [CGFloat] = [1, 1] { didSet { self.setNeedsDisplay() } } /// The y-values within the range of the datasource for the lines @objc public var floatValues: [CGFloat] = [] { didSet { self.setNeedsDisplay() } } @objc public init(dataSource: DSFSparkline.DataSource? = nil, floatValues: [CGFloat] = [], strokeColor: CGColor = DSFColor.gray.cgColor, strokeWidth: CGFloat = 1.0, dashStyle: [CGFloat] = [1.0, 1.0]) { self.floatValues = floatValues self.strokeColor = strokeColor self.strokeWidth = strokeWidth self.dashStyle = dashStyle super.init(dataSource: dataSource) } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.floatValues = orig.floatValues self.strokeColor = orig.strokeColor.copy() ?? .black self.strokeWidth = orig.strokeWidth self.dashStyle = orig.dashStyle super.init(layer: layer) } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } internal override func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { self.drawGridLines(context: context, bounds: bounds, scale: scale) } } } extension DSFSparklineOverlay.GridLines { func drawGridLines(context: CGContext, bounds: CGRect, scale _: CGFloat) { guard self.floatValues.count > 0, let dataSource = self.dataSource else { return } context.setLineWidth(self.strokeWidth) context.setStrokeColor(self.strokeColor) context.setLineDash(phase: 0.0, lengths: self.dashStyle) self.floatValues.forEach { value in let fractional = dataSource.fractionalPosition(for: value) let zeroPos = bounds.minY + bounds.height - (fractional * bounds.height).rounded(.towardZero) context.strokeLineSegments(between: [CGPoint(x: bounds.minX, y: zeroPos), CGPoint(x: bounds.maxX, y: zeroPos)]) } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-components/DSFSparklineOverlay+RangeHighlight.swift ================================================ // // DSFSparklineOverlay+RangeHighlight.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore public extension DSFSparklineOverlay { /// An overlay that draws a color range on the sparkline @objc(DSFSparklineOverlayRangeHighlight) class RangeHighlight: DSFSparklineOverlay.DataSource { static public let defaultFill = DSFSparkline.Fill.Color(CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.5, 0.5, 0.5, 0.5])!) /// The color to fill the specified range @objc public var fill: DSFSparklineFillable = RangeHighlight.defaultFill { didSet { self.setNeedsDisplay() } } /// The highlight range for the graph public var highlightRange: Range? { didSet { self.setNeedsDisplay() } } /// objective-c compatible highlight range setting @objc public func setHighlightRange(lowerBound: CGFloat, upperBound: CGFloat) { self.highlightRange = lowerBound ..< upperBound } public init(dataSource: DSFSparkline.DataSource? = nil, range: Range? = nil, fill: DSFSparklineFillable = RangeHighlight.defaultFill) { self.highlightRange = range self.fill = fill super.init(dataSource: dataSource) } @objc public init(lowerBound: CGFloat, upperBound: CGFloat, fill: DSFSparklineFillable = RangeHighlight.defaultFill) { self.highlightRange = lowerBound ..< upperBound self.fill = fill super.init() } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.highlightRange = orig.highlightRange self.fill = orig.fill.copyFill() super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } internal override func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { guard let dataSource = self.dataSource, let range = self.highlightRange else { return } let lb = 1.0 - dataSource.normalize(value: range.lowerBound) let l = (lb * bounds.height) + bounds.minY let ub = 1.0 - dataSource.normalize(value: range.upperBound) let u = (ub * bounds.height) + bounds.minY let destRect = CGRect(x: bounds.minX, y: u, width: bounds.width, height: l - u) context.clip(to: destRect) self.fill.fill(context: context, bounds: bounds) } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-components/DSFSparklineOverlay+ZeroLine.swift ================================================ // // DSFSparklineOverlay+ZeroLine.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparklineOverlay { @objc(DSFSparklineOverlayZeroLine) class ZeroLine: DSFSparklineOverlay.DataSource { /// The color of the dotted line at the zero point on the y-axis @objc public var strokeColor: CGColor { didSet { self.setNeedsDisplay() } } /// The width of the dotted line at the zero point on the y-axis @objc public var strokeWidth: CGFloat { didSet { self.setNeedsDisplay() } } /// The line style for the dotted line. Use [] to specify a solid line. @objc public var dashStyle: [CGFloat] = [1, 1] { didSet { self.setNeedsDisplay() } } @objc public init(dataSource: DSFSparkline.DataSource? = nil, strokeColor: CGColor = DSFColor.gray.cgColor, strokeWidth: CGFloat = 1.0, dashStyle: [CGFloat] = [1.0, 1.0]) { self.strokeColor = strokeColor self.strokeWidth = strokeWidth self.dashStyle = dashStyle super.init(dataSource: dataSource) } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.strokeColor = orig.strokeColor.copy() ?? .black self.strokeWidth = orig.strokeWidth self.dashStyle = orig.dashStyle super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } internal override func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { guard let dataSource = self.dataSource else { return } let frac = dataSource.fractionalPosition(for: dataSource.zeroLineValue) let zeroPos = bounds.height - (frac * bounds.height) + bounds.minY context.setLineWidth(self.strokeWidth) context.setStrokeColor(self.strokeColor) context.setLineDash(phase: 0.0, lengths: self.dashStyle) context.strokeLineSegments( between: [CGPoint(x: bounds.minX, y: zeroPos), CGPoint(x: bounds.width + bounds.minX, y: zeroPos)]) } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+ActivityGrid.swift ================================================ // // DSFSparklineOverlay+ActivityGrid.swift // DSFSparklines // // Copyright © 2023 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparklineOverlay { /// A GitHub-style activity grid. @objc(DSFSparklineOverlayActivityGrid) class ActivityGrid: DSFSparklineOverlay.StaticDataSource { /// The number of vertical cells in a column @objc @LayerInvalidating(.display) public var verticalCellCount: Int = 7 /// The number of horizontal cells in the grid. /// /// If `horizontalCellCount` == 0, cells will be added to fill the entire width of the view @objc @LayerInvalidating(.display) public var horizontalCellCount: Int = 0 /// The layout style for the grid @objc @LayerInvalidating(.display) public var layoutStyle: DSFSparkline.ActivityGrid.LayoutStyle = .github // MARK: Cell style /// The cell's drawing style @objc @LayerInvalidating(.display) public var cellStyle: DSFSparkline.ActivityGrid.CellStyle = .init() // MARK: Individual cell style setters/getters /// The color scheme to use when fill cells @objc public var cellFillScheme: DSFSparkline.ValueBasedFill { get { self.cellStyle.fillScheme } set { self.cellStyle = self.cellStyle.modify(fillScheme: newValue) } } /// The dimension of each cell @objc public var cellDimension: CGFloat { get { self.cellStyle.cellDimension } set { self.cellStyle = self.cellStyle.modify(cellDimension: newValue) } } /// The spacing between each of the cells @objc public var cellSpacing: CGFloat { get { self.cellStyle.cellSpacing } set { self.cellStyle = self.cellStyle.modify(cellSpacing: newValue) } } /// The color for the border of the cell @objc public var cellBorderColor: CGColor? { get { self.cellStyle.borderColor } set { self.cellStyle = self.cellStyle.modify(borderColor: newValue) } } /// The cell's border width @objc public var cellBorderWidth: CGFloat { get { self.cellStyle.borderWidth } set { self.cellStyle = self.cellStyle.modify(borderWidth: newValue) } } /// The cell's corner radius @objc public var cellCornerRadius: CGFloat { get { self.cellStyle.cornerRadius } set { self.cellStyle = self.cellStyle.modify(cornerRadius: newValue) } } /// Called when the activity cells are updated @objc public var cellsDidUpdateBlock: (() -> Void)? // MARK: Drawing and updates /// Called when the content of the data source changes override func staticDataSourceDidChange() { super.staticDataSourceDidChange() self.setNeedsDisplay() } /// Draws the graph override internal func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { self._cells.removeAll() switch self.layoutStyle { case .github: self.drawGithubStyle(context: context, bounds: bounds, scale: scale) case .defrag: self.drawDefragStyle(context: context, bounds: bounds, scale: scale) } self.cellsDidUpdateBlock?() } public var cells: [CGRect] { _cells } // MARK: Private private var _cells: [CGRect] = [] } } public extension DSFSparklineOverlay.ActivityGrid { /// Returns the index within the datasource of the value at the given point /// - Parameter point: The point within the activity grid to test /// - Returns: The data source index for the point, or -1 if /// 1. no cell was hit, or /// 2. The cell was a skip cell (ie. its value is `.infinity`, or /// 3. the cell hit was outside of bounds of the data source @objc func indexAtPoint(_ point: CGPoint) -> Int { if let index: Int = self._cells.firstIndex(where: { $0.contains(point) }) { if index < self.dataSource.values.count, self.dataSource.values[index] == .infinity { // It's an existing cell, but it's a skip cell return -1 } return index } return -1 } /// Return the cell frame for the given index /// - Parameter index: The index /// - Returns: The cell bounds for the given index @objc func cellFrame(for index: Int) -> CGRect { if index < self._cells.count { return self._cells[index] } // If we get here, the indexed cell was not visible on screen return .zero } } // MARK: - Sizing extension DSFSparklineOverlay.ActivityGrid { /// The expected height given the current settings @objc public var intrinsicHeight: CGFloat { if verticalCellCount <= 0 { return DSFView.noIntrinsicMetric } return (CGFloat(self.verticalCellCount) * (self.cellStyle.cellDimension + self.cellStyle.cellSpacing)) + self.cellStyle.cellSpacing } /// Minimum width for displaying the current values without padding @objc public var intrinsicWidth: CGFloat { if horizontalCellCount <= 0 { if verticalCellCount <= 0 { return DSFView.noIntrinsicMetric } var columnCount = self.dataSource.values.count / self.verticalCellCount if (self.dataSource.values.count % self.verticalCellCount) > 0 { columnCount += 1 } return (CGFloat(columnCount) * (self.cellStyle.cellDimension + self.cellStyle.cellSpacing)) + self.cellStyle.cellSpacing } else { // Fixed horizontal count return (CGFloat(horizontalCellCount) * (self.cellStyle.cellDimension + self.cellStyle.cellSpacing)) + self.cellStyle.cellSpacing } } /// Intrinsic size for the grid @objc public var intrinsicSize: CGSize { CGSize(width: self.intrinsicWidth, height: self.intrinsicHeight) } } extension DSFSparklineOverlay.ActivityGrid { private func drawGithubStyle(context: CGContext, bounds: CGRect, scale: CGFloat) { let style = self.cellStyle var xOffset = bounds.width - style.cellSpacing - style.cellDimension var dataOffset = 0 while xOffset > 0 { (0 ..< verticalCellCount).reversed().forEach { index in let cell = CGRect( x: xOffset, y: CGFloat(index) * (style.cellDimension + style.cellSpacing) + style.cellSpacing, width: style.cellDimension, height: style.cellDimension ) self.drawCell(context: context, inRect: cell, index: dataOffset) dataOffset += 1 } xOffset -= (self.cellStyle.cellDimension + self.cellStyle.cellSpacing) } } private func drawDefragStyle(context: CGContext, bounds: CGRect, scale: CGFloat) { var dataOffset = 0 var yOffset = self.cellStyle.cellSpacing let style = self.cellStyle let sp = Int((bounds.width - style.cellSpacing) / (style.cellSpacing + style.cellDimension)) let horizontalCellCount = self.horizontalCellCount == 0 ? sp : self.horizontalCellCount var xOffset = 0 while yOffset <= (bounds.height - style.cellSpacing - style.cellDimension) { while xOffset < horizontalCellCount { let cell = CGRect( x: style.cellSpacing + (CGFloat(xOffset) * (style.cellSpacing + style.cellDimension)), y: yOffset, width: style.cellDimension, height: style.cellDimension ) self.drawCell(context: context, inRect: cell, index: dataOffset) xOffset += 1 dataOffset += 1 } xOffset = 0 yOffset += self.cellStyle.cellDimension + self.cellStyle.cellSpacing } } } private extension DSFSparklineOverlay.ActivityGrid { func drawCell(context: CGContext, inRect cell: CGRect, index: Int) { let style = self.cellStyle // Store the cell information so we can access it later self._cells.append(cell) // The fractional value for the index let fracValue = self.dataSource.fractionalValue(at: index) if fracValue.isInfinite { // Treat an infinite value as a skip cell. Draw nothing return } let fraction = fracValue.isNaN ? 0.0 : fracValue let color = style.fillScheme.color(atFraction: fraction) context.addPath( CGPath( roundedRect: cell, cornerWidth: style.cornerRadius, cornerHeight: style.cornerRadius, transform: nil ) ) context.setFillColor(color) context.fillPath() if let c = self.cellStyle.borderColor { context.addPath( CGPath( roundedRect: cell, cornerWidth: style.cornerRadius, cornerHeight: style.cornerRadius, transform: nil ) ) context.setFillColor(.clear) context.setStrokeColor(c) context.setLineWidth(style.borderWidth) context.strokePath() } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+Bar.swift ================================================ // // DSFSparklineOverlay+Bar.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparklineOverlay { /// A bar graph overlay @objc(DSFSparklineOverlayBar) class Bar: Centerable { /// The width for the line drawn on the graph (in pixels) @objc public var strokeWidth: UInt = 1 { didSet { self.setNeedsDisplay() } } /// The spacing between each bar (in pixels) @objc public var barSpacing: UInt = 1 { didSet { self.setNeedsDisplay() } } /// Draw a shadow under the line @objc public var shadow: NSShadow? { didSet { self.setNeedsDisplay() } } public override init() { super.init() } override public init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.strokeWidth = orig.strokeWidth self.barSpacing = orig.barSpacing self.shadow = orig.shadow?.copy() as? NSShadow super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } internal override func edgeInsets(for rect: CGRect) -> DSFEdgeInsets { guard let dataSource = self.dataSource else { return .zero } let integralRect = bounds.integral let componentWidth = Int(integralRect.width) / Int(dataSource.windowSize) // The left offset in order to center X the full range let intOffset = Int(bounds.minX) + (Int(bounds.width) - (componentWidth * Int(dataSource.windowSize))) / 2 let xOffset = CGFloat(intOffset) let yOffset = CGFloat(self.strokeWidth) return DSFEdgeInsets(top: yOffset, left: xOffset, bottom: yOffset, right: xOffset) } internal override func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { if self.centeredAtZeroLine { self.drawCenteredBarGraph(context: context, bounds: bounds, scale: scale) } else { self.drawBarGraph(context: context, bounds: bounds, scale: scale) } } } } extension DSFSparklineOverlay.Bar { private func drawBarGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { guard let dataSource = self.dataSource else { return } let integralRect = bounds.integral // This represents the _full_ width of a bar within the graph, including the spacing. let componentWidth = Int(integralRect.width) / Int(dataSource.windowSize) // The width of the BAR component let barWidth = componentWidth - Int(barSpacing) // The left offset in order to center X let xOffset: Int = Int(bounds.minX) + (Int(bounds.width) - (componentWidth * Int(dataSource.windowSize))) / 2 // The available height range let range: ClosedRange = 0 ... integralRect.maxY let normy = dataSource.normalized let xDiff = bounds.width / CGFloat(normy.count) let points = normy.enumerated().map { CGPoint(x: CGFloat($0.offset) * xDiff, y: ($0.element * (integralRect.height - 1) + integralRect.minY).clamped(to: range)) } context.usingGState { outer in outer.setRenderingIntent(.relativeColorimetric) outer.interpolationQuality = .none outer.setShouldAntialias(false) if dataSource.counter < dataSource.windowSize { let pos = xOffset + (Int(dataSource.counter) * componentWidth) let clipRect = bounds.divided(atDistance: CGFloat(pos), from: .maxXEdge).slice outer.clip(to: clipRect) } var bars: [CGRect] = [] for point in points.enumerated() { let yVal = Int(point.element.y.rounded(.down)) let r = CGRect(x: xOffset + point.offset * componentWidth, y: Int(integralRect.minY) + Int(integralRect.height) - yVal, width: barWidth, height: yVal - Int(self.strokeWidth)) bars.append(r.integral) } let path = CGMutablePath() path.addRects(bars) if let primaryFill = self.primaryFill { outer.usingGState { fillCtx in fillCtx.addPath(path) fillCtx.clip() primaryFill.fill(context: fillCtx, bounds: bounds) } } if let stroke = self.primaryStrokeColor { outer.usingGState { strokeCtx in if let shadow = self.shadow { strokeCtx.setShadow(shadow) } strokeCtx.addPath(path) strokeCtx.setLineWidth(1.0 / scale * CGFloat(self.strokeWidth)) strokeCtx.setStrokeColor(stroke) strokeCtx.drawPath(using: .stroke) } } } } private func drawCenteredBarGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { guard let dataSource = self.dataSource else { return } let drawRect = bounds let height = drawRect.height - 1 let normy = dataSource.normalized let xDiff = bounds.width / CGFloat(normy.count) let centre = dataSource.normalizedZeroLineValue let centroid = (1 - centre) * (drawRect.height - 1) context.usingGState { outer in outer.setRenderingIntent(.relativeColorimetric) outer.interpolationQuality = .none if dataSource.counter < dataSource.windowSize { let pos = CGFloat(dataSource.counter) * xDiff + 1 let clipRect = bounds.divided(atDistance: pos, from: .maxXEdge).slice outer.clip(to: clipRect) } let split = self.bounds.divided(atDistance: centroid, from: .minYEdge) let positiveRegion: CGRect = split.slice let negativeRegion: CGRect = split.remainder var positivePath: [CGRect] = [] var negativePath: [CGRect] = [] for value in normy.enumerated() { let x = bounds.minX + CGFloat(value.offset) * xDiff if value.element >= centre { let yy = bounds.minY + ((centre - value.element) * height) let r = CGRect( x: x, y: centroid, width: xDiff - 1 - (CGFloat(self.barSpacing)), height: yy ) positivePath.append(r.integral) } else { let yy = bounds.minY + ((value.element - centre) * height) let r = CGRect( x: x, y: centroid + 1, width: xDiff - 1 - (CGFloat(self.barSpacing)), height: -yy - CGFloat(self.strokeWidth) + 1 ) negativePath.append(r.integral) } } outer.setShouldAntialias(false) if !positivePath.isEmpty { let path = CGMutablePath() path.addRects(positivePath) if let primaryFill = self.primaryFill { outer.usingGState { fillCtx in fillCtx.addPath(path) fillCtx.clip() primaryFill.fill(context: fillCtx, bounds: positiveRegion) } } if let stroke = self.primaryStrokeColor { outer.usingGState { strokeCtx in strokeCtx.addPath(path) strokeCtx.setLineWidth(1.0 / scale * CGFloat(self.strokeWidth)) strokeCtx.setStrokeColor(stroke) strokeCtx.strokePath() } } } if !negativePath.isEmpty { let path = CGMutablePath() path.addRects(negativePath) if let secondaryFill = self.secondaryFill { outer.usingGState { fillCtx in fillCtx.addPath(path) fillCtx.clip() secondaryFill.fill(context: fillCtx, bounds: negativeRegion) } } if let stroke = self.secondaryStrokeColor { outer.usingGState { strokeCtx in strokeCtx.addPath(path) strokeCtx.setLineWidth(1.0 / scale * CGFloat(self.strokeWidth)) strokeCtx.setStrokeColor(stroke) strokeCtx.strokePath() } } } } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+CircularGauge.swift ================================================ // // DSFSparklineOverlay+CircularGauge.swift // // Copyright © 2024 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import QuartzCore #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparklineOverlay { /// A circular gauge @objc(DSFSparklineOverlayCircularGauge) class CircularGauge: DSFSparklineOverlay { /// The default track style @objc public static let DefaultTrackStyle = DSFSparklineOverlay.CircularGauge.TrackStyle( width: 10, fillColor: DSFSparkline.Fill.Color(red: 0, green: 0, blue: 0, alpha: 0.2) ) /// The default value style @objc public static let DefaultLineStyle = DSFSparklineOverlay.CircularGauge.TrackStyle( width: 7, fillColor: DSFSparkline.Fill.Color(red: 0, green: 0, blue: 0, alpha: 1) ) /// The value assigned to the percent bar. A value between 0.0 and 1.0 @objc public var value: CGFloat = 0 { willSet { animationTransition = DSFSparkline.AnimationTransition(start: _value, stop: newValue) } didSet { if isAnimated { startAnimateIn() } else { self._value = self.value.clamped(to: 0 ... 1) } } } private var _value: CGFloat = 0 { didSet { self.setNeedsDisplay() } } /// The style to use when drawing the gauge's track @objc public var trackStyle: TrackStyle = TrackStyle( width: 5, fillColor: DSFSparkline.Fill.Color(srgbRed: 0, green: 0, blue: 0, alpha: 0.2) ) /// The style to use when drawing the gauge's value @objc public var lineStyle: TrackStyle = TrackStyle( width: 3, fillColor: DSFSparkline.Fill.Color(srgbRed: 0, green: 0, blue: 0) ) /// The animation style to apply @objc public var animationStyle: DSFSparkline.AnimationStyle? = nil private var isAnimated: Bool { animationStyle != nil } override func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { CATransaction.withDisabledActions { [weak self] in self?.drawCircularGauge(context: context, bounds: bounds, scale: scale) } } // MARK: Animation private var animator = ArbitraryAnimator() // private var animationTransition: DSFSparkline.AnimationTransition? } } extension DSFSparklineOverlay.CircularGauge { func startAnimateIn() { guard let animStyle = animationStyle else { fatalError() } // Stop any animation that is currently active self.animator.progressBlock = nil self.animator.stop() self.animator.animationFunction = animStyle.function.function self.animator.duration = animStyle.duration self.animator.progressBlock = { [weak self] progress in guard let `self` = self, let a = self.animationTransition else { return } _value = a.start + (a.stop - a.start) * progress } self.animator.start() } } public extension DSFSparklineOverlay.CircularGauge { /// A circular gauge track style @objc(DSFSparklineOverlayCircularGaugeTrackStyle) class TrackStyle: NSObject { /// The width of the track @objc public var width: CGFloat /// The fill style to use @objc public var fillColor: DSFSparklineFillable /// The stroke width @objc public var strokeWidth: CGFloat /// The stroke color @objc public var strokeColor: CGColor? /// The shadow to use @objc public var shadow: DSFSparkline.Shadow? /// The line's cap style @objc public var lineCap: CGLineCap /// Create @objc public init( width: CGFloat, fillColor: DSFSparklineFillable, strokeWidth: CGFloat = 0.0, strokeColor: CGColor? = nil, shadow: DSFSparkline.Shadow? = nil, lineCap: CGLineCap = .round ) { self.width = width self.fillColor = fillColor self.strokeWidth = strokeWidth self.strokeColor = strokeColor self.shadow = shadow self.lineCap = lineCap super.init() } } } private extension DSFSparklineOverlay.CircularGauge { func drawCircularGauge(context: CGContext, bounds: CGRect, scale: CGFloat) { var drawRect = bounds if drawRect.width != drawRect.height { let m = min(drawRect.width, drawRect.height) drawRect = CGRect( x: drawRect.minX + ((drawRect.width - m) / 2), y: drawRect.minY + ((drawRect.height - m) / 2), width: m, height: m ) } let baseRect = drawRect // Inset so that the line drawing doesn't crop at the edges let inset: CGFloat = { var inset = max(trackStyle.width + trackStyle.strokeWidth, lineStyle.width + lineStyle.strokeWidth) let trackW = trackStyle.shadow?.requiredShadowInset ?? 0 let valueW = lineStyle.shadow?.requiredShadowInset ?? 0 inset += max(trackW, valueW) return inset }() drawRect = drawRect.insetBy(dx: inset / 2, dy: inset / 2) let centerPoint = CGPoint(x: drawRect.midX, y: drawRect.midY) let radius: CGFloat = drawRect.width > drawRect.height ? (drawRect.height / 2.0) : (drawRect.width / 2.0) // The track self.drawComponent( context: context, bounds: baseRect, centerPoint: centerPoint, radius: radius, value: 1, style: trackStyle ) // The value self.drawComponent( context: context, bounds: baseRect, centerPoint: centerPoint, radius: radius, value: _value, style: lineStyle ) } } private extension DSFSparklineOverlay.CircularGauge { func drawComponent( context: CGContext, bounds: CGRect, centerPoint: CGPoint, radius: CGFloat, value: CGFloat, style: TrackStyle ) { let pth = CGMutablePath() pth.addArc( center: centerPoint, radius: radius, startAngle: Angle.degrees(135).radians, endAngle: Angle.degrees(135 + (270 * value)).radians, clockwise: false ) let act = pth.copy( strokingWithWidth: style.width, lineCap: style.lineCap, lineJoin: .round, miterLimit: 1.0 ) if let shadow = style.shadow { if shadow.isInner { context.usingGState { ctx in // Draw the track ctx.addPath(act) ctx.clip() style.fillColor.fill(context: ctx, bounds: bounds) // Overlay the inner shadow ctx.drawInnerShadow( in: act, shadowColor: shadow.color, offset: shadow.offset, blurRadius: shadow.blurRadius ) } } else { context.usingGState { ctx in ctx.addRect(bounds) ctx.addPath(act) ctx.clip(using: .evenOdd) ctx.setShadow(shadow.shadow) ctx.addPath(act) ctx.setFillColor(shadow.color!) ctx.fillPath() } context.usingGState { ctx in ctx.addPath(act) ctx.clip() style.fillColor.fill(context: ctx, bounds: bounds) } } } else { context.usingGState { ctx in ctx.addPath(act) ctx.clip() style.fillColor.fill(context: ctx, bounds: bounds) } } if let s = style.strokeColor { context.usingGState { ctx in ctx.addPath(act) ctx.setStrokeColor(s) ctx.setLineWidth(style.strokeWidth) ctx.setLineCap(.round) ctx.strokePath() } } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+CircularProgress.swift ================================================ // // DSFSparklineOverlay+CircularProgress.swift // // Copyright © 2024 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import QuartzCore #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparklineOverlay { /// A circular progress sparkline @objc(DSFSparklineOverlayCircularProgress) class CircularProgress: DSFSparklineOverlay { /// The value assigned to the percent bar. A value between 0.0 and 1.0 @objc public var value: CGFloat = 0.25 { didSet { self.valueDidChange() } } /// Default track width @objc public static let DefaultTrackWidth: CGFloat = 10.0 /// The width of the circular ring track @objc public var trackWidth: CGFloat = CircularProgress.DefaultTrackWidth { didSet { self.valueDidChange() } } /// The padding (inset) for drawing the ring @objc public var padding: CGFloat = 0.0 { didSet { self.valueDidChange() } } /// Default fill style @objc public static let DefaultFillStyle = DSFSparkline.Fill.Color(srgbRed: 0, green: 0, blue: 1) /// The fill style for the progress track @objc public var fillStyle: DSFSparklineFillable = CircularProgress.DefaultFillStyle { didSet { self.valueDidChange() } } /// Default track color @objc public static let DefaultTrackColor = CGColor(gray: 0.5, alpha: 0.1) /// The color of the track background @objc public var trackColor: CGColor = CircularProgress.DefaultTrackColor { didSet { self.valueDidChange() } } /// The icon appearing in the track at the top of the ring @objc public var icon: CGImage? = nil { didSet { self._flippedIcon = icon?.flipped() self.valueDidChange() } } private var _flippedIcon: CGImage? = nil override init() { super.init() self.drawsAsynchronously = true } init(value: CGFloat, trackWidth: CGFloat, fillStyle: DSFSparklineFillable, trackColor: CGColor = CircularProgress.DefaultTrackColor) { self.value = value self.trackWidth = trackWidth self.fillStyle = fillStyle self.trackColor = trackColor super.init() self.drawsAsynchronously = true } public override init(layer: Any) { if let layer = layer as? CircularProgress { self.value = layer.value self.trackWidth = layer.trackWidth self.fillStyle = layer.fillStyle self.trackColor = layer.trackColor.copy()! self.padding = layer.padding self.icon = layer.icon?.copy() } else { fatalError() } super.init(layer: layer) self.drawsAsynchronously = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override internal func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { self.drawCircularProgressGraph(context: context, bounds: bounds, scale: scale) } private let fullCircle = 2.0 * CGFloat.pi } } private extension DSFSparklineOverlay.CircularProgress { func valueDidChange() { self.setNeedsDisplay() } func drawCircularProgressGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { ActivityRing.drawRing( in: context, destination: bounds, value: self.value, padding: self.padding, trackWidth: self.trackWidth, fillStyle: self.fillStyle, trackColor: self.trackColor, icon: self._flippedIcon ) } } //// private class ActivityRing { static func drawRing( in ctx: CGContext, destination: CGRect = .zero, value: CGFloat, padding: CGFloat, trackWidth: CGFloat, fillStyle: DSFSparklineFillable, trackColor: CGColor, icon: CGImage? ) { assert(value >= 0.0) guard destination.isEmpty == false else { return } let _fullCircle = 2.0 * CGFloat.pi var drawRect = destination if drawRect.width != drawRect.height { let m = min(drawRect.width, drawRect.height) drawRect = CGRect( x: drawRect.minX + ((drawRect.width - m) / 2), y: drawRect.minY + ((drawRect.height - m) / 2), width: m, height: m ) } // Inset by the padding amount drawRect = drawRect.insetBy(dx: padding, dy: padding) guard drawRect.isEmpty == false else { return } let centerPoint = CGPoint(x: drawRect.midX, y: drawRect.midY) let dimension = drawRect.width > drawRect.height ? drawRect.height : drawRect.width let radius: CGFloat = drawRect.width > drawRect.height ? (drawRect.height - trackWidth) / 2.0 : (drawRect.width - trackWidth) / 2.0 // // Clip to the activity track // let pth = CGMutablePath() pth.addArc(center: centerPoint, radius: radius, startAngle: 0, endAngle: _fullCircle, clockwise: false) let act = pth.copy(strokingWithWidth: trackWidth, lineCap: .round, lineJoin: .round, miterLimit: 1.0) ctx.addPath(act) ctx.clip() // // Fill the background track // ctx.usingGState { ctx in ctx.setFillColor(trackColor) ctx.fill([drawRect]) } // // Draw the ring // var offset: CGFloat = 0 do { let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) let fill = CGContext( data: nil, width: Int(dimension), height: Int(dimension), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue )! // The full angle sweep (which may be > 1) let totalAngle = _fullCircle * value while offset <= totalAngle { let fractionalValue = totalAngle == 0 ? 0 : (offset / totalAngle) let c = fillStyle.color(at: fractionalValue) let x = radius + (radius * cos(offset - (CGFloat.pi * 0.5))) let y = radius + (radius * sin(offset - (CGFloat.pi * 0.5))) offset += 0.01 let p = CGPath( ellipseIn: CGRect(x: x - 0.1, y: y - 0.1, width: trackWidth + 0.2, height: trackWidth + 0.2), transform: nil) fill.addPath(p) fill.setFillColor(c) fill.fillPath() } /// An image containing the drawn ring let ringImage = fill.makeImage()! ctx.draw(ringImage, in: drawRect, byTiling: false) } // // Draw a circle at the end // if value >= 0.92 { ctx.usingGState { ctx in let x = drawRect.minX + (radius + (radius * cos(offset - (CGFloat.pi * 0.5)))) let y = drawRect.minY + (radius + (radius * sin(offset - (CGFloat.pi * 0.5)))) let cp = CGPath( ellipseIn: CGRect(x: x - 0.1, y: y - 0.1, width: trackWidth + 0.2, height: trackWidth + 0.2), transform: nil ) ctx.addPath(cp) ctx.setFillColor(fillStyle.color(at: 1.0)) let shadowAngle = offset + 0.1 let sx = drawRect.minX + (radius + (radius * cos(shadowAngle - (CGFloat.pi * 0.5)))) let sy = drawRect.minY + (radius + (radius * sin(shadowAngle - (CGFloat.pi * 0.5)))) ctx.setShadow( offset: CGSize(width: sx - x, height: sy - y), blur: dimension / 25, color: .black.copy(alpha: 0.4) ) ctx.fillPath() } } // // Draw the icon // if let icon = icon { let x = drawRect.minX + (radius + (radius * cos(-(CGFloat.pi * 0.5)))) let y = drawRect.minY + (radius + (radius * sin(-(CGFloat.pi * 0.5)))) let iconRect = CGRect(x: x, y: y, width: trackWidth, height: trackWidth).insetBy(dx: 2, dy: 2) ctx.draw(icon, in: iconRect, byTiling: false) } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+DataBar.swift ================================================ // // DSFSparklineOverlay+DataBar.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore public extension DSFSparklineOverlay { @objc(DSFSparklineOverlayDataBar) class DataBar: DSFSparklineOverlay.StaticDataSource { /// The maximum _total_ value. If the datasource values total is greater than this value, it clips the display @objc public var maximumTotalValue: CGFloat = -1 { didSet { self.setNeedsDisplay() } } /// The 'undrawn' color for the graph. /// /// If a maximum total value is defined, and the datasource doesn't completely fill the total, then /// the empty section of the databar is drawn using this color. @objc public var unsetColor: CGColor? { didSet { self.setNeedsDisplay() } } /// The stroke color for the line(s) to be drawn between each segment of the databar. @objc public var strokeColor: CGColor? { didSet { self.setNeedsDisplay() } } /// The width of the stroke line to be drawn between each segment of the databar. if 0, no stroke is drawn @objc public var lineWidth: CGFloat = 0.5 { didSet { self.setNeedsDisplay() } } /// The palette to use when drawing the databar @objc public var palette = DSFSparkline.Palette.shared { didSet { self.setNeedsDisplay() } } // MARK: Animation support /// The animation style @objc public var animationStyle: DSFSparkline.AnimationStyle? = nil private var animated: Bool { animationStyle != nil } // MARK: Initializers @objc public override init() { super.init() } override public init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.maximumTotalValue = orig.maximumTotalValue self.unsetColor = orig.unsetColor self.strokeColor = orig.strokeColor self.lineWidth = orig.lineWidth self.palette = orig.palette.copyPalette() super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: Data change override func staticDataSourceDidChange() { super.staticDataSourceDidChange() if self.animated { self.startAnimateIn() } else { self.fractionComplete = 1.0 self.setNeedsDisplay() } } internal override func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { self.drawDataBarGraph(context: context, bounds: bounds, scale: scale) } // MARK: - Privates internal var animator = ArbitraryAnimator() internal var fractionComplete: CGFloat = 0 } } private extension DSFSparklineOverlay.DataBar { func startAnimateIn() { guard let anim = self.animationStyle else { fatalError() } // Stop any animation that is currently active self.animator.progressBlock = nil self.animator.stop() self.fractionComplete = 0 self.animator.animationFunction = anim.function.function self.animator.duration = anim.duration self.animator.progressBlock = { [weak self] progress in self?.fractionComplete = CGFloat(progress) self?.setNeedsDisplay() } self.animator.start() } } private extension DSFSparklineOverlay.DataBar { func drawDataBarGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { if fractionComplete == 0 { return } let total = self.maximumTotalValue > 0 ? self.maximumTotalValue : self.dataSource.total let rect = bounds.integral var position: CGFloat = rect.minX let delta: CGFloat = (rect.width / total) * self.fractionComplete context.clip(to: rect) if self.maximumTotalValue > 0, let unsetColor = self.unsetColor { let cbheight = max(2, rect.height / 6) let center = rect.midY let centerBar = CGRect(x: rect.minX, y: center - (cbheight / 2), width: rect.width, height: cbheight) let pth = CGPath(roundedRect: centerBar, cornerWidth: cbheight / 2, cornerHeight: cbheight / 2, transform: nil) context.usingGState { state in state.addPath(pth) state.setFillColor(unsetColor) state.fillPath() } } for segment in self.dataSource.values.enumerated() { context.usingGState { state in state.setFillColor(self.palette.cgColorAtOffset(segment.offset)) let width = segment.element * delta let path = CGPath(rect: CGRect(x: position, y: rect.minY, width: width, height: rect.height), transform: nil) state.addPath(path) state.fillPath() if segment.offset > 0, let strokeColor = self.strokeColor { state.usingGState { separator in separator.setStrokeColor(strokeColor) separator.setLineWidth(self.lineWidth) separator.move(to: CGPoint(x: position, y: rect.minY)) separator.addLine(to: CGPoint(x: position, y: rect.maxY)) separator.strokePath() } } position = position + width } } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+Dot.swift ================================================ // // DSFSparklineOverlay+Dot.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore public extension DSFSparklineOverlay { /// A dot graph @objc(DSFSparklineOverlayDot) class Dot: DSFSparklineOverlay.DataSource { /// The number of vertical buckets to break the input data up into @objc public var verticalDotCount: UInt = 10 { didSet { self.setNeedsDisplay() } } /// The color to use when a 'dot' within the bar is on @objc public var onColor: CGColor = .black { didSet { self.setNeedsDisplay() } } /// The color to use when a 'dot' within the bar is off @objc public var offColor: CGColor? { didSet { self.setNeedsDisplay() } } /// Are the values drawn from the top down? @objc public var upsideDown: Bool = false { didSet { self.setNeedsDisplay() } } @objc public init() { super.init() } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.verticalDotCount = orig.verticalDotCount self.onColor = orig.onColor.copy() ?? .black self.offColor = orig.offColor?.copy() self.upsideDown = orig.upsideDown super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } internal override func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { self.drawDotGraph(context: context, bounds: bounds, scale: scale) } } } extension DSFSparklineOverlay.Dot { private func drawDotGraph(context: CGContext, bounds: CGRect, scale _: CGFloat) { guard let dataSource = self.dataSource else { return } let drawRect = bounds let height = drawRect.height let dotHeight = floor(height / CGFloat(self.verticalDotCount)) let xOffset: CGFloat = bounds.width.truncatingRemainder(dividingBy: dotHeight) / 2.0 let yOffset: CGFloat = bounds.height.truncatingRemainder(dividingBy: dotHeight) / 2.0 var position = drawRect.width - dotHeight - xOffset // Map normalized values to box positions let normalizedBoxed: [UInt] = dataSource.normalized.reversed().map { dataPoint in let floatBoxPos = CGFloat(self.verticalDotCount) * dataPoint return UInt(floatBoxPos.rounded(.awayFromZero)) } var pv: [CGRect] = [] var uv: [CGRect] = [] for dataPoint in normalizedBoxed { let boxCount = dataPoint for c in 0 ..< self.verticalDotCount { let pos = self.upsideDown ? (CGFloat(c) * dotHeight) + yOffset : height - (CGFloat(c) * dotHeight) - dotHeight - yOffset let r = CGRect(x: position, y: pos, width: dotHeight, height: dotHeight) let ri = r.insetBy(dx: 0.5, dy: 0.5) if c < boxCount { pv.append(ri) } else { uv.append(ri) } } // Move left. If we've hit the lower bound, then stop position -= dotHeight if position < 0 { break } } if pv.count > 0 { context.usingGState { state in let path = CGMutablePath() path.addRects(pv) state.addPath(path) state.setFillColor(self.onColor) state.drawPath(using: .fill) } } if uv.count > 0, let offColor = self.offColor { context.usingGState { state in let path = CGMutablePath() path.addRects(uv) state.addPath(path) state.setFillColor(offColor) state.drawPath(using: .fill) } } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+Line.swift ================================================ // // Copyright © 2025 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparklineOverlay { /// A line graph sparkline @objc(DSFSparklineOverlayLine) class Line: Centerable { /// The width for the line drawn on the graph @objc public var strokeWidth: CGFloat = 1.5 { didSet { self.setNeedsDisplay() } } /// Interpolate a curve between the points @objc public var interpolated: Bool = false { didSet { self.setNeedsDisplay() } } /// Draw a shadow under the line @objc public var shadow: NSShadow? { didSet { self.setNeedsDisplay() } } /// The size of the markers to draw. If the markerSize is less than 0, markers will not draw @objc public var markerSize: CGFloat = -1 { didSet { self.setNeedsDisplay() } } // MARK: - Markers /// Representation of a marker within the sparkline @objc(DSFSparklineOverlayLineMarker) public class Marker: NSObject { /// The raw data value for the marker @objc public let value: CGFloat /// The rect representing the marker @objc public let rect: CGRect /// If the graph is a centering graph, is this marker considered to be positive or negative? @objc public let isPositiveValue: Bool @objc public init(value: CGFloat, rect: CGRect, isPositiveValue: Bool) { self.value = value self.rect = rect self.isPositiveValue = isPositiveValue } } /// Marker drawing callback /// - Parameters: /// - context: The drawing context /// - markerFrames: The Marker definitions for all of the markers within the current sparkline in left-to-right order public typealias MarkerDrawingBlock = (_ context: CGContext, _ markerFrames: [Marker]) -> Void /// An optional drawing function for custom drawing markers. /// /// The `markerSize` value is used to determine the frameSize of each marker. /// If `markerSize` is less than 1, this block will not be called. /// /// Note that this function is called very frequently so make sure its performant! @objc public lazy var markerDrawingBlock: MarkerDrawingBlock? = nil // Return the drawing function to use when drawing markers private var actualMarkerDrawingBlock: MarkerDrawingBlock { return self.markerDrawingBlock ?? self.DefaultMarkerDrawingBlock } // Convert the default marker drawing function to a block private lazy var DefaultMarkerDrawingBlock: MarkerDrawingBlock = { context, markers in DSFSparklineOverlay.Line.DefaultMarkerDrawingFunc(what: self, context: context, markers: markers) } public override init() { super.init() } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.strokeWidth = orig.strokeWidth self.interpolated = orig.interpolated self.shadow = orig.shadow?.copy() as? NSShadow self.markerSize = orig.markerSize super.init(layer: layer) self.markerDrawingBlock = orig.markerDrawingBlock } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.markerDrawingBlock = nil } // MARK: - Draw handling // Override the edge insets to make sure our line graph fits override internal func edgeInsets(for rect: CGRect) -> DSFEdgeInsets { // If there's a shadow, inset by the maximum shadow offset + blur radius let shadowOffset: CGFloat = { if let s = shadow { return max(s.shadowOffset.width, s.shadowOffset.height) + s.shadowBlurRadius } else { return 0 } }() // Standard inset taking into account marker sizes and shadow offsets let inset = (self.markerSize > 0 ? self.markerSize / 2 : self.strokeWidth) + shadowOffset // Interpolation inset var interpolationInset: CGFloat = 0 if self.interpolated { // Hermite curve matching can overshoot. Until I can find/implement a better curve algorithm that // doesn't overshoot, we'll increase the height inset by a percentage of the height. // The following number is somewhat magic - based on worst cast overshoot and some visual testing let rrr = self.currentPath().boundingBoxOfPath if rrr.minY < 0 { interpolationInset = abs(rrr.minY) / 1.3 } } return DSFEdgeInsets( top: inset + interpolationInset, left: inset, bottom: inset + interpolationInset, right: inset ) } override internal func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { if self.centeredAtZeroLine { self.drawCenteredGraph(context: context, bounds: bounds, scale: scale) } else { self.drawLineGraph(context: context, bounds: bounds, scale: scale) } } } } // MARK: - Default marker drawing private extension DSFSparklineOverlay.Line { // Default marker drawing static func DefaultMarkerDrawingFunc(what: Line, context ctx: CGContext, markers: [Marker]) { guard let primaryStrokeColor = what.primaryStrokeColor else { return } let positiveMarkers = markers.filter { $0.isPositiveValue } let negativeMarkers = markers.filter { $0.isPositiveValue == false } // draw positives. For a non-centered graph, all values are 'positive' if positiveMarkers.count > 0 { ctx.usingGState { pCtx in let path = CGMutablePath() positiveMarkers.forEach { marker in path.addPath(CGPath(ellipseIn: marker.rect, transform: nil)) } pCtx.addPath(path) pCtx.setFillColor(primaryStrokeColor) if let shadow = what.shadow { pCtx.setShadow(shadow) } pCtx.fillPath() } } // draw negatives if negativeMarkers.count > 0 { let strokeColor = what.secondaryStrokeColor ?? primaryStrokeColor ctx.usingGState { nCtx in let path = CGMutablePath() negativeMarkers.forEach { marker in path.addPath(CGPath(ellipseIn: marker.rect, transform: nil)) } nCtx.addPath(path) nCtx.setFillColor(strokeColor) if let shadow = what.shadow { nCtx.setShadow(shadow) } nCtx.fillPath() } } } } // MARK: - Sparkline drawing private extension DSFSparklineOverlay.Line { func currentPath() -> CGPath { guard let dataSource = self.dataSource else { return CGPath(rect: .zero, transform: nil) } let normy = dataSource.normalized let xDiff = bounds.width / CGFloat(normy.count - 1) let points = normy.enumerated().map { CGPoint( x: CGFloat($0.offset) * xDiff + bounds.minX, y: bounds.height + bounds.minY - ($0.element * bounds.height) ) } return CGPath.pathWithPoints(points, smoothed: self.interpolated) } } private extension DSFSparklineOverlay.Line { func drawLineGraph(context: CGContext, bounds: CGRect, scale _: CGFloat) { guard let dataSource = self.dataSource, dataSource.counter != 0 else { // There's no line if there's either no data or just a single point // https://github.com/dagronf/DSFSparkline/issues/3#issuecomment-770324047 return } let normy = dataSource.normalized let xDiff = bounds.width / CGFloat(normy.count - 1) let points = normy.enumerated().map { CGPoint(x: CGFloat($0.offset) * xDiff + bounds.minX, y: bounds.height + bounds.minY - ($0.element * bounds.height)) } let path = CGPath.pathWithPoints(points, smoothed: self.interpolated) var markersBounds: [CGRect] = [] if self.markerSize > 0 { points.forEach { point in let w = self.markerSize / 2 let r = CGRect(x: point.x - w, y: point.y - w, width: self.markerSize, height: self.markerSize) markersBounds.append(r) } } context.usingGState { outer in if dataSource.counter < dataSource.windowSize { let pos = bounds.minX + (CGFloat(dataSource.counter) * xDiff) let clipRect = self.bounds.divided(atDistance: pos, from: .maxXEdge).slice outer.clip(to: clipRect) } if let fill = self.primaryFill { outer.usingGState { ctx in // Note that when using interpolated curves that the `bounds.maxY` value may NOT be zero as we've // scaled the curved graph down to reduce curve clipping. The primary fill needs to extend // down to the _full_ height of the graph or else we end up with a non-filled section at the lower // part of the graph. let clipper = path.mutableCopy()! clipper.addLine(to: CGPoint(x: bounds.maxX, y: points.last!.y)) clipper.addLine(to: CGPoint(x: bounds.maxX, y: self.bounds.maxY)) clipper.addLine(to: CGPoint(x: bounds.minX, y: self.bounds.maxY)) clipper.addLine(to: CGPoint(x: bounds.minX, y: points.first!.y)) clipper.closeSubpath() ctx.addPath(clipper) ctx.clip() fill.fill(context: ctx, bounds: self.bounds) } } if let strokeColor = self.primaryStrokeColor { outer.usingGState { ctx in ctx.addPath(path) ctx.setStrokeColor(strokeColor) ctx.setLineWidth(self.strokeWidth) if let shadow = self.shadow { ctx.setShadow(shadow) } ctx.setLineJoin(.round) ctx.setLineCap(.round) ctx.strokePath() } } /// Draw the markers if !markersBounds.isEmpty { outer.usingGState { ctx in // For the standard line, all values are 'positive' let markers = zip(dataSource.data, markersBounds).map { Marker(value: $0.0, rect: $0.1, isPositiveValue: true) } self.actualMarkerDrawingBlock(ctx, markers) } } } } func drawCenteredGraph(context: CGContext, bounds: CGRect, scale _: CGFloat) { guard let dataSource = self.dataSource, dataSource.counter != 0 else { // There's no line if there's either no data or just a single point // https://github.com/dagronf/DSFSparkline/issues/3#issuecomment-770324047 return } // Map the graph points within the updated bounds let normy = dataSource.normalized let xDiff = bounds.width / CGFloat(normy.count - 1) let points = normy.enumerated().map { CGPoint(x: CGFloat($0.offset) * xDiff + bounds.minX, y: bounds.height + bounds.minY - ($0.element * bounds.height)) } // Calculate the graph centroid let frac = dataSource.fractionalPosition(for: dataSource.zeroLineValue) let centroid = bounds.height - (frac * bounds.height) + bounds.minY var markers: [Marker] = [] if self.markerSize > 0 { points.enumerated().forEach { point in let w = self.markerSize / 2 let r = CGRect(x: point.element.x - w, y: point.element.y - w, width: self.markerSize, height: self.markerSize) markers.append( Marker( value: dataSource.data[point.offset], rect: r, isPositiveValue: !dataSource.valueAtOffsetIsBelowZeroline(point.offset))) } } let path = CGPath.pathWithPoints(points, smoothed: self.interpolated).mutableCopy()! for which in 0 ... 1 { // If the data source doesn't have enough data to fill the graph, then clip to the last x value if dataSource.counter < dataSource.windowSize { let pos = CGFloat(dataSource.counter) * xDiff let clipRect = self.bounds.divided(atDistance: pos, from: .maxXEdge).slice context.clip(to: clipRect) } context.usingGState { inner in // We want to clip the drawing to the _full_ Y range, so that if an interpolated line graph is // scaled to avoid clipping we don't end up with blank spaces at the top and bottom of the graph. let split = self.bounds.divided(atDistance: centroid, from: .minYEdge) let activeRegion = (which == 0) ? split.slice : split.remainder inner.clip(to: activeRegion) // Fill color let fillItem = (which == 0) ? self.primaryFill : self.secondaryFill if let fill = fillItem { inner.usingGState { ctx in // Note that when using interpolated curves that the `bounds.maxY` value may NOT be zero as we've // scaled the curved graph down to reduce curve clipping. The primary fill needs to extend // down to the _full_ height of the graph or else we end up with a non-filled section at the lower // part of the graph. let altY = which == 0 ? self.bounds.maxY : self.bounds.minY let clipper = path.mutableCopy()! clipper.addLine(to: CGPoint(x: bounds.maxX, y: points.last!.y)) clipper.addLine(to: CGPoint(x: bounds.maxX, y: altY)) clipper.addLine(to: CGPoint(x: bounds.minX, y: altY)) clipper.addLine(to: CGPoint(x: bounds.minX, y: points.first!.y)) clipper.closeSubpath() ctx.addPath(clipper) ctx.clip() fill.fill(context: ctx, bounds: activeRegion) } } let whichColor = (which == 0) ? self.primaryStrokeColor : self.secondaryStrokeColor if let stroke = whichColor { inner.usingGState { ctx in ctx.addPath(path) ctx.setStrokeColor(stroke) ctx.setLineWidth(self.strokeWidth) if let shadow = self.shadow { ctx.setShadow(shadow) } ctx.setLineJoin(.round) ctx.setLineCap(.round) ctx.strokePath() } } } } // Draw the markers if self.markerSize > 0 { context.usingGState { ctx in self.actualMarkerDrawingBlock(ctx, markers) } } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+PercentBar.swift ================================================ // // DSFSparklineOverlay+PercentBar.swift // DSFSparklines // // Created by Darren Ford on 16/3/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore import SwiftUI public extension DSFSparkline { @objc class PercentBar: NSObject { /// Percent Bar style class. Assigned values should be in the range 0 ... 1 @objc(DSFSparklinePercentBarStyle) public class Style: NSObject { // MARK: Public /// The corner radius for the bar/underbar @objc public var cornerRadius: CGFloat = 4 /// The graph background color @objc public var underBarColor = CGColor.DefaultWhite /// The color of text when drawn on the background @objc public var underBarTextColor = CGColor.DefaultBlack /// The color of the value bar @objc public var barColor = CGColor.DefaultBlack /// The color of text when drawn on top of the bar @objc public var barTextColor = CGColor.DefaultWhite /// The formatter to use @objc public var numberFormatter = Style.DefaultFormatter /// The font to use @objc public var font = DSFFont.systemFont(ofSize: 12.0) /// How much to inset the value bar from the bounds of the control @objc public var barEdgeInsets = DSFEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) /// Should we display a text percentage value on the bar? @objc public var showLabel: Bool = true @objc public func label(for value: CGFloat) -> String { return self.numberFormatter.string(from: NSNumber(value: Double(value))) ?? "" } // MARK: Private fileprivate static let DefaultFormatter: NumberFormatter = { let f = NumberFormatter() f.numberStyle = .percent f.minimumFractionDigits = 0 f.maximumFractionDigits = 0 return f }() // Convenience fileprivate var fontSize: CGFloat { return self.font.pointSize } } } } public extension DSFSparklineOverlay { /// A percent bar sparkline @objc(DSFSparklineOverlayPercentBar) class PercentBar: DSFSparklineOverlay { /// The value assigned to the percent bar. A value between 0.0 and 1.0 @objc public var value: CGFloat = 0.0 { didSet { self.valueDidChange() } } /// The value that is displayed in the control. This is the clamped version of `value` @objc public private(set) var displayValue: CGFloat = 0.0 /// The style to apply to the percent bar @objc public var displayStyle = DSFSparkline.PercentBar.Style() { didSet { self.displayStyleDidChange() } } /// Animation style @objc public var animationStyle: DSFSparkline.AnimationStyle? = nil internal var animated: Bool { animationStyle != nil } /// Creator @objc public init(style: DSFSparkline.PercentBar.Style = DSFSparkline.PercentBar.Style(), value: CGFloat) { super.init() self.addSublayer(self.fractionLayer) self.addSublayer(self.textLayer) self.cornerRadius = style.cornerRadius self.fractionLayer.zPosition = -3 self.fractionLayer.cornerRadius = self.displayStyle.cornerRadius self.textLayer.contentsScale = 2 self.displayStyle = style self.displayStyleDidChange() self.value = value self.valueDidChange() self.setNeedsLayout() } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.value = orig.value self.displayValue = orig.displayValue super.init(layer: layer) } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } override internal func drawGraph(context _: CGContext, bounds: CGRect, scale _: CGFloat) { // Do nothing. All the content is handled by layers return } // MARK: Layers private let textLayer = LCTextLayer() // Text layer private let fractionLayer = CAShapeLayer() // Bar layer // MARK: Value helpers private var previousValue: CGFloat = 0.0 // The previously assigned fractional value (for animation) // MARK: Animation private var animator = ArbitraryAnimator() // private var fractionComplete: CGFloat = 0 } } extension DSFSparklineOverlay.PercentBar { override public func layoutSublayers() { super.layoutSublayers() self.layoutGraph() } private func layoutGraph() { CATransaction.withDisabledActions { let diff = self.displayValue - self.previousValue let fDiff = diff * self.fractionComplete let complete = fDiff + self.previousValue let nb = bounds.inset(by: self.displayStyle.barEdgeInsets) let divide = nb.divided(atDistance: complete * bounds.width, from: .minXEdge) let setRect = divide.slice self.fractionLayer.frame = setRect if self.displayStyle.showLabel { let textSz = self.textLayer.textBounds( for: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).width let leftRect = setRect.insetBy(dx: 3, dy: 0) if textSz > leftRect.width { self.textLayer.frame = divide.remainder.insetBy(dx: 3, dy: 0) self.textLayer.alignmentMode = .left self.textLayer.foregroundColor = self.displayStyle.underBarTextColor } else { self.textLayer.frame = leftRect self.textLayer.alignmentMode = .right self.textLayer.foregroundColor = self.displayStyle.barTextColor } } } } } private extension DSFSparklineOverlay.PercentBar { func valueDidChange() { // Store the previous value for our animation self.previousValue = self.displayValue // Make sure our internal fraction value is always 0 ... 1 self.displayValue = self.value.clamped(to: 0 ... 1) // Recreate the text for the label self.updateLabel() // And notify that data changed self.dataDidChange() } func updateLabel() { let label = self.displayStyle.label(for: self.displayValue) CATransaction.withDisabledActions { self.textLayer.string = label } } func dataDidChange() { if self.animated { self.startAnimateIn() } else { self.fractionComplete = 1.0 self.setNeedsDisplay() } } func startAnimateIn() { guard let anim = self.animationStyle else { fatalError() } // Stop any animation that is currently active self.animator.progressBlock = nil self.animator.stop() self.fractionComplete = 0 self.animator.animationFunction = anim.function.function self.animator.duration = anim.duration self.animator.progressBlock = { [weak self] progress in self?.fractionComplete = CGFloat(progress) self?.setNeedsLayout() } self.animator.start() } func displayStyleDidChange() { CATransaction.withDisabledActions { self.cornerRadius = self.displayStyle.cornerRadius self.backgroundColor = self.displayStyle.underBarColor self.fractionLayer.backgroundColor = self.displayStyle.barColor self.fractionLayer.cornerRadius = self.displayStyle.cornerRadius self.textLayer.isHidden = !self.displayStyle.showLabel self.textLayer.font = CTFontCreateWithFontDescriptor(self.displayStyle.font.fontDescriptor, 0, nil) self.textLayer.fontSize = self.displayStyle.fontSize } self.setNeedsDisplay() } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+Pie.swift ================================================ // // DSFSparklineOverlay+Pie.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore public extension DSFSparklineOverlay { /// A pie graph @objc(DSFSparklineOverlayPie) class Pie: DSFSparklineOverlay.StaticDataSource { /// The palette to use when drawing the pie chart @objc public var palette = DSFSparkline.Palette.shared { didSet { self.setNeedsDisplay() } } /// The stroke color for the pie chart @objc public var strokeColor: CGColor? { didSet { self.setNeedsDisplay() } } /// The width of the stroke line @objc public var lineWidth: CGFloat = 0.5 { didSet { self.setNeedsDisplay() } } /// Should the pie chart animate in? @objc public var animationStyle: DSFSparkline.AnimationStyle? = nil // Should we animate? private var animated: Bool { animationStyle != nil } /// Create @objc public override init() { super.init() } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.palette = orig.palette.copyPalette() self.strokeColor = orig.strokeColor?.copy() self.lineWidth = orig.lineWidth self.animationStyle = orig.animationStyle super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func staticDataSourceDidChange() { super.staticDataSourceDidChange() if self.animated { self.startAnimateIn() } else { self.fractionComplete = 1.0 self.setNeedsDisplay() } } override internal func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { self.drawPieGraph(context: context, bounds: bounds, scale: scale) } // MARK: - Privates internal var animator = ArbitraryAnimator() internal var fractionComplete: CGFloat = 0 } } private extension DSFSparklineOverlay.Pie { func startAnimateIn() { guard let anim = self.animationStyle else { fatalError() } // Stop any animation that is currently active self.animator.stop() self.fractionComplete = 0 self.animator.animationFunction = anim.function.function self.animator.duration = anim.duration self.animator.progressBlock = { [weak self] progress in self?.fractionComplete = CGFloat(progress) self?.setNeedsDisplay() } self.animator.start() } } private extension DSFSparklineOverlay.Pie { func drawPieGraph(context: CGContext, bounds: CGRect, scale _: CGFloat) { if fractionComplete == 0 { return } let rect = bounds.insetBy(dx: 1, dy: 1) // radius is the half the frame's width or height (whichever is smallest) let radius = min(rect.width, rect.height) * 0.5 // center of the view let viewCenter = CGPoint(x: rect.midX, y: rect.midY) // the starting angle is -90 degrees (top of the circle, as the context is flipped). By default, 0 is the right hand side of the circle, with the positive angle being in an anti-clockwise direction (same as a unit circle in maths). var startAngle = -CGFloat.pi * 0.5 for segment in self.dataSource.values.enumerated() { // loop through the values array context.usingGState { state in // set fill color to the segment color state.setFillColor(self.palette.cgColorAtOffset(segment.offset)) // update the end angle of the segment let fraEndAngle = startAngle + (2 * .pi * (segment.element / self.dataSource.total)) * fractionComplete // move to the center of the pie chart state.move(to: viewCenter) // add arc from the center for each segment (anticlockwise is specified for the arc, but as the view flips the context, it will produce a clockwise arc) state.addArc(center: viewCenter, radius: radius, startAngle: startAngle, endAngle: fraEndAngle, clockwise: false) state.drawPath(using: .fill) // update starting angle of the next segment to the ending angle of this segment startAngle = fraEndAngle // endAngle } } // We draw the strokes AFTER we draw ALL the segment fills to avoid unpleasant rendering if let stroke = self.strokeColor { var startAngle = -CGFloat.pi * 0.5 context.setStrokeColor(stroke) context.setLineWidth(self.lineWidth) for segment in self.dataSource.values.enumerated() { // loop through the values array context.usingGState { state in // update the end angle of the segment let fraEndAngle = startAngle + (2 * .pi * (segment.element / self.dataSource.total)) * fractionComplete // move to the center of the pie chart state.move(to: viewCenter) // add arc from the center for each segment (anticlockwise is specified for the arc, but as the view flips the context, it will produce a clockwise arc) state.addArc(center: viewCenter, radius: radius, startAngle: startAngle, endAngle: fraEndAngle, clockwise: false) state.drawPath(using: .stroke) // update starting angle of the next segment to the ending angle of this segment startAngle = fraEndAngle // endAngle } } } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+Stackline.swift ================================================ // // Copyright © 2025 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparklineOverlay { /// A stackline graph @objc(DSFSparklineOverlayStackline) class Stackline: Centerable { /// The width for the line drawn on the graph @objc public var strokeWidth: CGFloat = 1.5 { didSet { self.setNeedsDisplay() } } /// Interpolate a curve between the points @objc public var barSpacing: UInt = 1 { didSet { self.setNeedsDisplay() } } /// Draw a shadow under the line @objc public var shadow: NSShadow? { didSet { self.setNeedsDisplay() } } @objc public override init() { super.init() } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.strokeWidth = orig.strokeWidth self.barSpacing = orig.barSpacing self.shadow = orig.shadow?.copy() as? NSShadow super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override internal func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { if self.centeredAtZeroLine { self.drawCenteredStackLineGraph(context: context, bounds: bounds, scale: scale) } else { self.drawStackLineGraph(context: context, bounds: bounds, scale: scale) } } } } extension DSFSparklineOverlay.Stackline { private func drawStackLineGraph(context: CGContext, bounds: CGRect, scale _: CGFloat) { guard let dataSource = self.dataSource else { return } let integralRect = bounds.integral let integralHeight: CGFloat = integralRect.height // This represents the _full_ width of a bar within the graph, including the spacing. let componentWidth = Int(integralRect.width) / Int(dataSource.windowSize) // The left offset in order to center X let xOffset: Int = (Int(bounds.width) - (componentWidth * Int(dataSource.windowSize))) / 2 // The available height range let range: ClosedRange = 1 ... max(1, integralRect.maxY) // The normalized (0 -> 1) data points let normalized = dataSource.normalized let points: [CGPoint] = normalized.enumerated().map { let xVal = xOffset + ($0.offset * componentWidth) let yVal = (integralHeight - ($0.element * integralHeight)).clamped(to: range) return CGPoint(x: xVal, y: Int(yVal)) } let ordered = points.enumerated() let emptyBuckets = Int(dataSource.emptyValueCount) let available = Array(ordered.dropFirst(emptyBuckets)) if available.count == 0 { // Nothing to do! return } context.usingGState { outer in outer.setRenderingIntent(.relativeColorimetric) outer.interpolationQuality = .none outer.setShouldAntialias(false) let linePath = CGMutablePath() for point in available { let currentPoint = point.element if linePath.isEmpty { // First point linePath.move(to: currentPoint) linePath.addLine(to: currentPoint.offsettingX(by: CGFloat(componentWidth))) } else { linePath.addLine(to: currentPoint) linePath.addLine(to: point.element.offsettingX(by: CGFloat(componentWidth))) } } if linePath.isEmpty { return } outer.usingGState { fillCtx in let fillPath = linePath.mutableCopy()! let bounds = fillPath.boundingBox fillPath.addLine(to: CGPoint(x: bounds.maxX, y: integralRect.maxY)) fillPath.addLine(to: CGPoint(x: bounds.minX, y: integralRect.maxY)) fillPath.closeSubpath() fillCtx.addPath(fillPath) fillCtx.clip() self.primaryFill?.fill(context: fillCtx, bounds: integralRect) } if let stroke = self.primaryStrokeColor { outer.addPath(linePath) outer.setStrokeColor(stroke) outer.setLineWidth(self.strokeWidth) if let shadow = self.shadow { outer.setShadow(shadow) } outer.strokePath() } } } private func drawCenteredStackLineGraph(context: CGContext, bounds: CGRect, scale _: CGFloat) { guard let dataSource = self.dataSource else { return } let integralRect = bounds.integral let integralHeight: CGFloat = integralRect.height // This represents the _full_ width of a bar within the graph, including the spacing. let componentWidth = Int(integralRect.width) / Int(dataSource.windowSize) // The left offset in order to center X let xOffset: Int = (Int(bounds.width) - (componentWidth * Int(dataSource.windowSize))) / 2 // The available height range let range: ClosedRange = 1 ... max(1, integralRect.maxY) // The normalized (0 -> 1) data points let normalized = dataSource.normalized let points: [CGPoint] = normalized.enumerated().map { let xVal = xOffset + ($0.offset * componentWidth) let yVal = (integralHeight - ($0.element * integralHeight)).clamped(to: range) return CGPoint(x: xVal, y: Int(yVal)) } let ordered = points.enumerated() let emptyBuckets = Int(dataSource.emptyValueCount) let available = Array(ordered.dropFirst(emptyBuckets)) if available.count == 0 { // Nothing to do! return } let centroid = (1 - dataSource.normalizedZeroLineValue) * (integralHeight - 1) let first = available.first! if first.offset != 0 { var clip = bounds clip.origin.x = first.element.x clip.size.width -= first.element.x context.clip(to: clip) } context.usingGState { outer in outer.setRenderingIntent(.relativeColorimetric) outer.interpolationQuality = .none outer.setShouldAntialias(false) let linePath = CGMutablePath() linePath.move(to: CGPoint(x: integralRect.minX - 1, y: centroid)) linePath.addLine(to: CGPoint(x: integralRect.minX - 1, y: available.first!.element.y)) for point in available { let currentPoint = point.element linePath.addLine(to: currentPoint) linePath.addLine(to: point.element.offsettingX(by: CGFloat(componentWidth))) } linePath.addLine(to: CGPoint(x: integralRect.maxX + 1, y: available.last!.element.y)) linePath.addLine(to: CGPoint(x: integralRect.maxX + 1, y: centroid)) if linePath.isEmpty { return } let outerClip = integralRect.insetBy(dx: CGFloat(xOffset), dy: 0) outer.clip(to: outerClip) outer.usingGState { clipped in let split = outerClip.divided(atDistance: centroid, from: .minYEdge) for which in 0 ... 1 { clipped.usingGState { inner in let whichRegion = (which == 0) ? split.slice : split.remainder inner.clip(to: whichRegion) let fillItem = (which == 0) ? self.primaryFill : self.secondaryFill if let fill = fillItem { inner.usingGState { fillCtx in fillCtx.addPath(linePath) fillCtx.clip() fill.fill(context: fillCtx, bounds: whichRegion) } } let whichStroke = (which == 0) ? self.primaryStrokeColor : self.secondaryStrokeColor if let strokeColor = whichStroke { inner.addPath(linePath) inner.setStrokeColor(strokeColor) inner.setLineWidth(self.strokeWidth) if let shadow = self.shadow { inner.setShadow(shadow) } inner.strokePath() } } } } } return } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+Stripes.swift ================================================ // // DSFSparklineOverlay+Stripes.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore public extension DSFSparklineOverlay { @objc(DSFSparklineOverlayStripes) class Stripes: DSFSparklineOverlay.DataSource { // A default gradient pattern static let defaultGradient = DSFSparkline.GradientBucket(posts: [ DSFSparkline.GradientBucket.Post(color: DSFColor.systemRed.cgColor, location: 0), DSFSparkline.GradientBucket.Post(color: DSFColor.systemOrange.cgColor, location: 1 / 5), DSFSparkline.GradientBucket.Post(color: DSFColor.systemYellow.cgColor, location: 2 / 5), DSFSparkline.GradientBucket.Post(color: DSFColor.systemGreen.cgColor, location: 3 / 5), DSFSparkline.GradientBucket.Post(color: DSFColor.systemBlue.cgColor, location: 4 / 5), DSFSparkline.GradientBucket.Post(color: DSFColor.systemPurple.cgColor, location: 5 / 5), ]) /// The width of the stroke for the tablet @objc public var integral: Bool = true { didSet { self.setNeedsDisplay() } } /// The spacing (in pixels) between each bar @objc public var barSpacing: UInt = 1 { didSet { self.setNeedsDisplay() } } /// The color gradient to use when rendering. /// /// Note that transparent gradients display strangely and not as I would expect them to. /// Stick with solid colors in your gradient for the current time. @objc public var gradient: DSFSparkline.GradientBucket = Stripes.defaultGradient { didSet { self.setNeedsDisplay() } } @objc public init() { super.init() } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.integral = orig.integral self.barSpacing = orig.barSpacing self.gradient = orig.gradient.copyGradientBucket() super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override internal func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { if self.integral { self.drawStripeGraph(context: context, bounds: bounds, scale: scale) } else { self.drawStripeGraphFloat(context: context, bounds: bounds, scale: scale) } } } } private extension DSFSparklineOverlay.Stripes { func drawStripeGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { guard let dataSource = self.dataSource else { return } let integralRect = bounds.integral // This represents the _full_ width of a bar within the graph, including the spacing. let componentWidth = Int(integralRect.width) / Int(dataSource.windowSize) // The width of the BAR component let barWidth = componentWidth - Int(barSpacing) // The left offset in order to center X let xOffset: Int = Int(self.bounds.minX) + (Int(bounds.width) - (componentWidth * Int(dataSource.windowSize))) / 2 let normalizedPoints = dataSource.normalized context.usingGState { outer in outer.setRenderingIntent(.perceptual) outer.interpolationQuality = .default outer.setShouldAntialias(false) if dataSource.counter < dataSource.windowSize { let pos = xOffset + (Int(dataSource.counter) * componentWidth) let clipRect = bounds.divided(atDistance: CGFloat(pos), from: .maxXEdge).slice outer.clip(to: clipRect) } for value in normalizedPoints.enumerated() { outer.usingGState { inner in let color = gradient.color(at: value.element) inner.setFillColor(color) let r = CGRect(x: CGFloat(xOffset + value.offset * componentWidth), y: integralRect.minX, width: CGFloat(barWidth) + (1.0 / scale), height: integralRect.height) inner.fill(r) } } } } private func drawStripeGraphFloat(context: CGContext, bounds: CGRect, scale _: CGFloat) { guard let dataSource = self.dataSource else { return } let drawRect = bounds // This represents the _full_ width of a bar within the graph, including the spacing. let componentWidth = drawRect.width / CGFloat(dataSource.windowSize) // The width of the BAR component let barWidth = componentWidth - CGFloat(barSpacing) let normalizedPoints = dataSource.normalized context.usingGState { outer in if dataSource.counter < dataSource.windowSize { let pos = CGFloat(dataSource.counter) * componentWidth let clipRect = bounds.divided(atDistance: CGFloat(pos), from: .maxXEdge).slice outer.clip(to: clipRect) } for value in normalizedPoints.enumerated() { outer.usingGState { inner in let color = gradient.color(at: value.element) inner.setFillColor(color) let r = CGRect( x: bounds.minX + CGFloat(value.offset) * componentWidth - (barSpacing == 0 ? 0.5 : 0), y: drawRect.minY, width: barWidth + (barSpacing == 0 ? 0.5 : 0), height: drawRect.height ) inner.addRect(r) inner.drawPath(using: .fill) } } } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+Tablet.swift ================================================ // // DSFSparklineOverlay+Tablet.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore public extension DSFSparklineOverlay { /// A tablet-style graph. @objc(DSFSparklineOverlayTablet) class Tablet: DSFSparklineOverlay.DataSource { static let greenStroke = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 1, 0, 1])! static let redStroke = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 0, 0, 1])! static let greenFill = DSFSparkline.Fill.Color(CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 1, 0, 0.3])!) static let redFill = DSFSparkline.Fill.Color(CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 0, 0, 0.3])!) /// The width of the stroke for the tablet @objc public var lineWidth: CGFloat = 1.0 { didSet { self.setNeedsDisplay() } } /// The spacing (in pixels) between each tablet @objc public var tabletSpacing: CGFloat = 1.0 { didSet { self.setNeedsDisplay() } } /// The color to draw the win tablets @objc public var winStrokeColor: CGColor = Tablet.greenStroke { didSet { self.setNeedsDisplay() } } /// The color to draw the 'win' boxes @objc public var winFill: DSFSparklineFillable? = Tablet.greenFill { didSet { self.setNeedsDisplay() } } /// The color to draw the win tablets @objc public var lossStrokeColor: CGColor = Tablet.redStroke { didSet { self.setNeedsDisplay() } } /// The color to draw the 'win' boxes @objc public var lossFill: DSFSparklineFillable? { didSet { self.setNeedsDisplay() } } @objc public init() { super.init() } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.lineWidth = orig.lineWidth self.tabletSpacing = orig.tabletSpacing self.winStrokeColor = orig.winStrokeColor self.winFill = orig.winFill?.copyFill() self.lossStrokeColor = orig.lossStrokeColor self.lossFill = orig.lossFill?.copyFill() super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } internal override func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { self.drawTabletGraph(context: context, bounds: bounds, scale: scale) } } } private extension DSFSparklineOverlay.Tablet { func drawTabletGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { guard let dataSource = self.dataSource else { return } let integralRect = bounds.insetBy(dx: 0, dy: 1) let windowSize = CGFloat(dataSource.windowSize) // The amount of space left in the rect once we've removed the bar spacing for all elements let w = integralRect.width - (windowSize * (self.tabletSpacing + self.lineWidth)) - 2*self.lineWidth // The size of a circle let circleSize = min(w / CGFloat(windowSize), integralRect.height) // This represents the _full_ width of a circle, including the spacing. let componentWidth = circleSize + self.tabletSpacing + self.lineWidth // The left offset in order to center X let xOffset: CGFloat = (integralRect.width - (componentWidth * windowSize)) / 2 // Map the +ve values to true, the -ve (and 0) to false let winLoss: [Int] = dataSource.data.map { if $0 > 0 { return 1 } return -1 } let midPoint = bounds.midY context.usingGState { outer in if dataSource.counter < dataSource.windowSize { let pos = CGFloat(dataSource.counter) * componentWidth let clipRect = integralRect.divided(atDistance: CGFloat(pos + xOffset + CGFloat(self.tabletSpacing / 2)), from: .maxXEdge).slice outer.clip(to: clipRect.integral) } let winPath = CGMutablePath() let lossPath = CGMutablePath() for point in winLoss.enumerated() { let x = xOffset + CGFloat(point.offset) * componentWidth if point.element == 1 { let rect = CGRect(x: x, y: midPoint - (circleSize / 2), width: circleSize, height: circleSize) winPath.addEllipse(in: rect.integral) } else if point.element == -1 { let rect = CGRect(x: x, y: midPoint - (circleSize / 2), width: circleSize, height: circleSize) lossPath.addEllipse(in: rect.integral) } } if !winPath.isEmpty { outer.usingGState { winState in if let fill = self.winFill { winState.usingGState { (fillCtx) in winState.addPath(winPath) winState.clip() fill.fill(context: fillCtx, bounds: integralRect) } } winState.addPath(winPath) winState.setStrokeColor(self.winStrokeColor) winState.setLineWidth(self.lineWidth) winState.drawPath(using: .stroke) } } if !lossPath.isEmpty { outer.usingGState { lossState in if let fill = self.lossFill { lossState.usingGState { (fillCtx) in lossState.addPath(lossPath) lossState.clip() fill.fill(context: fillCtx, bounds: integralRect) } } lossState.addPath(lossPath) lossState.setStrokeColor(self.lossStrokeColor) lossState.setLineWidth(self.lineWidth) lossState.drawPath(using: .stroke) } } } } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+WinLossTie.swift ================================================ // // DSFSparklineOverlay+WinLossTie.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import QuartzCore public extension DSFSparklineOverlay { @objc(DSFSparklineOverlayWinLossTie) class WinLossTie: DSFSparklineOverlay.DataSource { static let greenStroke = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 1, 0, 1])! static let greenFill = DSFSparkline.Fill.Color(CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 1, 0, 0.3])!) static let redStroke = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 0, 0, 1])! static let redFill = DSFSparkline.Fill.Color(CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 0, 0, 0.3])!) /// The width of the stroke for the tablet @objc public var lineWidth: UInt = 1 { didSet { self.setNeedsDisplay() } } /// The spacing (in pixels) between each bar @objc public var barSpacing: UInt = 1 { didSet { self.setNeedsDisplay() } } /// The color to draw the 'win' boxes @objc public var winStroke: CGColor = WinLossTie.greenStroke { didSet { self.setNeedsDisplay() } } /// The color to draw the 'win' boxes @objc public var winFill: DSFSparklineFillable? = WinLossTie.greenFill { didSet { self.setNeedsDisplay() } } /// The color to draw the 'loss' boxes @objc public var lossStroke: CGColor = WinLossTie.redStroke { didSet { self.setNeedsDisplay() } } /// The color to draw the 'loss' boxes @objc public var lossFill: DSFSparklineFillable? = WinLossTie.redFill { didSet { self.setNeedsDisplay() } } /// The color to draw the 'tie' boxes @objc public var tieStroke: CGColor? { didSet { self.setNeedsDisplay() } } /// The color to draw the 'tie' boxes @objc public var tieFill: DSFSparklineFillable? { didSet { self.setNeedsDisplay() } } /// The line to separate the win and loss sections of the sparkline @objc public var centerLine: DSFSparkline.ZeroLineDefinition? { didSet { self.setNeedsDisplay() } } @objc public init() { super.init() } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.lineWidth = orig.lineWidth self.barSpacing = orig.barSpacing self.winStroke = orig.winStroke self.winFill = orig.winFill?.copyFill() self.lossStroke = orig.lossStroke self.lossFill = orig.lossFill?.copyFill() self.tieStroke = orig.tieStroke self.tieFill = orig.tieFill?.copyFill() self.centerLine = orig.centerLine?.copyZeroLineDefinition() super.init(layer: layer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } internal override func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { self.drawWinLossGraph(context: context, bounds: bounds, scale: scale) } } } private extension DSFSparklineOverlay.WinLossTie { func drawWinLossGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { guard let dataSource = self.dataSource else { return } let integralRect = bounds.integral let windowSize: Int = Int(dataSource.windowSize) // This represents the _full_ width of a bar within the graph, including the spacing. let componentWidth: Int = Int(integralRect.width) / windowSize // The width of the BAR component let barWidth: Int = componentWidth - Int(self.barSpacing) // The left offset in order to center X let xOffset: Int = (Int(integralRect.width) - (componentWidth * windowSize)) / 2 // Map the +ve values to true, the -ve (and 0) to false let winLoss: [Int] = dataSource.data.map { if $0 > 0 { return 1 } if $0 < 0 { return -1 } return 0 } let graphLineWidth: CGFloat = 1 / scale * CGFloat(self.lineWidth) let midPoint = Int(bounds.midY.rounded()) let barHeight = Int(integralRect.midY) - Int(self.lineWidth) context.usingGState { outer in outer.setShouldAntialias(false) outer.setRenderingIntent(.relativeColorimetric) outer.interpolationQuality = .none if let zeroLine = self.centerLine { outer.usingGState { centerlineState in let zeroPos = 0.5 * bounds.height centerlineState.setLineWidth(zeroLine.lineWidth) centerlineState.setStrokeColor(zeroLine.color.cgColor) centerlineState.setLineDash(phase: 0.0, lengths: zeroLine.lineDashStyle) centerlineState.strokeLineSegments(between: [CGPoint(x: 0.0, y: zeroPos), CGPoint(x: bounds.width, y: zeroPos)]) } } if dataSource.counter < dataSource.windowSize { let pos = Int(dataSource.counter) * componentWidth let clipRect = integralRect.divided(atDistance: CGFloat(pos + xOffset), from: .maxXEdge).slice outer.clip(to: clipRect.integral) } let winPath = CGMutablePath() let lossPath = CGMutablePath() let tiePath = CGMutablePath() for point in winLoss.enumerated() { let x = xOffset + point.offset * componentWidth if point.element == 1 { let rect = CGRect(x: x, y: 1, width: barWidth, height: barHeight) winPath.addRect(rect.integral) } else if point.element == -1 { let rect = CGRect(x: x, y: midPoint + 1, width: barWidth, height: barHeight) lossPath.addRect(rect.integral) } else { let rect = CGRect(x: x, y: Int(integralRect.height) / 2 - (barHeight / 4), width: barWidth, height: barHeight / 2) tiePath.addRect(rect.integral) } } if !winPath.isEmpty { outer.usingGState { winState in if let fill = self.winFill { winState.usingGState { fillCtx in fillCtx.addPath(winPath) fillCtx.clip() fill.fill(context: fillCtx, bounds: integralRect) } } winState.addPath(winPath) winState.setStrokeColor(self.winStroke) winState.setLineWidth(graphLineWidth) winState.drawPath(using: .stroke) } } if !lossPath.isEmpty { outer.usingGState { lossState in if let fill = self.lossFill { lossState.usingGState { fillCtx in fillCtx.addPath(lossPath) fillCtx.clip() fill.fill(context: fillCtx, bounds: integralRect) } } lossState.addPath(lossPath) lossState.setStrokeColor(self.lossStroke) lossState.setLineWidth(graphLineWidth) lossState.drawPath(using: .stroke) } } if !tiePath.isEmpty, self.tieFill != nil || self.tieStroke != nil { if let fill = self.tieFill { outer.usingGState { tieState in tieState.addPath(tiePath) tieState.clip() fill.fill(context: tieState, bounds: integralRect) } } if let stroke = self.tieStroke { outer.usingGState { tieState in tieState.addPath(tiePath) tieState.setLineWidth(graphLineWidth) tieState.setStrokeColor(stroke) tieState.drawPath(using: .stroke) } } } } return } } ================================================ FILE: Sources/DSFSparkline/overlay/renderers/overlays-graph/DSFSparklineOverlay+WiperGauge.swift ================================================ // // DSFSparklineOverlay+WiperGauge.swift // DSFSparklines // // Copyright © 2023 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import QuartzCore #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparklineOverlay { /// A pie graph @objc(DSFSparklineOverlayWiperGauge) class WiperGauge: DSFSparklineOverlay.StaticDataSource { // MARK: - Settings /// The value to display in the gauge @objc public var value: CGFloat = 0.0 { didSet { let v = max(0, min(1, self.value)) let tra = DSFSparkline.AnimationTransition(start: currentValue__, stop: v) if isAnimated { self.animate(tra) } else { self.currentValue__ = v } } } /// The palette to use when drawing the value part of the gauge @objc public var valueColor: DSFSparkline.ValueBasedFill = DSFSparkline.ValueBasedFill.sharedPalette { didSet { self.updatePalette() } } /// The palette to use when drawing the unset value part of the gauge @objc public var valueBackgroundColor: CGColor = _valueBackgroundColor { didSet { self.updatePalette() } } /// The color to draw the dial and outer border @objc public var gaugeUpperArcColor: CGColor = _gaugeUpperArcColor { didSet { self.updatePalette() } } /// The color to draw the value in radial component @objc public var gaugePointerColor: CGColor = _gaugePointerColor { didSet { self.updatePalette() } } /// The color to draw in the background @objc public var gaugeBackgroundColor: CGColor? = _gaugeBackgroundColor { didSet { self.updatePalette() } } /// Should the pie chart animate in? @objc public var animationStyle: DSFSparkline.AnimationStyle? = nil private var isAnimated: Bool { self.animationStyle != nil } // MARK: - Initializers @objc public override init() { super.init() self.configure() } public override init(layer: Any) { guard let orig = layer as? Self else { fatalError() } self.value = orig.value self.valueColor = orig.valueColor.copyColorContainer() self.valueBackgroundColor = orig.valueBackgroundColor.copy() ?? CGColor(gray: 0, alpha: 0) self.gaugePointerColor = orig.gaugePointerColor.copy() ?? CGColor(gray: 1, alpha: 1) self.gaugeUpperArcColor = orig.gaugeUpperArcColor.copy() ?? CGColor(gray: 1, alpha: 1) self.gaugeBackgroundColor = orig.gaugeBackgroundColor?.copy() ?? CGColor(gray: 0, alpha: 0) self.animationStyle = orig.animationStyle super.init(layer: layer) self.configure() } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Private override internal func drawGraph(context: CGContext, bounds: CGRect, scale: CGFloat) { // Do nothing. All the content is handled by layers return } // Private private let wiperBackgroundShape = CAShapeLayer() private let arcShape = CAShapeLayer() private let arcInnerShape = CAShapeLayer() private let arcColorInnerShape = CAShapeLayer() private let pinion = CAShapeLayer() private let arcline = CAShapeLayer() private var animator = ArbitraryAnimator() private var currentValue__: CGFloat = 0.0 { didSet { self.updatePalette() self.updateLayout() } } private static let _gaugeBackgroundColor: CGColor? = nil #if os(macOS) private static let _valueBackgroundColor = NSColor.quaternaryLabelColor.cgColor private static let _gaugeUpperArcColor = NSColor.textColor.cgColor private static let _gaugePointerColor = NSColor.textColor.cgColor #else private static let _valueBackgroundColor = UIColor.quaternaryLabel.cgColor private static let _gaugeUpperArcColor = UIColor.label.cgColor private static let _gaugePointerColor = UIColor.label.cgColor #endif } } extension DSFSparklineOverlay.WiperGauge { private func configure() { self.addSublayer(wiperBackgroundShape) wiperBackgroundShape.zPosition = -110 self.addSublayer(arcShape) arcShape.zPosition = -100 self.addSublayer(arcInnerShape) arcInnerShape.zPosition = -90 self.addSublayer(arcColorInnerShape) arcColorInnerShape.zPosition = -80 self.addSublayer(pinion) pinion.zPosition = -70 self.addSublayer(arcline) arcline.zPosition = -80 self.updatePalette() } // Update the colors used within the gauge private func updatePalette() { // The gauge's background color wiperBackgroundShape.fillColor = self.gaugeBackgroundColor if self.valueColor.isPalette == false { // We want paletted colors to fade when the color changes CATransaction.setDisableActions(true) } // The outer ring arcShape.fillColor = .clear arcShape.strokeColor = self.gaugeUpperArcColor // The color component of the arc arcColorInnerShape.fillColor = self.valueColor.color(atFraction: self.value) arcColorInnerShape.strokeColor = .clear // The background color component of the arc arcInnerShape.fillColor = self.valueBackgroundColor arcInnerShape.strokeColor = .clear // The pointer and the pinion arcline.strokeColor = self.gaugePointerColor pinion.fillColor = self.gaugePointerColor if self.valueColor.isPalette == false { CATransaction.commit() } } private func updateLayout() { CATransaction.setDisableActions(true) defer { CATransaction.commit() } let bb = self.bounds if bb.isEmpty { return } let sx = bb.width let sy = bb.height var destWidth: CGFloat = 0 var destHeight: CGFloat = 0 let rr: CGRect = { if (sx / 2) > sy { // wider than it is higher let dx = sy/(sx/2) destWidth = bb.width * dx destHeight = bb.height return CGRect( x: (bb.width - destWidth) / 2, y: 0, width: destWidth, height: destHeight ) } else { // higher than it is wide let dy = (sx / 2) / sy destWidth = bb.width destHeight = bb.height * dy let x = (bb.width - destWidth) / 2.0 let y = (bb.height - destHeight) / 2.0 return CGRect( x: x, y: y, width: destWidth, height: destHeight ) } }() let sz = rr.size.height let ptsize = sz / 12 let origin = CGPoint( x: (rr.width / 2) + rr.origin.x, y: rr.origin.y + ptsize ) // The gauge's pinion point let centroidLocation = CGPoint(x: origin.x, y: bb.height - (bb.height - rr.height) / 2 - ptsize) // Draw the background do { let pth = CGMutablePath() pth.addArc(center: centroidLocation, radius: sz - ptsize, startAngle: .pi, endAngle: .pi * 2, clockwise: false) let brect = CGRect( x: centroidLocation.x - (sz - ptsize), y: centroidLocation.y, width: (sz - ptsize) * 2, height: ptsize ) pth.addRect(brect) pth.closeSubpath() wiperBackgroundShape.path = pth } // Draw the outer ring do { let path = CGMutablePath() path.addArc(center: centroidLocation, radius: sz - (ptsize * 2), startAngle: .pi + 0.2, endAngle: .pi - .pi - 0.2, clockwise: false) arcShape.path = path arcShape.lineWidth = ptsize arcShape.lineCap = .round } let frac: CGFloat = self.currentValue__ let fullSweep: CGFloat = .pi - 0.2 - 0.2 // Draw the inner sweeps do { // Color fill do { let colorstart: CGFloat = ((1.0 - frac) * fullSweep) let pth = CGMutablePath() pth.addArc(center: centroidLocation, radius: ptsize * 2, startAngle: .pi + 0.2, endAngle: .pi * 2 - 0.2 - colorstart, clockwise: false) pth.addArc(center: centroidLocation, radius: (sz - (ptsize * 2)) / 1.15, startAngle: .pi * 2 - 0.2 - colorstart, endAngle: .pi + 0.2, clockwise: true) pth.closeSubpath() arcColorInnerShape.path = pth } // Background fill do { let colorstart: CGFloat = (frac * fullSweep) let pth = CGMutablePath() pth.addArc(center: centroidLocation, radius: ptsize * 2, startAngle: .pi + 0.2 + colorstart, endAngle: .pi * 2 - 0.2, clockwise: false) pth.addArc(center: centroidLocation, radius: (sz - (ptsize * 2)) / 1.15, startAngle: .pi * 2 - 0.2, endAngle: .pi + 0.2 + colorstart, clockwise: true) pth.closeSubpath() arcInnerShape.path = pth } } // Draw the pinon (the circle where the pointer rotates around) do { let dialPoint = CGPoint(x: centroidLocation.x - ptsize, y: centroidLocation.y - ptsize) let pointy = CGRect(origin: dialPoint, size: CGSize(width: ptsize*2, height: ptsize*2)) let path2 = CGPath(ellipseIn: pointy, transform: nil) pinion.path = path2 } // Draw the pointer do { let pth = CGMutablePath() pth.move(to: CGPoint(x: 0, y: 0)) pth.addLine(to: CGPoint(x: sz / 1.4, y: 0)) let colorstart = ((1.0 - frac) * fullSweep) let res = CGMutablePath() let transform = CGAffineTransform(translationX: centroidLocation.x, y: centroidLocation.y) .rotated(by: .pi - .pi - 0.2 - colorstart) res.addPath(pth, transform: transform) arcline.path = res arcline.lineWidth = ptsize arcline.lineCap = .round } } public override func layoutSublayers() { super.layoutSublayers() self.updateLayout() } } // MARK: Animation private extension DSFSparklineOverlay.WiperGauge { func animate(_ transition: DSFSparkline.AnimationTransition) { guard let anim = self.animationStyle else { return } // Stop any animation that is currently active self.animator.progressBlock = nil self.animator.stop() self.animator.animationFunction = anim.function.function self.animator.duration = anim.duration self.animator.progressBlock = { [weak self] progress in self?.currentValue__ = transition.start + (transition.distance * progress) } self.animator.start() } } ================================================ FILE: Sources/DSFSparkline/overlay/surfaces/DSFSparklineSurface+AttributedString.swift ================================================ // // DSFSparklineSurface+AttributedString.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // // NSAttributedString extensions for DSFSparklineSurface.Bitmap import CoreGraphics import Foundation public extension DSFSparklineSurface.Bitmap { /// Returns an NSAttributedString containing an image of the sparkline /// - Parameters: /// - size: The dimensions of the image /// - scale: The scale for the returned image. For example, for a retina scale (144dpi) image, scale == 2 /// - Returns: An NSAttributedString containing the sparkline bitmap, or nil if the bitmap couldn't be generated @objc(attributedString::) func attributedString(size: CGSize, scale: CGFloat = 1) -> NSAttributedString? { guard let attachment = self.textAttachment(size: size, scale: scale) else { return nil } return NSAttributedString(attachment: attachment) } } // MARK: - AppKit Additions #if os(macOS) import AppKit public extension DSFSparklineSurface.Bitmap { /// Returns a TextAttachment containing an image of the sparkline /// - Parameters: /// - size: The dimensions of the image /// - scale: The scale for the returned image. For example, for a retina scale (144dpi) image, scale == 2 /// - Returns: An NSTextAttachment containing the sparkline bitmap, or nil if the bitmap couldn't be generated @objc(textAttachment::) func textAttachment(size: CGSize, scale: CGFloat = 1) -> NSTextAttachment? { guard let image = self.image(size: size, scale: scale) else { return nil } let attachment = NSTextAttachment() let flipped = NSImage(size: size, flipped: false, drawingHandler: { (rect: NSRect) -> Bool in image.draw(in: rect) return true }) attachment.image = flipped return attachment } } #else // MARK: - UIKit Additions import UIKit public extension DSFSparklineSurface.Bitmap { /// Returns an NSTextAttachment containing an image of the sparkline /// - Parameters: /// - size: The dimensions of the image /// - scale: The scale for the returned image. For example, for a retina scale (144dpi) image, scale == 2 /// - Returns: An NSTextAttachment containing the sparkline bitmap, or nil if the bitmap couldn't be generated @objc(textAttachment::) func textAttachment(size: CGSize, scale: CGFloat = 1) -> NSTextAttachment? { guard let image = self.image(size: size, scale: scale) else { return nil } let attachment = NSTextAttachment() attachment.image = image attachment.bounds = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height) return attachment } } #endif ================================================ FILE: Sources/DSFSparkline/overlay/surfaces/DSFSparklineSurface+Bitmap.swift ================================================ // // DSFSparklineSurface+Bitmap.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import CoreGraphics import Foundation public extension DSFSparklineSurface { /// A surface for drawing a sparkline into an image @objc(DSFSparklineSurfaceBitmap) class Bitmap: DSFSparklineSurface { private func edgeInsets(for rect: CGRect) -> DSFEdgeInsets { /// Calculate the total inset required return self.overlays.reduce(DSFEdgeInsets.zero) { (result, overlay) in result.combineMaximum(using: overlay.edgeInsets(for: rect)) } } /// Add a sparkline overlay to the surface @objc public func addOverlay(_ overlay: DSFSparklineOverlay) { self.overlays.append(overlay) } /// Return a CGImage representation of the sparklline /// - Parameters: /// - size: The dimension in pixels /// - scale: The scale to use (eg. retina == 2) /// - Returns: A CGImage representation, or nil if the image couldn't be generated @objc public func cgImage(size: CGSize, scale: CGFloat = 2) -> CGImage? { let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) // Create the bitmap context to draw into guard let bitmapContext = self.generateBitmapContext(rect: rect, scale: scale) else { return nil } self.overlays.forEach { $0.frame = CGRect(origin: .zero, size: size) } // Calculate the inset required let bounds = rect.inset(by: self.edgeInsets(for: rect)) // Loop through each overlay and ask it to draw self.overlays.forEach { overlay in bitmapContext.usingGState { ctx in overlay.frame = rect overlay.setNeedsDisplay() overlay.contentsScale = scale #if os(macOS) overlay.isGeometryFlipped = true #endif // Render the layer content overlay.render(in: ctx) // Render any bitmap content overlay.drawGraph(context: ctx, bounds: bounds, scale: scale) } } return bitmapContext.makeImage() } // MARK: Private // The overlays to use when generating the image private var overlays: [DSFSparklineOverlay] = [] } } // MARK: - AppKit Additions #if os(macOS) import AppKit public extension DSFSparklineSurface.Bitmap { /// Generate an NSImage with the contents of the surface /// - Parameters: /// - size: The dimensions of the image /// - scale: The scale for the returned image. For example, for a retina scale (144dpi) image, scale == 2 /// - Returns: The created image, or nil if something went wrong @objc func image(size: CGSize, scale: CGFloat = 1) -> NSImage? { guard let cgImage = self.cgImage(size: size, scale: scale) else { return nil } return NSImage(cgImage: cgImage, size: size) } /// Generate an NSImage with the contents of the surface /// - Parameters: /// - width: The width of the resultant image /// - height: The height of the resultant image /// - scale: The scale for the returned image. For example, for a retina scale (144dpi) image, scale == 2 /// - Returns: The created image, or nil if something went wrong @objc func image(width: CGFloat, height: CGFloat, scale: CGFloat = 1) -> NSImage? { let size = CGSize(width: width, height: height) guard let cgImage = self.cgImage(size: size, scale: scale) else { return nil } return NSImage(cgImage: cgImage, size: size) } } #else // MARK: - UIKit Additions import UIKit public extension DSFSparklineSurface.Bitmap { /// Generate an NSImage with the contents of the surface /// - Parameters: /// - size: The dimensions of the image /// - scale: The scale for the returned image. For example, for a retina scale (144dpi) image, scale == 2 /// - Returns: The created image, or nil if something went wrong @objc func image(size: CGSize, scale: CGFloat = 1) -> UIImage? { guard let cgImage = self.cgImage(size: size, scale: scale) else { return nil } return UIImage( cgImage: cgImage, scale: scale, orientation: UIImage.Orientation.up ) } /// Generate an NSImage with the contents of the surface /// - Parameters: /// - width: The width of the resultant image /// - height: The height of the resultant image /// - scale: The scale for the returned image. For example, for a retina scale (144dpi) image, scale == 2 /// - Returns: The created image, or nil if something went wrong @objc func image(width: CGFloat, height: CGFloat, scale: CGFloat = 1) -> UIImage? { guard let cgImage = self.cgImage(size: CGSize(width: width, height: height), scale: scale) else { return nil } return UIImage( cgImage: cgImage, scale: scale, orientation: UIImage.Orientation.up ) } } #endif // MARK: - Private private extension DSFSparklineSurface.Bitmap { // Generate a bitmap context for the specified rect and scale func generateBitmapContext(rect: CGRect, scale: CGFloat) -> CGContext? { let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) guard let bitmapContext = CGContext( data: nil, width: Int(rect.width * scale), height: Int(rect.height * scale), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue ) else { Swift.print("(ERROR) DSFSparklineBitmap unable to generate bitmap context for drawing") return nil } // Need to flip bitmapContext.scaleBy(x: scale, y: -scale) bitmapContext.translateBy(x: 0, y: -rect.height) return bitmapContext } } ================================================ FILE: Sources/DSFSparkline/overlay/surfaces/DSFSparklineSurface+SwiftUI.swift ================================================ // // DSFSparklineSurface+SwiftUI.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import SwiftUI public extension DSFSparklineSurface { /// A surface for creating a sparkline using overlays struct SwiftUI { let overlays: [DSFSparklineOverlay] public init(_ overlays: [DSFSparklineOverlay]) { self.overlays = overlays } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineSurface.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineSurfaceView #else public typealias UIViewType = DSFSparklineSurfaceView #endif public class Coordinator: NSObject { let parent: DSFSparklineSurface.SwiftUI init(_ sparkline: DSFSparklineSurface.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } private func makeSurface(_ context: Context) -> DSFSparklineSurfaceView { let view = DSFSparklineSurfaceView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false let base = context.coordinator.parent base.overlays.forEach { view.addOverlay($0) } return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineSurface.SwiftUI { func makeUIView(context: Context) -> DSFSparklineSurfaceView { return self.makeSurface(context) } func updateUIView(_ view: DSFSparklineSurfaceView, context: Context) { self.updateView(view, context: context) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineSurface.SwiftUI { func makeNSView(context: Context) -> DSFSparklineSurfaceView { return self.makeSurface(context) } func updateNSView(_ nsView: DSFSparklineSurfaceView, context: Context) { self.updateView(nsView, context: context) } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineSurface.SwiftUI { func updateView(_ view: DSFSparklineSurfaceView, context: Context) { } } ================================================ FILE: Sources/DSFSparkline/overlay/surfaces/DSFSparklineSurface+View.swift ================================================ // // DSFSparklineSurface+View.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if os(macOS) import Cocoa #else import UIKit #endif /// A surface for drawing a sparkline into a view. /// /// Represents the generic base class for a view. @objc public class DSFSparklineSurfaceView: DSFView { #if os(macOS) override public var isFlipped: Bool { return true } #endif // Render delegate instance lazy private var renderDelegate: RendererDelegate = { return RendererDelegate(view: self) }() var rootLayer: CALayer { #if os(macOS) return self.layer! #else return self.layer #endif } deinit {} override public init(frame: CGRect) { super.init(frame: frame) self.setup() } public required init?(coder: NSCoder) { super.init(coder: coder) self.setup() } private func setup() { #if os(macOS) self.wantsLayer = true #else // Configure iOS/tvOS to make the background transparent. // If isOpaque is true (the default value) iOS assumes that you're drawing // the ENTIRE content of the control (which we are not). self.isOpaque = false #endif } /// Multi-platform function for telling the view to update itself public func updateDisplay() { #if os(macOS) self.needsDisplay = true #else self.setNeedsDisplay() #endif } } extension DSFSparklineSurfaceView { var overlays: [DSFSparklineOverlay] { return self.rootLayer.sublayers?.compactMap { $0 as? DSFSparklineOverlay } ?? [] } /// Add a sparkline overlay to the view public func addOverlay(_ overlay: DSFSparklineOverlay) { self.rootLayer.addSublayer(overlay) overlay.bounds = self.bounds overlay.delegate = self.renderDelegate self.syncLayers() overlay.setNeedsLayout() overlay.setNeedsDisplay() } /// Remove a sparkline overlay to the view public func removeOverlay(_ overlay: DSFSparklineOverlay) { overlay.removeFromSuperlayer() } func edgeInsets(for rect: CGRect) -> DSFEdgeInsets { /// Calculate the total inset required return self.overlays.reduce(DSFEdgeInsets.zero) { (result, overlay) in result.combineMaximum(using: overlay.edgeInsets(for: rect)) } } private func syncLayers() { CATransaction.withDisabledActions { self.rootLayer.sublayers?.forEach { layer in layer.bounds = self.bounds layer.contentsScale = self.retinaScale() layer.setNeedsDisplay() } } } #if os(macOS) public override func layout() { super.layout() self.syncLayers() } public override func viewDidMoveToWindow() { super.viewDidMoveToWindow() self.syncLayers() } #else public override func layoutSubviews() { super.layoutSubviews() self.syncLayers() } public override func didMoveToWindow() { super.didMoveToWindow() self.syncLayers() } #endif } // the draw delegate for the overlay layers fileprivate class RendererDelegate: NSObject, CALayerDelegate { let view: DSFSparklineSurfaceView init(view: DSFSparklineSurfaceView) { self.view = view super.init() } func draw(_ layer: CALayer, in ctx: CGContext) { if let l = layer as? DSFSparklineOverlay { let scale = view.retinaScale() l.contentsScale = scale let insetBounds = view.edgeInsets(for: view.bounds) l.drawGraph(context: ctx, bounds: view.bounds.inset(by: insetBounds), scale: scale) } } } ================================================ FILE: Sources/DSFSparkline/overlay/surfaces/DSFSparklineSurface.swift ================================================ // // DSFSparklineSurface.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation @objc public class DSFSparklineSurface: NSObject { } ================================================ FILE: Sources/DSFSparkline/overlay/types/DSFSparkline+ActivityGridDefinition.swift ================================================ // // DSFSparkline+ActivityGridDefinition.swift // // Created by Darren Ford on 26/2/21. // Copyright © 2023 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparkline { class ActivityGrid { private init() { } } } public extension DSFSparkline.ActivityGrid { /// The drawing style for the activity grid @objc(DSFSparklineActivityGridLayoutStyle) enum LayoutStyle: Int { /// 'Newest' value at bottom right, works up then left case github = 0 /// 'Newest' value at top left, works right then down case defrag = 1 } } public extension DSFSparkline.ActivityGrid { /// The style for drawing cells in the activity grid @objc(DSFSparklineActivityGridCellStyle) class CellStyle: NSObject { @objc public let fillScheme: DSFSparkline.ValueBasedFill @objc public let borderColor: CGColor? @objc public let borderWidth: CGFloat @objc public let cellDimension: CGFloat @objc public let cellSpacing: CGFloat @objc public let cornerRadius: CGFloat @objc public init( fillScheme: DSFSparkline.ValueBasedFill, borderColor: CGColor? = nil, borderWidth: CGFloat = 1.0, cellDimension: CGFloat = 11.0, cellSpacing: CGFloat = 2.5, cornerRadius: CGFloat = 2.5 ) { self.fillScheme = fillScheme self.borderColor = borderColor self.borderWidth = borderWidth self.cellDimension = cellDimension self.cellSpacing = cellSpacing self.cornerRadius = cornerRadius super.init() } /// Default style - dark github @objc public convenience override init() { self.init(fillScheme: CellStyle.DefaultDark) } /// Return a copy of this cell style changing the specified attribute values public func modify( fillScheme: DSFSparkline.ValueBasedFill? = nil, borderColor: CGColor? = nil, borderWidth: CGFloat? = nil, cellDimension: CGFloat? = nil, cellSpacing: CGFloat? = nil, cornerRadius: CGFloat? = nil ) -> CellStyle { let fs = fillScheme ?? self.fillScheme let bc = borderColor ?? self.borderColor let bw = borderWidth ?? self.borderWidth let cd = cellDimension ?? self.cellDimension let cs = cellSpacing ?? self.cellSpacing let cr = cornerRadius ?? self.cornerRadius return CellStyle( fillScheme: fs, borderColor: bc, borderWidth: bw, cellDimension: cd, cellSpacing: cs, cornerRadius: cr ) } /// A default palette used when no palette is specified. public static let DefaultLight = DSFSparkline.ValueBasedFill(colors: [ DSFColor(red: 0.820, green: 0.830, blue: 0.842, alpha: 1.000), DSFColor(red: 0.606, green: 0.914, blue: 0.657, alpha: 1.000), DSFColor(red: 0.248, green: 0.768, blue: 0.387, alpha: 1.000), DSFColor(red: 0.190, green: 0.633, blue: 0.306, alpha: 1.000), DSFColor(red: 0.132, green: 0.432, blue: 0.222, alpha: 1.000), ]) public static let DefaultDark = DSFSparkline.ValueBasedFill(colors: [ DSFColor(red: 0.086, green: 0.106, blue: 0.132, alpha: 1.000), DSFColor(red: 0.055, green: 0.269, blue: 0.159, alpha: 1.000), DSFColor(red: 0.000, green: 0.429, blue: 0.194, alpha: 1.000), DSFColor(red: 0.148, green: 0.649, blue: 0.257, alpha: 1.000), DSFColor(red: 0.219, green: 0.829, blue: 0.323, alpha: 1.000), ]) } } ================================================ FILE: Sources/DSFSparkline/overlay/types/DSFSparkline+GradientBucket.swift ================================================ // // DSFSparkline+GradientBucket.swift // DSFSparklines // // Created by Darren Ford on 15/2/2021. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import CoreGraphics import Foundation public extension DSFSparkline { /// A class that represents buckets of color within a gradient within the RGB colorspace. /// /// Defines a smooth transition between colors. /// /// **Buckets** /// A gradient object can also be 'bucketed', so that rather than a smooth transition the gradient output is /// broken up into equal buckets containing a color @objc(DSFGradientBucket) class GradientBucket: NSObject { static let rgbSpace = CGColorSpaceCreateDeviceRGB() static let EmptyColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 0])! /// The number of buckets to create. If 0 or 1, the gradient is smooth. @objc public var bucketCount: UInt = 0 { didSet { self.buildBuckets() } } private func sorted(_ posts: [Post]) -> [Post] { return posts.sorted(by: { (a, b) -> Bool in a.location < b.location }) } // The posts sorted in order of their location 0.0 -> 1.0 private var sortedPosts: [Post] = [] // The color buckets private var buckets: [Bucket] = [] /// Create a gradient /// - Parameter posts: The color 'posts' within the gradient @objc public init(posts: [Post], bucketCount: UInt = 0) { self.bucketCount = bucketCount super.init() self.sortedPosts = self.sorted(posts) self.buildBuckets() } /// Create a gradient /// - Parameter colors: The colors to use evenly across the gradient fill /// - Parameter bucketCount: The number of color buckets to create public init(colors: [CGColor], bucketCount: UInt = 0) { assert(colors.count > 1) self.bucketCount = bucketCount super.init() var offset: CGFloat = 0.0 let diff = 1.0 / CGFloat(colors.count - 1) var posts: [Post] = [] colors.forEach { color in let clamped = offset.clamped(to: 0.0 ... 1.0) let post = DSFSparkline.GradientBucket.Post(color: color, location: clamped) posts.append(post) offset += diff } self.sortedPosts = self.sorted(posts) self.buildBuckets() } /// Make a copy of the gradient bucket @objc public func copyGradientBucket() -> GradientBucket { GradientBucket( posts: self.sortedPosts.map { $0.copyPost() }, bucketCount: self.bucketCount ) } // MARK: - Buckets private func buildBuckets() { // // |-----|-----|-----|-----| // | | | | | // | | | | | // |-----|-----|-----|-----| // // // If the bucket count is 4, we want the FIRST bucket to be equal // to the first color in the gradient, and the LAST bucket to be // equal to the last color in the gradient. // Which means that the colors for bucket 2 and 3 are represented // by the 1/3 and 2/3 colors to get an even spread across the buckets // self.buckets = [] guard self.bucketCount > 1 else { return } if self.bucketCount == 2 { self.buckets = [ Bucket(range: 0.0 ..< 0.5, color: self.color(at: 0)), Bucket(range: 0.5 ..< .infinity, color: self.color(at: 1)), ] return } // The diff for the buckets let bucketDiff: CGFloat = 1.0 / CGFloat(self.bucketCount) // The diff for the colors let colordiff: CGFloat = 1.0 / CGFloat(self.bucketCount - 1) // Add in the first bucket. We want the first bucket to be the first color in the gradient let first = Bucket(range: -.infinity ..< bucketDiff, color: self.color(at: 0)) var bkts = [first] // The offset within the middle buckets var offset: CGFloat = 1 var rangeOffset = offset * bucketDiff // We only need to fill the center buckets because we do first and last manually (1 ..< self.bucketCount - 1).forEach { index in let colorIndex = colordiff * CGFloat(index) let color = self.color(at: colorIndex) // let rangeOffset = offset * bucketDiff let bucket = Bucket(range: rangeOffset ..< rangeOffset + bucketDiff, color: color) bkts.append(bucket) // Step to the next middle bucket offset += 1 rangeOffset = rangeOffset + bucketDiff } // Add in the last bucket. We want the last bucket to be the last color in the gradient let last = Bucket(range: (1 - bucketDiff) ..< .infinity, color: self.color(at: 1)) bkts.append(last) self.buckets = bkts } // MARK: - Colors // Returns the color at the specified fractional value func color(at fraction: CGFloat) -> CGColor { if self.sortedPosts.count == 0 { return Self.EmptyColor } if self.sortedPosts.count == 1 { // Just the first color return self.sortedPosts[0].color.c } if fraction <= 0 { // Just the first color return self.sortedPosts.first!.color.c } if fraction >= 1 { // Just the last color return self.sortedPosts.last!.color.c } if self.buckets.isEmpty { return self.gradientColor(at: fraction) } else { return self.bucketColor(at: fraction) } } private func gradientColor(at fraction: CGFloat) -> CGColor { var location: Int? for index in 0 ..< self.sortedPosts.count - 1 { let range = self.sortedPosts[index].location ..< self.sortedPosts[index + 1].location if range.contains(fraction) { location = index break } } guard let loc = location else { return self.sortedPosts.last!.color.c } let delta = self.sortedPosts[loc + 1].location - self.sortedPosts[loc].location let divisor = (fraction - self.sortedPosts[loc].location) / delta let c1 = self.sortedPosts[loc].color let r1 = c1.r let g1 = c1.g let b1 = c1.b let a1 = c1.a let c2 = self.sortedPosts[loc + 1].color let r2 = c2.r let g2 = c2.g let b2 = c2.b let a2 = c2.a let newR = r1 + ((r2 - r1) * divisor) let newG = g1 + ((g2 - g1) * divisor) let newB = b1 + ((b2 - b1) * divisor) let newA = a1 + ((a2 - a1) * divisor) return CGColor(colorSpace: Self.rgbSpace, components: [newR, newG, newB, newA])! } private func bucketColor(at fraction: CGFloat) -> CGColor { /// Map the fraction to the bucket ranges if let whichColor = self.buckets.first(where: { bucket in bucket.range.contains(fraction) })?.color { return whichColor } return Self.EmptyColor } } } public extension DSFSparkline.GradientBucket { /// A gradient 'post' represents an absolute color at a fractional point within the gradient. @objc(DSFGradientBucketPost) class Post: NSObject { /// The color at the location let color: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat, c: CGColor) /// The fractional (0 -> 1) location within the gradient for a color. Values outside this range are clamped let location: CGFloat private static let rgbSpace = CGColorSpaceCreateDeviceRGB() /// Create a post /// - Parameters: /// - color: the color for the post /// - location: the location for the color within the gradient (0.0 -> 1.0) @objc public init(color: CGColor, location: CGFloat) { self.location = max(0, min(location, 1)) let rgbColor = color.converted(to: Self.rgbSpace, intent: .perceptual, options: nil)! assert(rgbColor.numberOfComponents == 4) let r1 = rgbColor.components![0] let g1 = rgbColor.components![1] let b1 = rgbColor.components![2] let a1 = rgbColor.components![3] self.color = (r1, g1, b1, a1, rgbColor) super.init() } /// Make a copy of this gradient bucket post @objc public func copyPost() -> Post { return Post( r: self.color.r, g: self.color.g, b: self.color.b, a: self.color.a, location: self.location ) } /// Create a post /// - Parameters: /// - r: the red component (0.0 -> 1.0) /// - g: the green component (0.0 -> 1.0) /// - b: the blue component (0.0 -> 1.0) /// - location: the location for the color within the gradient (0.0 -> 1.0) @objc public init(r: CGFloat, g: CGFloat, b: CGFloat, location: CGFloat) { self.location = max(0, min(location, 1)) self.color = (r, g, b, 1.0, CGColor(colorSpace: Self.rgbSpace, components: [r, g, b, 1.0])!) super.init() } /// Create a post /// - Parameters: /// - r: the red component (0.0 -> 1.0) /// - g: the green component (0.0 -> 1.0) /// - b: the blue component (0.0 -> 1.0) /// - a: the alpha component (0.0 -> 1.0) /// - location: the location for the color within the gradient (0.0 -> 1.0) @objc public init(r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat, location: CGFloat) { self.location = max(0, min(location, 1)) self.color = (r, g, b, a, CGColor(colorSpace: Self.rgbSpace, components: [r, g, b, a])!) super.init() } } } private extension DSFSparkline.GradientBucket { /// Color bucket definition struct Bucket: CustomDebugStringConvertible { let range: Range let color: CGColor var debugDescription: String { return "color: \(self.color), range: \(self.range)" } } } ================================================ FILE: Sources/DSFSparkline/overlay/types/DSFSparkline+GridLinesDefinition.swift ================================================ // // Created by Darren Ford on 25/01/21. // Copyright © 2023 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparkline { /// Grid lines definition @objc(DSFSparklineGridLinesDefinition) class GridLinesDefinition: NSObject { /// Grid lines color @objc public let color: DSFColor /// Grid lines width @objc public let width: CGFloat /// The dash style to use when drawing @objc public let dashStyle: [CGFloat] /// The positions to draw the gridlines for the data source @objc public let values: [CGFloat] /// Create a grid lines definition /// - Parameters: /// - color: The color of the grid lines /// - width: The width to draw the grid lines /// - dashStyle: The dash style for the grid lines /// - values: The positions to draw the gridlines for the data source @objc public init( color: DSFColor = .init(white: 0.5, alpha: 0.5), width: CGFloat = 1.0, dashStyle: [CGFloat] = [1.0, 1.0], values: [CGFloat] = [] ) { self.color = color self.width = width self.dashStyle = dashStyle self.values = values } } } ================================================ FILE: Sources/DSFSparkline/overlay/types/DSFSparkline+HighlightRangeDefinition.swift ================================================ // // DSFSparkline+HighlightRangeDefinition.swift // DSFSparklines // // Created by Darren Ford on 25/01/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparkline { /// A highlight range definition @objc(DSFSparklineHighlightRangeDefinition) class HighlightRangeDefinition: NSObject { public static let DefaultFill = DSFSparkline.Fill.Color(DSFColor.systemGray.cgColor) /// The range in the sparkline to highlight public var range: Range /// The highlight fill to use @objc public var fill: DSFSparklineFillable public init(range: Range, fill: DSFSparklineFillable = DefaultFill) { self.range = range self.fill = fill super.init() } public init(range: Range, fillColor: CGColor) { self.range = range self.fill = DSFSparkline.Fill.Color(fillColor) super.init() } /// Objective-C compatible initializer. Lowerbound MUST be less than upperbound! @objc public init(lowerBound: CGFloat, upperBound: CGFloat, fill: DSFSparklineFillable = DefaultFill) { assert(lowerBound < upperBound) self.range = lowerBound ..< upperBound self.fill = fill super.init() } /// Objective-C compatible initializer. Lowerbound MUST be less than upperbound! @objc public init(lowerBound: CGFloat, upperBound: CGFloat, fillColor: CGColor) { assert(lowerBound < upperBound) self.range = lowerBound ..< upperBound self.fill = DSFSparkline.Fill.Color(fillColor) super.init() } @objc public func copyHighlightRangeDefinition() -> HighlightRangeDefinition { HighlightRangeDefinition( lowerBound: self.range.lowerBound, upperBound: self.range.upperBound, fill: self.fill) } } } // MARK: - Objective-C helpers public extension DSFSparkline.HighlightRangeDefinition { @objc var lowerBound: CGFloat { get { return self.range.lowerBound } set { self.range = newValue ..< self.range.upperBound } } @objc var upperBound: CGFloat { get { return self.range.upperBound } set { self.range = self.range.lowerBound ..< newValue } } } ================================================ FILE: Sources/DSFSparkline/overlay/types/DSFSparkline+Palette.swift ================================================ // // DSFSparkline+Palette.swift // DSFSparklines // // Created by Darren Ford on 12/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparkline { /// The palette to use when drawing the pie. The first value in the datasource uses the first color, /// the second the second color etc. If there are more datapoints than colors (you shouldn't do this!) then /// the chart will start back at the start of the palette. /// /// These palettes can be safely shared between multiple pie views @objc(DSFSparklinePalette) class Palette: NSObject { /// The colors to be used when drawing segments @objc public let colors: [DSFColor] @objc public let cgColors: NSArray /// A default palette used when no palette is specified. @objc public static let shared = DSFSparkline.Palette([ DSFColor.systemRed, DSFColor.systemOrange, DSFColor.systemYellow, DSFColor.systemGreen, DSFColor.systemBlue, DSFColor.systemPurple, DSFColor.systemPink, ]) /// A default palette used when no palette is specified @objc public static let sharedGrays = DSFSparkline.Palette([ DSFColor(white: 0.9, alpha: 1), DSFColor(white: 0.7, alpha: 1), DSFColor(white: 0.5, alpha: 1), DSFColor(white: 0.3, alpha: 1), DSFColor(white: 0.1, alpha: 1), ]) @objc public init(_ colors: [DSFColor]) { self.colors = colors self.cgColors = NSArray(array: colors.map { $0.cgColor }) super.init() } @objc public func copyPalette() -> Palette { let copied = self.colors.compactMap { $0.copy() as? DSFColor ?? .black } return Palette(copied) } @inlinable @objc func colorAtOffset(_ offset: Int) -> DSFColor { return self.colors[offset % self.colors.count] } @inlinable @objc func cgColorAtOffset(_ offset: Int) -> CGColor { return self.cgColors[offset % self.colors.count] as! CGColor } } } ================================================ FILE: Sources/DSFSparkline/overlay/types/DSFSparkline+ValueBasedFill.swift ================================================ // // DSFSparkline+FillColor.swift // // Copyright © 2023 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import QuartzCore #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparkline { /// A fill color that can change depending on a value @objc(DSFSparklineValueBasedFill) class ValueBasedFill: NSObject { /// Is this a simple flat color? @objc public var isFlatColor: Bool { self.flatColor != nil } /// Is this a color palette? @objc public var isPalette: Bool { self.palette != nil } /// Is this a gradient? @objc public var isGradient: Bool { self.gradient != nil } @objc public static let sharedPalette = ValueBasedFill(palette: DSFSparkline.Palette.shared) private var flatColor: CGColor? = nil private var palette: DSFSparkline.Palette? = nil private var gradient: DSFSparkline.GradientBucket? = nil @objc public init(flatColor: CGColor) { self.flatColor = flatColor } @objc public init(palette: DSFSparkline.Palette) { self.palette = palette super.init() } /// Create a fill object containing an array of colors /// - Parameter colors: The colors @objc public convenience init(colors: [DSFColor]) { self.init(palette: DSFSparkline.Palette(colors)) } @objc public init(gradient: DSFSparkline.GradientBucket) { self.gradient = gradient } init(flatColor: CGColor?, palette: DSFSparkline.Palette?, gradient: DSFSparkline.GradientBucket?) { self.flatColor = flatColor?.copy() self.palette = palette?.copyPalette() self.gradient = gradient?.copyGradientBucket() } func copyColorContainer() -> ValueBasedFill { return .init(flatColor: self.flatColor, palette: self.palette, gradient: self.gradient) } func color(atFraction fraction: CGFloat) -> CGColor { let f = max(0, min(1 - CGFloat.ulpOfOne, fraction)) if let flatColor = self.flatColor { return flatColor } else if let gradient = self.gradient { return gradient.color(at: f) } else if let palette = self.palette { let div = 1.0 / CGFloat(palette.colors.count) let indexed = Int((f / div).rounded(.towardZero)) return palette.cgColorAtOffset(indexed) } return CGColor(red: 1, green: 0, blue: 0, alpha: 1) } } } ================================================ FILE: Sources/DSFSparkline/overlay/types/DSFSparkline+ZeroLineDefinition.swift ================================================ // // DSFSparkline+ZeroLineDefinition.swift // DSFSparklines // // Created by Darren Ford on 25/01/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparkline { /// Drawing definition for the zero-line operations @objc(DSFSparklineZeroLineDefinition) class ZeroLineDefinition: NSObject { #if os(macOS) public static let DefaultColor = DSFColor.disabledControlTextColor #else public static let DefaultColor = DSFColor.systemGray #endif /// A shared 'default' drawing pattern public static let shared = DSFSparkline.ZeroLineDefinition() /// The color to draw the zero line let color: DSFColor /// The width of the zero line let lineWidth: CGFloat /// The pattern for drawing the line let lineDashStyle: [CGFloat] /// Drawing definition for the zero-line for a graph /// - Parameters: /// - color: The color to draw the zero line /// - lineWidth: The width of the zero line /// - lineDashStyle: The pattern for drawing the line. An array of values that specify the lengths, in user space coordinates, of the painted and unpainted segments of the dash pattern. For example, the array [2,3] sets a dash pattern that alternates between a 2-unit-long painted segment and a 3-unit-long unpainted segment. The array [1,3,4,2] sets the pattern to a 1-unit painted segment, a 3-unit unpainted segment, a 4-unit painted segment, and a 2-unit unpainted segment. Pass an empty array to clear the dash pattern so that all stroke drawing in the context uses solid lines. public init(color: DSFColor = DefaultColor, lineWidth: CGFloat = 1.0, lineDashStyle: [CGFloat] = [1, 1]) { self.color = color self.lineWidth = lineWidth self.lineDashStyle = lineDashStyle } @objc public func copyZeroLineDefinition() -> ZeroLineDefinition { ZeroLineDefinition( color: self.color.copy() as? DSFColor ?? .black, lineWidth: self.lineWidth, lineDashStyle: self.lineDashStyle ) } } } ================================================ FILE: Sources/DSFSparkline/overlay/types/DSFSparkline.swift ================================================ // // DSFSparkline.swift // DSFSparklines // // Created by Darren Ford on 26/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if os(macOS) import AppKit #else import UIKit #endif @objc public class DSFSparkline: NSObject { } ================================================ FILE: Sources/DSFSparkline/overlay/types/fill/DSFSparkline+FillColor.swift ================================================ // // DSFSparkline+FillColor.swift // DSFSparklines // // Copyright © 2022 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import CoreGraphics import Foundation // MARK: - Solid color fill public extension DSFSparkline.Fill { /// The solid color fill @objc(DSFSparklineFillColor) class `Color`: NSObject, DSFSparklineFillable { /// Black color @objc public static var black: DSFSparkline.Fill.Color { .init(gray: 0) } /// White color @objc public static var white: DSFSparkline.Fill.Color { .init(gray: 1) } /// Clear color @objc public static var clear: DSFSparkline.Fill.Color { .init(gray: 0, alpha: 0) } /// The fill color @objc public var color: CGColor /// Create a color using a CGColor /// - Parameter color: The color @objc public init(_ color: CGColor) { self.color = color } /// Create a fill color using an sRGB color /// - Parameters: /// - red: red component /// - green: green component /// - blue: blue component /// - alpha: alpha component @objc public init(srgbRed red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1.0) { let cs = CGColorSpace(name: CGColorSpace.sRGB)! self.color = CGColor(colorSpace: cs, components: [red, green, blue, alpha]) ?? CGColor.black } /// Create a fill color using an rgb color /// - Parameters: /// - red: red component /// - green: green component /// - blue: blue component /// - alpha: alpha component @objc public init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1.0) { self.color = CGColor(red: red, green: green, blue: blue, alpha: alpha) } /// Create a fill color using an sRGB color /// - Parameters: /// - red: red component /// - green: green component /// - blue: blue component /// - alpha: alpha component @objc public init(gray: CGFloat, alpha: CGFloat = 1.0) { self.color = CGColor(gray: gray, alpha: alpha) } public func fill(context: CGContext, bounds: CGRect) { context.setFillColor(color) context.fill(bounds) } @objc public func copyFill() -> DSFSparklineFillable { return Color(self.color.copy() ?? .black) } @objc public func color(at fractionalValue: CGFloat) -> CGColor { self.color } } } ================================================ FILE: Sources/DSFSparkline/overlay/types/fill/DSFSparkline+FillGradient.swift ================================================ // // DSFSparkline+FillGradient.swift // DSFSparklines // // Copyright © 2022 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import CoreGraphics import Foundation // MARK: - Gradient fill public extension DSFSparkline.Fill { /// The gradient fill (vertical only at the moment) @objc(DSFSparklineFillGradient) class `Gradient`: NSObject, DSFSparklineFillable { /// The gradient to use when filling @objc public var gradient: CGGradient /// Is the gradient horizontal or vertical @objc public var isHorizontal: Bool /// A workaround for retrieving a color at a fractional location within a CGGradient private lazy var peek: GradientPeek = { GradientPeek(gradient: self.gradient) }() /// Create a fill gradient @objc public init(gradient: CGGradient, isHorizontal: Bool = false) { self.gradient = gradient self.isHorizontal = isHorizontal } /// Create a fill gradient public init(colors: [CGColor], isHorizontal: Bool = false) { assert(colors.count >= 2) self.isHorizontal = isHorizontal let count = colors.count let divisor = 1.0 / (CGFloat(count) - 1) let locations = (0 ..< count - 1).map { Double($0) * divisor }.appending(1) let gradient = CGGradient( colorsSpace: nil, colors: colors as CFArray, locations: locations) self.gradient = gradient! } /// Make a copy of a gradient @objc public func copyFill() -> DSFSparklineFillable { Gradient(gradient: self.gradient, isHorizontal: isHorizontal) } @objc public func fill(context: CGContext, bounds: CGRect) { if isHorizontal { context.drawLinearGradient( gradient, start: CGPoint(x: bounds.minX, y: bounds.maxY), end: CGPoint(x: bounds.maxX, y: bounds.maxY), options: [.drawsAfterEndLocation, .drawsBeforeStartLocation] ) } else { context.drawLinearGradient( gradient, start: CGPoint(x: 0.0, y: bounds.maxY), end: CGPoint(x: 0.0, y: bounds.minY), options: [.drawsAfterEndLocation, .drawsBeforeStartLocation] ) } } /// Return the color at a fractional (0 -> 1) position within the gradient @objc public func color(at fractionalValue: CGFloat) -> CGColor { self.peek.color(at: fractionalValue) } } } private extension DSFSparkline.Fill.Gradient { class GradientPeek { static let divisor: CGFloat = 1.0 / 255.0 let snapshotSize = 4096 let gradient: CGGradient private let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)! private lazy var bitmap: [UInt8] = { [UInt8](repeating: 0, count: (self.snapshotSize + 1) * 4) }() init(gradient: CGGradient) { self.gradient = gradient self.build() } func color(at fraction: CGFloat) -> CGColor { let fraction = fraction.clamped(to: 0 ... 1) let pixel = Int((CGFloat(self.snapshotSize) * fraction).rounded(.towardZero)) let offset = pixel * 4 let r = bitmap[offset + 0] let g = bitmap[offset + 1] let b = bitmap[offset + 2] let a = bitmap[offset + 3] return CGColor( colorSpace: self.colorSpace, components: [ CGFloat(r) * Self.divisor, CGFloat(g) * Self.divisor, CGFloat(b) * Self.divisor, CGFloat(a) * Self.divisor ] )! } private func build() { let sz = self.snapshotSize + 1 let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) guard let ctx = CGContext( data: &bitmap, width: sz, height: 1, bitsPerComponent: 8, bytesPerRow: sz * 4, space: self.colorSpace, bitmapInfo: bitmapInfo.rawValue ) else { fatalError() } ctx.drawLinearGradient( gradient, start: .init(x: 0, y: 0), end: .init(x: sz, y: 0), options: [] ) } } } ================================================ FILE: Sources/DSFSparkline/overlay/types/fill/DSFSparkline+Fillable.swift ================================================ // // DSFSparkline+Fillable.swift // DSFSparklines // // Copyright © 2022 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import CoreGraphics import Foundation /// A protocol definition for objects that can 'fill' a rectangle within a context with a color/gradient/pattern etc @objc public protocol DSFSparklineFillable: NSObjectProtocol { @objc func fill(context: CGContext, bounds: CGRect) @objc func copyFill() -> DSFSparklineFillable @objc func color(at fractionalValue: CGFloat) -> CGColor } public extension DSFSparkline { /// Defining a namespace for fillables class Fill { private init() {} } } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineActivityGridView+SwiftUI.swift ================================================ // // DSFSparklineActivityGridView+SwiftUI.swift // // Copyright © 2023 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import Foundation import SwiftUI #if os(macOS) import AppKit #else import UIKit #endif @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineActivityGridView { /// The SwiftUI percent bar graph struct SwiftUI { let values: [Double] let range: ClosedRange? let verticalCellCount: UInt? let horizontalCellCount: UInt let cellStyle: DSFSparkline.ActivityGrid.CellStyle let layoutStyle: DSFSparkline.ActivityGrid.LayoutStyle var _tooltipStringForCell: ((Int) -> String?)? /// Create an Activity Grid /// - Parameters: /// - values: The values to display /// - range: The allowable upper/lower bounds for the input values /// - verticalCellCount: The number of vertical cells /// - cellStyle: The stying to apply to each cell /// - layoutStyle: The style for drawing the activity grid public init( values: [Double], range: ClosedRange? = nil, verticalCellCount: UInt? = nil, horizontalCellCount: UInt = 0, cellStyle: DSFSparkline.ActivityGrid.CellStyle = .init(), layoutStyle: DSFSparkline.ActivityGrid.LayoutStyle = .github ) { self.values = values self.range = range self.verticalCellCount = verticalCellCount self.horizontalCellCount = horizontalCellCount self.cellStyle = cellStyle self.layoutStyle = layoutStyle } /// Create an Activity Grid /// - Parameters: /// - values: The values to display /// - range: The allowable upper/lower bounds for the input values /// - verticalCellCount: The number of vertical cells, or 0 to fill vertically /// - horizontalCellCount: The number of horizontal cells, or 0 to fill horizontally /// - layoutStyle: The style for drawing the activity grid /// - fillStyle: The fill mechanism /// - borderColor: Cell border color /// - borderWidth: Cell border width /// - cellDimension: Cell dimension (cells are always square) /// - cellSpacing: The spacing between cells /// - cornerRadius: Cell corner radius public init( values: [Double], range: ClosedRange? = nil, verticalCellCount: UInt? = nil, horizontalCellCount: UInt = 0, layoutStyle: DSFSparkline.ActivityGrid.LayoutStyle = .github, fillScheme: DSFSparkline.ValueBasedFill, borderColor: CGColor? = nil, borderWidth: CGFloat = 1, cellDimension: CGFloat = 11, cellSpacing: CGFloat = 2.5, cornerRadius: CGFloat = 2.5 ) { self.values = values self.range = range self.verticalCellCount = verticalCellCount self.horizontalCellCount = horizontalCellCount self.layoutStyle = layoutStyle self.cellStyle = DSFSparkline.ActivityGrid.CellStyle( fillScheme: fillScheme, borderColor: borderColor, borderWidth: borderWidth, cellDimension: cellDimension, cellSpacing: cellSpacing, cornerRadius: cornerRadius ) } /// A callback block for retrieving the tooltip text for a cell within the activity grid public func tooltipStringForCell(_ block: @escaping (Int) -> String?) -> Self { var copy = self copy._tooltipStringForCell = block return copy } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineActivityGridView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineActivityGridView #else public typealias UIViewType = DSFSparklineActivityGridView #endif public class Coordinator: NSObject { let parent: DSFSparklineActivityGridView.SwiftUI init(_ sparkline: DSFSparklineActivityGridView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } func makeActivityGrid(_ context: Context) -> DSFSparklineActivityGridView { let view = DSFSparklineActivityGridView(frame: .zero) self.updateView(view) view.cellTooltipString = self._tooltipStringForCell return view } #if os(macOS) @available(macOS 13.0, *) public func sizeThatFits(_ proposal: ProposedViewSize, nsView: DSFSparklineActivityGridView, context: Context) -> CGSize? { let w = (nsView.horizontalCellCount == 0) ? (proposal.width ?? nsView.activityLayer.intrinsicWidth) : nsView.activityLayer.intrinsicWidth return CGSize(width: w, height: nsView.intrinsicContentSize.height) } #else @available(iOS 16.0, tvOS 16.0, *) public func sizeThatFits(_ proposal: ProposedViewSize, uiView: DSFSparklineActivityGridView, context: Context) -> CGSize? { let w = (uiView.horizontalCellCount == 0) ? (proposal.width ?? uiView.activityLayer.intrinsicWidth) : uiView.activityLayer.intrinsicWidth return CGSize(width: w, height: uiView.intrinsicContentSize.height) } #endif } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineActivityGridView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineActivityGridView { return self.makeActivityGrid(context) } func updateUIView(_ view: DSFSparklineActivityGridView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineActivityGridView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineActivityGridView { let v = self.makeActivityGrid(context) return v } func updateNSView(_ view: DSFSparklineActivityGridView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineActivityGridView.SwiftUI { func updateView(_ view: DSFSparklineActivityGridView) { let v = self.values.map { CGFloat($0) } if let vh = self.verticalCellCount { view.verticalCellCount = vh } view.horizontalCellCount = self.horizontalCellCount if let range = self.range { view.setValues(v, range: CGFloat(range.lowerBound) ... CGFloat(range.upperBound)) } else { view.setValues(v) } view.cellStyle = self.cellStyle view.layoutStyle = self.layoutStyle } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineActivityGridView.swift ================================================ // // DSFSparklineActivityGridView.swift // // Copyright © 2023 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation #if os(macOS) import AppKit #else import UIKit #endif @objc public class DSFSparklineActivityGridView: DSFSparklineSurfaceView { /// The view's data source @objc public var dataSource: DSFSparkline.StaticDataSource { get { self.activityLayer.dataSource } set { self.activityLayer.dataSource = newValue self.setNeedsDisplay() } } /// Set the values for the data source /// - Parameters: /// - values: The values @objc public func setValues(_ values: [CGFloat]) { self.dataSource = DSFSparkline.StaticDataSource(values) } /// Set the values and supported range for the data source /// - Parameters: /// - values: The values /// - range: The acceptable range for the input data public func setValues(_ values: [CGFloat], range: ClosedRange) { self.dataSource = DSFSparkline.StaticDataSource(values, range: range) } /// Set the values and supported range for the data source /// - Parameters: /// - values: The values /// - lowerBound: The acceptable lower bounds for the input data /// - upperBound: The acceptable upper bounds for the input data @objc public func setValues(_ values: [CGFloat], lowerBound: CGFloat, upperBound: CGFloat) { self.activityLayer.dataSource = DSFSparkline.StaticDataSource( values, lowerBound: lowerBound, upperBound: upperBound ) } /// The layout style for the grid @objc public var cellStyle: DSFSparkline.ActivityGrid.CellStyle { get { self.activityLayer.cellStyle } set { self.activityLayer.cellStyle = newValue } } /// The layout style for the grid @objc public var layoutStyle: DSFSparkline.ActivityGrid.LayoutStyle { get { self.activityLayer.layoutStyle } set { self.activityLayer.layoutStyle = newValue } } /// The number of vertical cells in a column @objc public var verticalCellCount: UInt { get { UInt(self.activityLayer.verticalCellCount) } set { self.activityLayer.verticalCellCount = Int(newValue) } } /// The number of horizontal cells in a column. @objc public var horizontalCellCount: UInt { get { UInt(self.activityLayer.horizontalCellCount) } set { self.activityLayer.horizontalCellCount = Int(newValue) } } /// The color scheme to use when filling cells @objc public var cellFillScheme: DSFSparkline.ValueBasedFill { get { self.activityLayer.cellFillScheme } set { self.activityLayer.cellFillScheme = newValue } } /// The dimension of each cell @objc public var cellDimension: CGFloat { get { self.activityLayer.cellDimension } set { self.activityLayer.cellDimension = newValue } } /// The spacing between each of the cells @objc public var cellSpacing: CGFloat { get { self.activityLayer.cellSpacing } set { self.activityLayer.cellSpacing = newValue } } /// The border color for each individual cell @objc public var cellBorderColor: DSFColor? { get { if let c = self.activityLayer.borderColor { return DSFColor(cgColor: c) } return nil } set { self.activityLayer.cellBorderColor = newValue?.cgColor } } /// The cell's border width @objc public var cellBorderWidth: CGFloat { get { self.activityLayer.borderWidth } set { self.activityLayer.borderWidth = newValue } } /// The cell's corner radius @objc public var cellCornerRadius: CGFloat { get { self.activityLayer.cellCornerRadius } set { self.activityLayer.cellCornerRadius = newValue } } /// A block called to retrieve the tooltip text for a specific cell index @objc public var cellTooltipString: ((Int) -> String?)? { didSet { self.cellsDidUpdate() } } /// Initializer public override init(frame: CGRect) { super.init(frame: frame) self.configure() } /// Initializer public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } func configure() { self.addOverlay(self.activityLayer) self.activityLayer.cellsDidUpdateBlock = cellsDidUpdate #if os(macOS) self.updateTooltips() #endif } public override var intrinsicContentSize: CGSize { self.activityLayer.intrinsicSize } #if os(macOS) public override func layout() { super.layout() self.activityLayer.frame = self.bounds self.updateTooltips() } #else public override func layoutSubviews() { super.layoutSubviews() self.activityLayer.frame = self.bounds } #endif // private internal let activityLayer = DSFSparklineOverlay.ActivityGrid() #if os(macOS) private var tooltipTags: [NSView.ToolTipTag] = [] #endif } internal extension DSFSparklineActivityGridView { // Called when the activity grid layer updates func cellsDidUpdate() { #if os(macOS) if let _ = self.cellTooltipString, self.tooltipTags.count == 0 { self.updateTooltips() } #endif } } #if os(macOS) extension DSFSparklineActivityGridView: NSViewToolTipOwner { func updateTooltips() { guard let _ = self.cellTooltipString else { return } if !Thread.isMainThread { // Must be called on the main thread DispatchQueue.main.async { [weak self] in self?.updateTooltips() } return } self.tooltipTags.forEach { self.removeToolTip($0) } self.tooltipTags.removeAll() self.activityLayer.cells.enumerated().forEach { item in let t = self.addToolTip(item.element, owner: self, userData: nil) self.tooltipTags.append(t) } } public func view(_ view: NSView, stringForToolTip tag: NSView.ToolTipTag, point: NSPoint, userData data: UnsafeMutableRawPointer?) -> String { assert(Thread.isMainThread) guard let block = self.cellTooltipString, let index = self.tooltipTags.firstIndex(where: { $0 == tag }) else { return "" } return block(index) ?? "" } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineBarGraphView+SwiftUI.swift ================================================ // // DSFSparklineBarGraphView+SwiftUI.swift // DSFSparklines // // Created by Darren Ford on 7/12/20. // Copyright © 2020 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineBarGraphView { struct SwiftUI { /// Datasource for the graph let dataSource: DSFSparkline.DataSource /// The primary color for the sparkline let graphColor: DSFColor /// The line width (in pixels) to use when drawing the border of each bar let lineWidth: UInt /// The spacing (in pixels) between each bar let barSpacing: UInt /// Draw a dotted line at the zero point on the y-axis let showZeroLine: Bool /// The drawing definition for the zero line point let zeroLineDefinition: DSFSparkline.ZeroLineDefinition /// Should the line graph be centered around the zero-line? let centeredAtZeroLine: Bool /// The color used to draw values lower than the zero-line, or nil for the same as the graph color let lowerGraphColor: DSFColor? /// Highlight y-ranges within the graph let highlightDefinitions: [DSFSparkline.HighlightRangeDefinition] /// The grid lines definition let gridLines: DSFSparkline.GridLinesDefinition? /// Primary fill let primaryFill: DSFSparklineFillable? /// Secondary fill let secondaryFill: DSFSparklineFillable? /// Create a bar graph sparkline /// - Parameters: /// - dataSource: The data source for the graph /// - graphColor: The color to draw the graph /// - lineWidth: The width of the line around each bar /// - barSpacing: The spacing between the bars /// - showZeroLine: Show or hide a 'zero line' horizontal line /// - zeroLineDefinition: the settings for drawing the zero line /// - centeredAtZeroLine: Should the line graph be centered around the zero-line? /// - lowerGraphColor: The color used to draw values lower than the zero-line, or nil for the same as the graph color /// - highlightDefinitions: The style of the y-range highlight /// - gridLines: The grid lines to draw on the graph /// - primaryFill: The fill to use for values above the zero line /// - secondaryFill: The fill to use for values below the zero line public init( dataSource: DSFSparkline.DataSource, graphColor: DSFColor, lineWidth: UInt = 1, barSpacing: UInt = 1, showZeroLine: Bool = false, zeroLineDefinition: DSFSparkline.ZeroLineDefinition = .shared, centeredAtZeroLine: Bool = false, lowerGraphColor: DSFColor? = nil, showHighlightRange: Bool = false, highlightDefinitions: [DSFSparkline.HighlightRangeDefinition] = [], gridLines: DSFSparkline.GridLinesDefinition? = nil, primaryFill: (any DSFSparklineFillable)? = nil, secondaryFill: (any DSFSparklineFillable)? = nil ) { self.dataSource = dataSource self.graphColor = graphColor self.showZeroLine = showZeroLine self.zeroLineDefinition = zeroLineDefinition self.centeredAtZeroLine = centeredAtZeroLine self.lowerGraphColor = lowerGraphColor self.lineWidth = lineWidth self.barSpacing = barSpacing self.highlightDefinitions = highlightDefinitions self.gridLines = gridLines self.primaryFill = primaryFill self.secondaryFill = secondaryFill } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineBarGraphView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineBarGraphView #else public typealias UIViewType = DSFSparklineBarGraphView #endif public class Coordinator: NSObject { let parent: DSFSparklineBarGraphView.SwiftUI init(_ sparkline: DSFSparklineBarGraphView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } private func makeBarGraph(_: Context) -> DSFSparklineBarGraphView { let view = DSFSparklineBarGraphView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.dataSource = self.dataSource view.graphColor = self.graphColor view.barSpacing = self.barSpacing view.lineWidth = self.lineWidth view.zeroLineVisible = self.showZeroLine view.setZeroLineDefinition(self.zeroLineDefinition) view.centeredAtZeroLine = self.centeredAtZeroLine view.lowerGraphColor = self.lowerGraphColor if self.highlightDefinitions.count > 0 { view.highlightRangeVisible = true view.highlightRangeDefinition = self.highlightDefinitions } if let pf = self.primaryFill { view.primaryFill = pf } if let sf = self.secondaryFill { view.secondaryFill = sf } return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineBarGraphView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineBarGraphView { return self.makeBarGraph(context) } func updateUIView(_ view: DSFSparklineBarGraphView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineBarGraphView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineBarGraphView { return self.makeBarGraph(context) } func updateNSView(_ view: DSFSparklineBarGraphView, context _: Context) { self.updateView(view) } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineBarGraphView.SwiftUI { func updateView(_ view: DSFSparklineBarGraphView) { UpdateIfNotEqual(result: &view.graphColor, val: self.graphColor) UpdateIfNotEqual(result: &view.barSpacing, val: self.barSpacing) UpdateIfNotEqual(result: &view.lineWidth, val: self.lineWidth) UpdateIfNotEqual(result: &view.zeroLineVisible, val: self.showZeroLine) view.setZeroLineDefinition(self.zeroLineDefinition) UpdateIfNotEqual(result: &view.centeredAtZeroLine, val: self.centeredAtZeroLine) UpdateIfNotEqual(result: &view.lowerGraphColor, val: self.lowerGraphColor) if self.highlightDefinitions.count > 0 { view.highlightRangeVisible = true view.highlightRangeDefinition = self.highlightDefinitions } else { view.highlightRangeVisible = false view.highlightRangeDefinition = [] } if let gridLines = self.gridLines { view.setGridLineDefinition(gridLines) } else { view.gridLinesVisible = false } if let pf = self.primaryFill { view.primaryFill = pf } if let sf = self.secondaryFill { view.secondaryFill = sf } } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineBarGraphView.swift ================================================ // // DSFSparklineBarGraphView.swift // DSFSparklines // // Created by Darren Ford on 20/12/19. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if os(macOS) import AppKit #else import UIKit #endif /// A sparkline graph that displays bars @objc public class DSFSparklineBarGraphView: DSFSparklineZeroLineGraphView { let overlay = DSFSparklineOverlay.Bar() /// The line width (in pixels) to use when drawing the border of each bar @objc public dynamic var lineWidth: UInt = 1 { didSet { self.overlay.strokeWidth = self.lineWidth } } /// The spacing (in pixels) between each bar @objc public dynamic var barSpacing: UInt = 1 { didSet { self.overlay.barSpacing = self.barSpacing } } /// Draw a shadow under the line @objc public dynamic var shadowed: Bool = false { didSet { self.overlay.shadow = self.shadowed ? NSShadow.sparklineDefault : nil } } /// Should the graph be centered at the zero line? @objc public dynamic var centeredAtZeroLine: Bool = false { didSet { self.overlay.centeredAtZeroLine = self.centeredAtZeroLine } } /// The primary fill (for the area of the graph ABOVE the zero line) @objc public dynamic var primaryFill: (any DSFSparklineFillable)? { get { self.overlay.primaryFill } set { self.overlay.primaryFill = newValue } } /// The secondary fill (for the area of the graph UNDER the zero line) @objc public dynamic var secondaryFill: (any DSFSparklineFillable)? { get { self.overlay.secondaryFill } set { self.overlay.secondaryFill = newValue } } public override init(frame: CGRect) { super.init(frame: frame) self.configure() } public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } func configure() { self.addOverlay(self.overlay) self.overlay.setNeedsDisplay() } override func colorDidChange() { super.colorDidChange() self.overlay.strokeWidth = self.lineWidth self.overlay.barSpacing = self.barSpacing self.overlay.primaryStrokeColor = self.graphColor.cgColor self.overlay.secondaryStrokeColor = self.lowerColor.cgColor self.overlay.centeredAtZeroLine = self.centeredAtZeroLine // Backwards compatibility let color = self.graphColor let fill = DSFSparkline.Fill.Gradient(colors: [ color.withAlphaComponent(0.4).cgColor, color.withAlphaComponent(0.2).cgColor ]) self.overlay.primaryFill = fill if let lowerColor = self.lowerGraphColor { let fill = DSFSparkline.Fill.Gradient(colors: [ lowerColor.withAlphaComponent(0.4).cgColor, lowerColor.withAlphaComponent(0.2).cgColor ]) self.overlay.secondaryFill = fill } else { // Fallback - if secondary fill not defined the compatibility view is to use the primary fill view self.overlay.secondaryFill = self.overlay.primaryFill } } } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineCircularGaugeView+SwiftUI.swift ================================================ // // DSFSparklineCircularGaugeView+SwiftUI.swift // // Copyright © 2024 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineCircularGaugeView { enum Animation { case linear(Double) case easeInEaseOut(Double) } /// A SwiftUI wrapper for the Circular Progress sparkline overlay struct SwiftUI { let value: Double let animationStyle: DSFSparkline.AnimationStyle? let trackStyle: DSFSparklineOverlay.CircularGauge.TrackStyle let lineStyle: DSFSparklineOverlay.CircularGauge.TrackStyle /// Create a Circular Gauge view /// - Parameters: /// - value: The value to display /// - animationStyle: The animation style, or nil for no animation /// - trackStyle: The style to use when drawing the track (background ring) /// - lineStyle: The style to use when drawing the value line public init( value: Double, animationStyle: DSFSparkline.AnimationStyle? = nil, trackStyle: DSFSparklineOverlay.CircularGauge.TrackStyle, lineStyle: DSFSparklineOverlay.CircularGauge.TrackStyle ) { self.value = value self.animationStyle = animationStyle self.trackStyle = trackStyle self.lineStyle = lineStyle } /// Create a Circular Gauge view /// - Parameters: /// - value: The value to display /// - animationStyle: The animation style, or nil for no animation /// - trackWidth: The width of the track /// - trackFill: The fill style to use for the track /// - valueWidth: The width of the value line /// - valueFill: The fill style to use for the value line public init( value: Double, animationStyle: DSFSparkline.AnimationStyle? = nil, trackWidth: Double = 10, trackFill: DSFSparklineFillable, valueWidth: Double = 7, valueFill: DSFSparklineFillable ) { self.value = value self.animationStyle = animationStyle self.trackStyle = .init(width: trackWidth, fillColor: trackFill) self.lineStyle = .init(width: valueWidth, fillColor: valueFill) } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineCircularGaugeView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineCircularGaugeView #else public typealias UIViewType = DSFSparklineCircularGaugeView #endif func makeCircularGauge(_: Context) -> DSFSparklineCircularGaugeView { let view = DSFSparklineCircularGaugeView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.value = self.value view.animationStyle = self.animationStyle view.trackStyle = self.trackStyle view.lineStyle = self.lineStyle return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineCircularGaugeView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineCircularGaugeView { return self.makeCircularGauge(context) } func updateUIView(_ view: DSFSparklineCircularGaugeView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineCircularGaugeView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineCircularGaugeView { return self.makeCircularGauge(context) } func updateNSView(_ view: DSFSparklineCircularGaugeView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineCircularGaugeView.SwiftUI { func updateView(_ view: DSFSparklineCircularGaugeView) { UpdateIfNotEqual(result: &view.value, val: self.value) if view.animationStyle !== self.animationStyle { view.animationStyle = self.animationStyle } if view.trackStyle !== self.trackStyle { view.trackStyle = self.trackStyle } if view.lineStyle !== self.lineStyle { view.lineStyle = self.lineStyle } } } #if DEBUG @available(macOS 10.15, *) struct DSFSparklineCircularGaugeViewPreviews: PreviewProvider { static var previews: some SwiftUI.View { let ge = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 1, green: 0.000, blue: 0.00, alpha: 1.0), CGColor(srgbRed: 0, green: 0.0, blue: 1, alpha: 1.0), ] ) let vals = [Double](stride(from: 0, through: 1, by: 0.2)) VStack { HStack { ForEach(vals, id: \.self) { value in DSFSparklineCircularGaugeView.SwiftUI( value: value, trackStyle: .init(width: 20, fillColor: DSFSparkline.Fill.Color(gray: 0.5, alpha: 0.2)), lineStyle: .init(width: 10, fillColor: DSFSparkline.Fill.Color(gray: 0.5, alpha: 1.0)) ) .frame(width: 64, height: 64) } } Divider() ForEach([0, 1, 2], id: \.self) { isShadowed in HStack { ForEach(vals, id: \.self) { value in DSFSparklineCircularGaugeView.SwiftUI( value: value, trackStyle: .init(width: 20, fillColor: DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 1, alpha: 0.2), shadow: isShadowed == 1 ? .init(blurRadius: 2, offset: CGSize(width: 1, height: 1), color: .black) : nil), lineStyle: .init(width: 10, fillColor: DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 1, alpha: 1), shadow: isShadowed == 2 ? .init(blurRadius: 2, offset: CGSize(width: 1, height: 1), color: .black) : nil) ) .frame(width: 64, height: 64) } } } Divider() ForEach([0, 1, 2], id: \.self) { isShadowed in HStack { ForEach(vals, id: \.self) { value in DSFSparklineCircularGaugeView.SwiftUI( value: value, trackStyle: .init( width: 20, fillColor: DSFSparkline.Fill.Color(gray: 0.5, alpha: 0.2), shadow: isShadowed == 1 ? .init(blurRadius: 1, offset: CGSize(width: 1, height: 1), color: .black.copy(alpha: 0.8)!, isInner: true) : nil ), lineStyle: .init( width: 10, fillColor: ge, strokeWidth: 0.2, strokeColor: CGColor.standard.black, shadow: isShadowed == 2 ? .init(blurRadius: 1, offset: CGSize(width: 1, height: 1), color: .black.copy(alpha: 0.8)!, isInner: true) : nil ) ) .frame(width: 64, height: 64) } } } } .padding() } } #endif #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineCircularGaugeView.swift ================================================ // // DSFSparklineCircularGaugeView.swift // // Copyright © 2024 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif /// A circular gauge public class DSFSparklineCircularGaugeView: DSFSparklineSurfaceView { /// The value to display (0 ... 1) @objc public dynamic var value: CGFloat = 0.0 { didSet { self.overlay.value = self.value } } /// The style to use when drawing the gauge's track @objc public var trackStyle = DSFSparklineOverlay.CircularGauge.DefaultTrackStyle { didSet { self.overlay.trackStyle = self.trackStyle } } /// The width of the track @objc public dynamic var trackWidth: CGFloat { get { self.overlay.trackStyle.width } set { self.overlay.trackStyle.width = newValue } } /// Track color @objc public dynamic var trackColor: DSFColor = .black.withAlphaComponent(0.1) { didSet { self.overlay.trackStyle.fillColor = DSFSparkline.Fill.Color(trackColor.cgColor) } } /// The style to use when drawing the gauge's value @objc public var lineStyle = DSFSparklineOverlay.CircularGauge.DefaultLineStyle { didSet { self.overlay.lineStyle = self.lineStyle } } /// The width of the track @objc public dynamic var lineWidth: CGFloat { get { self.overlay.lineStyle.width } set { self.overlay.lineStyle.width = newValue } } /// Line color @objc public dynamic var lineColor: DSFColor = .black { didSet { self.overlay.lineStyle.fillColor = DSFSparkline.Fill.Color(lineColor.cgColor) } } /// The animation style to use when the value changes @objc public var animationStyle: DSFSparkline.AnimationStyle? = nil { didSet { self.overlay.animationStyle = animationStyle } } // MARK: - Initializers /// Create public override init(frame: CGRect) { super.init(frame: frame) self.configure() } /// Create public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } private let overlay = DSFSparklineOverlay.CircularGauge() } extension DSFSparklineCircularGaugeView { func configure() { self.addOverlay(self.overlay) self.clipsToBounds = false self.setNeedsDisplay() } } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineCircularProgressView+SwiftUI.swift ================================================ // // DSFSparklineCircularProgressView+SwiftUI.swift // // Copyright © 2024 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineCircularProgressView { /// A SwiftUI wrapper for the Circular Progress sparkline overlay struct SwiftUI { let value: Double let trackWidth: Double let fillStyle: DSFSparklineFillable let padding: CGFloat let trackColor: CGColor? let trackIcon: CGImage? public init( value: Double, fillStyle: DSFSparklineFillable = DSFSparklineOverlay.CircularProgress.DefaultFillStyle, trackWidth: Double = DSFSparklineOverlay.CircularProgress.DefaultTrackWidth, padding: Double = 0.0, trackColor: CGColor? = nil, trackIcon: CGImage? = nil ) { self.value = value self.trackWidth = trackWidth self.fillStyle = fillStyle self.padding = padding self.trackColor = trackColor self.trackIcon = trackIcon } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineCircularProgressView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineCircularProgressView #else public typealias UIViewType = DSFSparklineCircularProgressView #endif func makeProgressGraph(_: Context) -> DSFSparklineCircularProgressView { let view = DSFSparklineCircularProgressView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.value = self.value view.trackWidth = self.trackWidth view.fillStyle = self.fillStyle.copyFill() view.padding = self.padding if let t = self.trackColor { view.trackColor = DSFColor(cgColor: t) } view.trackIcon = trackIcon return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineCircularProgressView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineCircularProgressView { return self.makeProgressGraph(context) } func updateUIView(_ view: DSFSparklineCircularProgressView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineCircularProgressView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineCircularProgressView { return self.makeProgressGraph(context) } func updateNSView(_ view: DSFSparklineCircularProgressView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineCircularProgressView.SwiftUI { func updateView(_ view: DSFSparklineCircularProgressView) { UpdateIfNotEqual(result: &view.value, val: self.value) UpdateIfNotEqual(result: &view.trackWidth, val: self.trackWidth) UpdateIfNotEqual(result: &view.padding, val: self.padding) if view.fillStyle !== self.fillStyle { view.fillStyle = self.fillStyle } if view.trackColor !== self.trackColor { if let t = self.trackColor { view.trackColor = DSFColor(cgColor: t) } } if view.trackIcon !== self.trackIcon { view.trackIcon = self.trackIcon } } } #if DEBUG func namedImage(_ name: String) -> CGImage? { #if os(macOS) if #available(macOS 11.0, *) { let ns = NSImage(systemSymbolName: name, accessibilityDescription: nil)! //ns. return ns.cgImage(forProposedRect: nil, context: nil, hints: nil) } return nil #else let ui = UIImage(systemName: name)! return ui.cgImage #endif } @available(macOS 10.15, *) struct DSFSparklineCircularProgressViewPreviews: PreviewProvider { static var previews: some SwiftUI.View { let ge = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 1, green: 0.000, blue: 0.00, alpha: 1.0), CGColor(srgbRed: 0, green: 0.0, blue: 1, alpha: 1.0), ] ) let g = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 1.0), CGColor(srgbRed: 0.891, green: 0.000, blue: 0.090, alpha: 1.0), ] ) let g1 = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 1.0), CGColor(srgbRed: 0.601, green: 1.000, blue: 0.009, alpha: 1.0), ] ) let g2 = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 1.0), CGColor(srgbRed: 0.015, green: 0.847, blue: 1.000, alpha: 1.0), ] ) let g3 = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.996, green: 0.759, blue: 0.300, alpha: 1.0), CGColor(srgbRed: 0.996, green: 0.459, blue: 0.000, alpha: 1.0), ] ) let i1 = namedImage("arrow.right") let i2 = namedImage("arrow.up") let i3 = namedImage("arrow.triangle.swap") let i4 = namedImage("phone.arrow.right") ScrollView([.horizontal, .vertical]) { VStack { HStack(alignment: .center) { DSFSparklineCircularProgressView.SwiftUI(value: 0, trackWidth: 20) .frame(width: 100, height: 100) DSFSparklineCircularProgressView.SwiftUI(value: 0.4, trackWidth: 20) .frame(width: 100, height: 100) DSFSparklineCircularProgressView.SwiftUI(value: 0.8, trackWidth: 20) .frame(width: 100, height: 100) DSFSparklineCircularProgressView.SwiftUI(value: 1.2, trackWidth: 20) .frame(width: 100, height: 100) DSFSparklineCircularProgressView.SwiftUI(value: 1.6, trackWidth: 20) .frame(width: 100, height: 100) DSFSparklineCircularProgressView.SwiftUI(value: 2.0, trackWidth: 20) .frame(width: 100, height: 100) } ForEach(0 ..< 2) { which in HStack(alignment: .center) { DSFSparklineCircularProgressView.SwiftUI(value: 0, fillStyle: ge, trackWidth: 25, trackIcon: which == 1 ? i1 : nil) .frame(width: 100, height: 100) DSFSparklineCircularProgressView.SwiftUI(value: 0.4, fillStyle: ge, trackWidth: 25, trackIcon: which == 1 ? i1 : nil) .frame(width: 100, height: 100) DSFSparklineCircularProgressView.SwiftUI(value: 0.8, fillStyle: ge, trackWidth: 25, trackIcon: which == 1 ? i1 : nil) .frame(width: 100, height: 100) DSFSparklineCircularProgressView.SwiftUI(value: 1.2, fillStyle: ge, trackWidth: 25, trackIcon: which == 1 ? i1 : nil) .frame(width: 100, height: 100) DSFSparklineCircularProgressView.SwiftUI(value: 1.6, fillStyle: ge, trackWidth: 25, trackIcon: which == 1 ? i1 : nil) .frame(width: 100, height: 100) DSFSparklineCircularProgressView.SwiftUI(value: 2.0, fillStyle: ge, trackWidth: 25, trackIcon: which == 1 ? i1 : nil) .frame(width: 100, height: 100) } } HStack(alignment: .center) { ZStack { DSFSparklineCircularProgressView.SwiftUI(value: 0, fillStyle: g, trackColor: CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 0.1) , trackIcon: i1) DSFSparklineCircularProgressView.SwiftUI(value: 0, fillStyle: g1, padding: 12, trackColor: CGColor(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 0.1), trackIcon: i2) DSFSparklineCircularProgressView.SwiftUI(value: 0, fillStyle: g2, padding: 24, trackColor: CGColor(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 0.1), trackIcon: i3) DSFSparklineCircularProgressView.SwiftUI(value: 0, fillStyle: g3, padding: 36, trackColor: CGColor(srgbRed: 0.996, green: 0.759, blue: 0.300, alpha: 0.1), trackIcon: i4) } .frame(width: 150, height: 150) ZStack { DSFSparklineCircularProgressView.SwiftUI(value: 0.4, fillStyle: g, trackColor: CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 0.1) , trackIcon: i1) DSFSparklineCircularProgressView.SwiftUI(value: 0.3, fillStyle: g1, padding: 12, trackColor: CGColor(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 0.1), trackIcon: i2) DSFSparklineCircularProgressView.SwiftUI(value: 0.2, fillStyle: g2, padding: 24, trackColor: CGColor(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 0.1), trackIcon: i3) DSFSparklineCircularProgressView.SwiftUI(value: 0.1, fillStyle: g3, padding: 36, trackColor: CGColor(srgbRed: 0.996, green: 0.759, blue: 0.300, alpha: 0.1), trackIcon: i4) } .frame(width: 150, height: 150) ZStack { DSFSparklineCircularProgressView.SwiftUI(value: 0.7, fillStyle: g, trackColor: CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 0.1) , trackIcon: i1) DSFSparklineCircularProgressView.SwiftUI(value: 0.6, fillStyle: g1, padding: 12, trackColor: CGColor(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 0.1), trackIcon: i2) DSFSparklineCircularProgressView.SwiftUI(value: 0.5, fillStyle: g2, padding: 24, trackColor: CGColor(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 0.1), trackIcon: i3) DSFSparklineCircularProgressView.SwiftUI(value: 0.4, fillStyle: g3, padding: 36, trackColor: CGColor(srgbRed: 0.996, green: 0.759, blue: 0.300, alpha: 0.1), trackIcon: i4) } .frame(width: 150, height: 150) ZStack { DSFSparklineCircularProgressView.SwiftUI(value: 1.4, fillStyle: g, trackColor: CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 0.1) , trackIcon: i1) DSFSparklineCircularProgressView.SwiftUI(value: 1.3, fillStyle: g1, padding: 12, trackColor: CGColor(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 0.1), trackIcon: i2) DSFSparklineCircularProgressView.SwiftUI(value: 1.2, fillStyle: g2, padding: 24, trackColor: CGColor(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 0.1), trackIcon: i3) DSFSparklineCircularProgressView.SwiftUI(value: 1.1, fillStyle: g3, padding: 36, trackColor: CGColor(srgbRed: 0.996, green: 0.759, blue: 0.300, alpha: 0.1), trackIcon: i4) } .frame(width: 150, height: 150) ZStack { DSFSparklineCircularProgressView.SwiftUI(value: 1.9, fillStyle: g, trackColor: CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 0.1) , trackIcon: i1) DSFSparklineCircularProgressView.SwiftUI(value: 1.8, fillStyle: g1, padding: 12, trackColor: CGColor(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 0.1), trackIcon: i2) DSFSparklineCircularProgressView.SwiftUI(value: 1.7, fillStyle: g2, padding: 24, trackColor: CGColor(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 0.1), trackIcon: i3) DSFSparklineCircularProgressView.SwiftUI(value: 1.6, fillStyle: g3, padding: 36, trackColor: CGColor(srgbRed: 0.996, green: 0.759, blue: 0.300, alpha: 0.1), trackIcon: i4) } .frame(width: 150, height: 150) } } .padding() } } } #endif #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineCircularProgressView.swift ================================================ // // DSFSparklineCircularProgressView.swift // // Copyright © 2024 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif @objc public class DSFSparklineCircularProgressView: DSFSparklineSurfaceView { @objc public dynamic var value: CGFloat = 0.0 { didSet { self.overlay.value = self.value } } @objc public dynamic var trackWidth: CGFloat = 10 { didSet { self.overlay.trackWidth = self.trackWidth } } /// The padding (inset) for drawing the ring @objc public dynamic var padding: CGFloat = 0.0 { didSet { self.overlay.padding = self.padding } } /// The stroke color for the pie chart @objc public dynamic var trackColor: DSFColor? { didSet { if let t = self.trackColor?.cgColor { self.overlay.trackColor = t } } } /// The stroke color for the pie chart @objc public dynamic var progressColor: DSFColor? { didSet { if let t = self.progressColor?.cgColor { self.overlay.fillStyle = DSFSparkline.Fill.Color(t) } } } /// The track's icon @objc public dynamic var trackIcon: CGImage? { didSet { self.overlay.icon = self.trackIcon } } /// The fill color for the value ring @objc public var fillStyle: DSFSparklineFillable = DSFSparkline.Fill.Color.white { didSet { self.overlay.fillStyle = fillStyle } } // MARK: - Initializers public override init(frame: CGRect) { super.init(frame: frame) self.configure() } public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } private let overlay = DSFSparklineOverlay.CircularProgress() } extension DSFSparklineCircularProgressView { func configure() { self.addOverlay(self.overlay) self.setNeedsDisplay() } } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineDataBarGraphView+SwiftUI.swift ================================================ // // DSFSparklineDataBarGraphView+SwiftUI.swift // DSFSparklines // // Created by Darren Ford on 12/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineDataBarGraphView { struct SwiftUI { /// Datasource for the graph let dataSource: DSFSparkline.StaticDataSource /// Maximum total value. If -1, this value is i let maximumTotalValue: CGFloat /// The 'undrawn' color for the graph let unsetColor: DSFColor? /// Palette to use when coloring the chart let palette: DSFSparkline.Palette /// Stroke Color let strokeColor: DSFColor? /// Stroke Width let lineWidth: CGFloat /// The animation style to apply when datasource changes let animationStyle: DSFSparkline.AnimationStyle? /// Create a databar graph /// - Parameters: /// - dataSource: The data source for the graph /// - maximumTotalValue: The maximum _total_ value. If the datasource values total is greater than this value, it clips the display /// - palette: The color palette to use when drawing the graph /// - unsetColor: (optional) the color to use when drawing the background (useful when the maximumValue is also set) /// - strokeColor: The color to draw the separator lines between data points /// - lineWidth: The width of the separator lines /// - animationStyle: The animation style, or nil for no animation public init( dataSource: DSFSparkline.StaticDataSource, maximumTotalValue: CGFloat = -1, palette: DSFSparkline.Palette = .shared, unsetColor: DSFColor? = nil, strokeColor: DSFColor? = nil, lineWidth: CGFloat = 1.0, animationStyle: DSFSparkline.AnimationStyle? = nil ) { self.dataSource = dataSource self.maximumTotalValue = maximumTotalValue self.unsetColor = unsetColor self.strokeColor = strokeColor self.lineWidth = lineWidth self.palette = palette self.animationStyle = animationStyle } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineDataBarGraphView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineDataBarGraphView #else public typealias UIViewType = DSFSparklineDataBarGraphView #endif public class Coordinator: NSObject { let parent: DSFSparklineDataBarGraphView.SwiftUI init(_ sparkline: DSFSparklineDataBarGraphView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } func makeDataBarGraph(_: Context) -> DSFSparklineDataBarGraphView { let view = DSFSparklineDataBarGraphView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.strokeColor = self.strokeColor view.unsetColor = self.unsetColor view.lineWidth = self.lineWidth view.palette = self.palette view.animationStyle = self.animationStyle view.dataSource = self.dataSource view.maximumTotalValue = self.maximumTotalValue return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineDataBarGraphView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineDataBarGraphView { return self.makeDataBarGraph(context) } func updateUIView(_ view: DSFSparklineDataBarGraphView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineDataBarGraphView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineDataBarGraphView { return self.makeDataBarGraph(context) } func updateNSView(_ view: DSFSparklineDataBarGraphView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineDataBarGraphView.SwiftUI { func updateView(_ view: DSFSparklineDataBarGraphView) { UpdateIfNotEqual(result: &view.strokeColor, val: self.strokeColor) UpdateIfNotEqual(result: &view.unsetColor, val: self.unsetColor) UpdateIfNotEqual(result: &view.lineWidth, val: self.lineWidth) UpdateIfNotEqual(result: &view.palette, val: self.palette) UpdateIfNotEqual(result: &view.animationStyle, val: self.animationStyle) UpdateIfNotEqual(result: &view.dataSource, val: self.dataSource) UpdateIfNotEqual(result: &view.maximumTotalValue, val: self.maximumTotalValue) } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineDataBarGraphView.swift ================================================ // // DSFSparklineDataBarGraphView.swift // DSFSparklines // // Created by Darren Ford on 12/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif /// A sparkline that draws a simple pie chart public class DSFSparklineDataBarGraphView: DSFSparklineSurfaceView { let databarOverlay = DSFSparklineOverlay.DataBar() /// The data to be displayed in the data bar. /// /// The values become a percentage of the total value stored within the /// dataStore, and as such each value ends up being drawn as a fraction of the total. /// So for example, if you want the pie chart to represent the number of red cars vs. number of /// blue cars, you just set the values directly. @objc public var dataSource = DSFSparkline.StaticDataSource() { didSet { self.databarOverlay.dataSource = self.dataSource } } // MARK: - Maximum Total /// The maximum _total_ value. If the datasource values total is greater than this value, it clips the display @objc public dynamic var maximumTotalValue: CGFloat = -1 { didSet { self.databarOverlay.maximumTotalValue = self.maximumTotalValue } } // MARK: Optional background color /// The 'undrawn' color for the graph @objc public dynamic var unsetColor: DSFColor? { didSet { self.databarOverlay.unsetColor = self.unsetColor?.cgColor } } // MARK: - Stroke /// The stroke color for the pie chart @objc public dynamic var strokeColor: DSFColor? { didSet { self.databarOverlay.strokeColor = self.strokeColor?.cgColor } } /// The width of the stroke line @objc public dynamic var lineWidth: CGFloat = 0.5 { didSet { self.updateDisplay() } } /// The animation style to apply when datasource changes, or nil for no animation @objc public var animationStyle: DSFSparkline.AnimationStyle? = nil { didSet { self.databarOverlay.animationStyle = self.animationStyle } } /// The palette to use when drawing the pie chart @objc public var palette = DSFSparkline.Palette.shared { didSet { self.databarOverlay.palette = self.palette } } // MARK: - Initializers public override init(frame: CGRect) { super.init(frame: frame) self.configure() } public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } func configure() { self.addOverlay(self.databarOverlay) self.databarOverlay.setNeedsDisplay() self.databarOverlay.unsetColor = self.unsetColor?.cgColor self.databarOverlay.strokeColor = self.strokeColor?.cgColor self.databarOverlay.lineWidth = self.lineWidth self.databarOverlay.maximumTotalValue = self.maximumTotalValue self.databarOverlay.animationStyle = self.animationStyle self.databarOverlay.dataSource = self.dataSource } // MARK: - Privates internal var animator = ArbitraryAnimator() internal var fractionComplete: CGFloat = 0 internal var total: CGFloat = 0.0 } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineDotGraphView+SwiftUI.swift ================================================ // // DSFSparklineDotGraphView+SwiftUI.swift // DSFSparklines // // Created by Darren Ford on 7/12/20. // Copyright © 2020 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineDotGraphView { struct SwiftUI { /// Datasource for the graph let dataSource: DSFSparkline.DataSource /// The primary color for the sparkline let graphColor: DSFColor /// Are the values drawn from the top down? let upsideDown: Bool /// The number of vertical buckets to break the input data up into let verticalDotCount: UInt /// The secondary color for the sparkline let unsetGraphColor: DSFColor /// Create a sparkline graph that displays dots (like the CPU history graph in Activity Monitor) /// - Parameters: /// - dataSource: The data source for the graph /// - graphColor: The color of the dots that are set /// - unsetGraphColor: The color of the dots that are not set /// - verticalDotCount: The number of dots vertically /// - upsideDown: Draw the graph upside down public init(dataSource: DSFSparkline.DataSource, graphColor: DSFColor, unsetGraphColor: DSFColor = DSFColor.clear, verticalDotCount: UInt = 10, upsideDown: Bool = false) { self.dataSource = dataSource self.graphColor = graphColor self.verticalDotCount = verticalDotCount self.upsideDown = upsideDown self.unsetGraphColor = unsetGraphColor } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineDotGraphView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineDotGraphView #else public typealias UIViewType = DSFSparklineDotGraphView #endif public class Coordinator: NSObject { let parent: DSFSparklineDotGraphView.SwiftUI init(_ sparkline: DSFSparklineDotGraphView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } private func makeDotGraph(_: Context) -> DSFSparklineDotGraphView { let view = DSFSparklineDotGraphView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.dataSource = self.dataSource view.graphColor = self.graphColor view.verticalDotCount = self.verticalDotCount view.unsetGraphColor = self.unsetGraphColor view.upsideDown = self.upsideDown return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 99.0, *) public extension DSFSparklineDotGraphView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineDotGraphView { return self.makeDotGraph(context) } func updateUIView(_ view: DSFSparklineDotGraphView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineDotGraphView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineDotGraphView { return self.makeDotGraph(context) } func updateNSView(_ view: DSFSparklineDotGraphView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineDotGraphView.SwiftUI { func updateView(_ view: DSFSparklineDotGraphView) { UpdateIfNotEqual(result: &view.graphColor, val: self.graphColor) UpdateIfNotEqual(result: &view.upsideDown, val: self.upsideDown) UpdateIfNotEqual(result: &view.verticalDotCount, val: self.verticalDotCount) UpdateIfNotEqual(result: &view.unsetGraphColor, val: self.unsetGraphColor) UpdateIfNotEqual(result: &view.dataSource, val: self.dataSource) } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineDotGraphView.swift ================================================ // // DSFSparklineDataSource.swift // DSFSparklines // // Created by Darren Ford on 16/1/20. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif /// A sparkline graph that displays dots (like the CPU history graph in Activity Monitor) @objc public class DSFSparklineDotGraphView: DSFSparklineDataSourceView { let overlay = DSFSparklineOverlay.Dot() /// Are the values drawn from the top down? @objc public dynamic var upsideDown: Bool = false { didSet { self.overlay.upsideDown = self.upsideDown self.updateDisplay() } } /// The number of vertical buckets to break the input data up into @objc public dynamic var verticalDotCount: UInt = 10 { didSet { self.overlay.verticalDotCount = self.verticalDotCount self.updateDisplay() } } /// The secondary color for the sparkline #if os(macOS) @objc public dynamic var unsetGraphColor: NSColor = NSColor.clear { didSet { self.colorDidChange() } } #else @objc public dynamic var unsetGraphColor: UIColor = UIColor.clear { didSet { self.colorDidChange() } } #endif public override init(frame: CGRect) { super.init(frame: frame) self.configure() } public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } func configure() { self.addOverlay(self.overlay) self.overlay.setNeedsDisplay() } override func colorDidChange() { super.colorDidChange() self.overlay.upsideDown = self.upsideDown self.overlay.verticalDotCount = self.verticalDotCount self.overlay.onColor = self.graphColor.cgColor self.overlay.offColor = self.unsetGraphColor.cgColor } } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineLineGraphView+SwiftUI.swift ================================================ // // DSFSparklineLineGraphView+SwiftUI.swift // DSFSparklines // // Created by Darren Ford on 7/12/20. // Copyright © 2020 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineLineGraphView { struct SwiftUI { /// Datasource for the graph let dataSource: DSFSparkline.DataSource /// The primary color for the sparkline let graphColor: DSFColor /// Draw a dotted line at the zero point on the y-axis let showZeroLine: Bool /// The drawing definition for the zero line point let zeroLineDefinition: DSFSparkline.ZeroLineDefinition /// The width for the line drawn on the graph let lineWidth: CGFloat /// The number of vertical buckets to break the input data up into let interpolated: Bool /// The secondary color for the sparkline let lineShading: Bool /// Draw a shadow under the line let shadowed: Bool /// Should the line graph be centered around the zero-line? let centeredAtZeroLine: Bool /// The color used to draw values lower than the zero-line, or nil for the same as the graph color let lowerGraphColor: DSFColor? /// Highlight y-ranges within the graph let highlightDefinitions: [DSFSparkline.HighlightRangeDefinition] /// The size of the markers to draw. If the markerSize is less than 0, markers will not draw let markerSize: CGFloat let markerDrawingBlock: DSFSparklineOverlay.Line.MarkerDrawingBlock? /// The grid lines definition let gridLines: DSFSparkline.GridLinesDefinition? /// Primary fill let primaryFill: DSFSparklineFillable? /// Secondary fill let secondaryFill: DSFSparklineFillable? /// Create a sparkline graph that displays dots (like the CPU history graph in Activity Monitor) /// - Parameters: /// - dataSource: The data source for the graph /// - graphColor: The color of the dots that are set /// - lineWidth: The width of the line /// - interpolated: If true, curves the line through the graph points. /// - lineShading: If true, shades the underside of the drawn line. /// - shadowed: If true, draws a shadow under the line part of the graph. /// - showZeroLine: Show or hide a 'zero line' horizontal line /// - zeroLineDefinition: the settings for drawing the zero line /// - centeredAtZeroLine: Should the line graph be centered around the zero-line? /// - lowerGraphColor: The color used to draw values lower than the zero-line, or nil for the same as the graph color /// - highlightDefinitions: The style of the y-range highlight /// - markerSize: The size of the markers to draw. If the markerSize is less than 0, markers will not draw /// - markerDrawingBlock: (optional) function to draw the markers /// - gridLines: The grid lines to draw on the graph public init( dataSource: DSFSparkline.DataSource, graphColor: DSFColor, lineWidth: CGFloat = 1.5, interpolated: Bool = false, lineShading: Bool = true, shadowed: Bool = false, showZeroLine: Bool = false, zeroLineDefinition: DSFSparkline.ZeroLineDefinition = .shared, centeredAtZeroLine: Bool = false, lowerGraphColor: DSFColor? = nil, highlightDefinitions: [DSFSparkline.HighlightRangeDefinition] = [], markerSize: CGFloat = -1, markerDrawingBlock: DSFSparklineOverlay.Line.MarkerDrawingBlock? = nil, gridLines: DSFSparkline.GridLinesDefinition? = nil, primaryFill: (any DSFSparklineFillable)? = nil, secondaryFill: (any DSFSparklineFillable)? = nil ) { self.dataSource = dataSource self.graphColor = graphColor self.showZeroLine = showZeroLine self.zeroLineDefinition = zeroLineDefinition self.centeredAtZeroLine = centeredAtZeroLine self.lowerGraphColor = lowerGraphColor self.lineWidth = lineWidth self.interpolated = interpolated self.lineShading = lineShading self.shadowed = shadowed self.highlightDefinitions = highlightDefinitions self.markerSize = markerSize self.markerDrawingBlock = markerDrawingBlock self.gridLines = gridLines self.primaryFill = primaryFill self.secondaryFill = secondaryFill } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineLineGraphView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineLineGraphView #else public typealias UIViewType = DSFSparklineLineGraphView #endif public class Coordinator: NSObject { let parent: DSFSparklineLineGraphView.SwiftUI init(_ sparkline: DSFSparklineLineGraphView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } func makeLineGraph(_: Context) -> DSFSparklineLineGraphView { let view = DSFSparklineLineGraphView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.dataSource = self.dataSource view.graphColor = self.graphColor view.lineWidth = self.lineWidth view.interpolated = self.interpolated view.lineShading = self.lineShading view.shadowed = self.shadowed view.zeroLineVisible = self.showZeroLine view.setZeroLineDefinition(self.zeroLineDefinition) view.centeredAtZeroLine = self.centeredAtZeroLine view.lowerGraphColor = self.lowerGraphColor if self.highlightDefinitions.count > 0 { view.highlightRangeVisible = true view.highlightRangeDefinition = self.highlightDefinitions } if let gridLines = self.gridLines { view.setGridLineDefinition(gridLines) } else { view.gridLinesVisible = false } view.markerSize = self.markerSize if let pf = self.primaryFill { view.primaryFill = pf } if let sf = self.secondaryFill { view.secondaryFill = sf } return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineLineGraphView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineLineGraphView { return self.makeLineGraph(context) } func updateUIView(_ view: DSFSparklineLineGraphView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineLineGraphView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineLineGraphView { return self.makeLineGraph(context) } func updateNSView(_ view: DSFSparklineLineGraphView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineLineGraphView.SwiftUI { func updateView(_ view: DSFSparklineLineGraphView) { UpdateIfNotEqual(result: &view.graphColor, val: self.graphColor) UpdateIfNotEqual(result: &view.zeroLineVisible, val: self.showZeroLine) view.setZeroLineDefinition(self.zeroLineDefinition) UpdateIfNotEqual(result: &view.centeredAtZeroLine, val: self.centeredAtZeroLine) UpdateIfNotEqual(result: &view.lowerGraphColor, val: self.lowerGraphColor) UpdateIfNotEqual(result: &view.lineWidth, val: self.lineWidth) UpdateIfNotEqual(result: &view.interpolated, val: self.interpolated) UpdateIfNotEqual(result: &view.lineShading, val: self.lineShading) UpdateIfNotEqual(result: &view.shadowed, val: self.shadowed) UpdateIfNotEqual(result: &view.markerSize, val: self.markerSize) view.markerDrawingBlock = self.markerDrawingBlock if self.highlightDefinitions.count > 0 { view.highlightRangeVisible = true view.highlightRangeDefinition = self.highlightDefinitions } else { view.highlightRangeVisible = false view.highlightRangeDefinition = [] } if let pf = self.primaryFill { view.primaryFill = pf } if let sf = self.secondaryFill { view.secondaryFill = sf } UpdateIfNotEqual(result: &view.dataSource, val: self.dataSource) } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineLineGraphView.swift ================================================ // // DSFSparklineLineGraphView.swift // DSFSparklines // // Created by Darren Ford on 20/12/19. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if os(macOS) import Cocoa #else import UIKit #endif /// A sparkline that draws a line graph @objc public class DSFSparklineLineGraphView: DSFSparklineZeroLineGraphView { let overlay = DSFSparklineOverlay.Line() /// The width for the line drawn on the graph @objc public dynamic var lineWidth: CGFloat = 1.5 { didSet { self.overlay.strokeWidth = self.lineWidth } } /// Interpolate a curve between the points @objc public dynamic var interpolated: Bool = false { didSet { self.overlay.interpolated = self.interpolated } } /// Shade the area under the line @objc public dynamic var lineShading: Bool = true { didSet { if lineShading == true { self.overlay.primaryFill = DSFSparkline.Fill.Color(self.graphColor.withAlphaComponent(0.4).cgColor) } else { self.overlay.primaryFill = nil } } } /// Draw a shadow under the line @objc public dynamic var shadowed: Bool = false { didSet { self.overlay.shadow = self.shadowed ? NSShadow.sparklineDefault : nil } } /// The size of the markers to draw. If the markerSize is less than 0, markers will not draw @objc public dynamic var markerSize: CGFloat = -1 { didSet { self.overlay.markerSize = self.markerSize } } /// An optional drawing function for custom drawing markers. When nil, uses the standard circle for each marker /// /// The `markerSize` value is used to determine the frameSize of each marker. /// If `markerSize` is less than 1, this block will not be called. /// /// Note that this function is called very frequently so make sure its performant! @objc public var markerDrawingBlock: DSFSparklineOverlay.Line.MarkerDrawingBlock? = nil { didSet { self.overlay.markerDrawingBlock = self.markerDrawingBlock } } /// Should the graph be centered at the zero line? @objc public dynamic var centeredAtZeroLine: Bool = false { didSet { self.overlay.centeredAtZeroLine = self.centeredAtZeroLine } } /// The primary fill (for the area of the graph ABOVE the zero line) @objc public dynamic var primaryFill: (any DSFSparklineFillable)? { get { self.overlay.primaryFill } set { self.overlay.primaryFill = newValue } } /// The secondary fill (for the area of the graph UNDER the zero line) @objc public dynamic var secondaryFill: (any DSFSparklineFillable)? { get { self.overlay.secondaryFill } set { self.overlay.secondaryFill = newValue } } // Optional gradient colors internal var gradient: CGGradient? internal var lowerGradient: CGGradient? public override init(frame: CGRect) { super.init(frame: frame) self.configure() } public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } func configure() { self.addOverlay(self.overlay) self.colorDidChange() } override func colorDidChange() { super.colorDidChange() self.overlay.strokeWidth = self.lineWidth self.overlay.interpolated = self.interpolated self.overlay.shadow = self.shadowed ? NSShadow.sparklineDefault : nil self.overlay.markerSize = self.markerSize self.overlay.centeredAtZeroLine = self.centeredAtZeroLine self.overlay.primaryStrokeColor = self.graphColor.cgColor self.overlay.secondaryStrokeColor = self.lowerColor.cgColor // Backwards compatibility let color = self.graphColor let fill = DSFSparkline.Fill.Gradient(colors: [ color.withAlphaComponent(0.4).cgColor, color.withAlphaComponent(0.2).cgColor ]) self.overlay.primaryFill = fill if let lowerColor = self.lowerGraphColor { let fill = DSFSparkline.Fill.Gradient(colors: [ lowerColor.withAlphaComponent(0.4).cgColor, lowerColor.withAlphaComponent(0.2).cgColor ]) self.overlay.secondaryFill = fill } else { // Fallback - if secondary fill not defined the compatibility view is to use the primary fill view self.overlay.secondaryFill = self.overlay.primaryFill } } } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklinePercentBarGraphView+SwiftUI.swift ================================================ // // DSFSparklinePercentBarGraphView+SwiftUI.swift // DSFSparklines // // Created by Darren Ford on 12/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklinePercentBarGraphView { /// The SwiftUI percent bar graph struct SwiftUI { /// Datasource for the graph let style: DSFSparkline.PercentBar.Style /// Palette to use when coloring the chart let value: Double /// The animation style to apply to changes let animationStyle: DSFSparkline.AnimationStyle? /// Create a sparkline graph that displays dots (like the CPU history graph in Activity Monitor) /// - Parameters: public init( style: DSFSparkline.PercentBar.Style = DSFSparkline.PercentBar.Style(), value: Double, animationStyle: DSFSparkline.AnimationStyle? = nil ) { self.style = style self.value = value self.animationStyle = animationStyle } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklinePercentBarGraphView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklinePercentBarGraphView #else public typealias UIViewType = DSFSparklinePercentBarGraphView #endif public class Coordinator: NSObject { let parent: DSFSparklinePercentBarGraphView.SwiftUI init(_ sparkline: DSFSparklinePercentBarGraphView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } func makePercentBar(_: Context) -> DSFSparklinePercentBarGraphView { let view = DSFSparklinePercentBarGraphView(frame: .zero) view.displayStyle = self.style view.value = CGFloat(self.value) view.animationStyle = self.animationStyle return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklinePercentBarGraphView.SwiftUI { func makeUIView(context: Context) -> DSFSparklinePercentBarGraphView { return self.makePercentBar(context) } func updateUIView(_ view: DSFSparklinePercentBarGraphView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklinePercentBarGraphView.SwiftUI { func makeNSView(context: Context) -> DSFSparklinePercentBarGraphView { return self.makePercentBar(context) } func updateNSView(_ view: DSFSparklinePercentBarGraphView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklinePercentBarGraphView.SwiftUI { func updateView(_ view: DSFSparklinePercentBarGraphView) { view.displayStyle = self.style view.value = CGFloat(self.value) view.animationStyle = self.animationStyle } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklinePercentBarGraphView.swift ================================================ // // DSFSparklinePercentBarGraphView.swift // DSFSparklines // // Created by Darren Ford on 16/3/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if os(macOS) import AppKit #else import UIKit #endif /// A sparkline that draws a percent bar @objc public class DSFSparklinePercentBarGraphView: DSFSparklineSurfaceView { // MARK: - Value /// The initial value to display in the percent bar @objc public dynamic var value: CGFloat = 0.2 { didSet { self.overlay.value = self.value self.displayUpdate() } } /// The style for presenting the percent bar @objc public var displayStyle = DSFSparkline.PercentBar.Style() { didSet { self.overlay.displayStyle = displayStyle self.updateDisplay() } } /// The corner radius for the bar @objc public dynamic var cornerRadius: CGFloat = 5 { didSet { self.displayStyle.cornerRadius = self.cornerRadius self.displayUpdate() } } /// Should the control display a text label for the percent bar @objc public dynamic var showLabel: Bool = true { didSet { self.displayStyle.showLabel = self.showLabel self.displayUpdate() } } /// The background of the bar color for the percent bar chart @objc public dynamic var underBarColor: DSFColor = .clear { didSet { self.displayStyle.underBarColor = self.underBarColor.cgColor self.displayUpdate() } } /// The color for text displayed on the background @objc public dynamic var underBarTextColor: DSFColor = .white { didSet { self.displayStyle.underBarTextColor = self.underBarTextColor.cgColor self.displayUpdate() } } // MARK: - Bar Color /// The bar color for the percent bar chart @objc public dynamic var barColor: DSFColor = .black { didSet { self.displayStyle.barColor = self.barColor.cgColor self.displayUpdate() } } // MARK: - Background Color /// The color for text displayed on the bar @objc public dynamic var barTextColor: DSFColor = .white { didSet { self.displayStyle.barTextColor = self.barTextColor.cgColor self.displayUpdate() } } // MARK: - Font /// The name of the font to use when drawing the percent bar label @objc public dynamic var fontName: String? = nil { didSet { if let f = fontName, let font = DSFFont(name: f, size: self.fontSize) { self.displayStyle.font = font } else { self.displayStyle.font = DSFFont.systemFont(ofSize: self.fontSize) } self.displayUpdate() } } /// The size (in points) of the font to use when drawing the percent bar label @objc public dynamic var fontSize: CGFloat = 12 { didSet { let font = self.displayStyle.font #if os(macOS) if let newFont = DSFFont(descriptor: font.fontDescriptor, size: fontSize) { self.displayStyle.font = newFont } #else self.displayStyle.font = DSFFont(descriptor: font.fontDescriptor, size: fontSize) #endif self.overlay.displayStyle = self.displayStyle self.displayUpdate() } } /// The left inset for the bar @objc public dynamic var leftInset: CGFloat = 0 { didSet { self.displayStyle.barEdgeInsets.left = self.leftInset self.displayUpdate() } } /// The top inset for the bar @objc public dynamic var topInset: CGFloat = 0 { didSet { self.displayStyle.barEdgeInsets.top = self.topInset self.displayUpdate() } } /// The right inset for the bar @objc public dynamic var rightInset: CGFloat = 0 { didSet { self.displayStyle.barEdgeInsets.right = self.rightInset self.displayUpdate() } } /// The bottom inset for the bar @objc public dynamic var bottomInset: CGFloat = 0 { didSet { self.displayStyle.barEdgeInsets.bottom = self.bottomInset self.displayUpdate() } } // MARK: - Animation @objc public var animationStyle: DSFSparkline.AnimationStyle? = nil { didSet { self.overlay.animationStyle = self.animationStyle self.displayUpdate() } } // MARK: - Control /// Initializer public override init(frame: CGRect) { super.init(frame: frame) self.configure() } /// Initializer public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } // The overlay private let overlay = DSFSparklineOverlay.PercentBar(value: 0) } private extension DSFSparklinePercentBarGraphView { func configure() { self.addOverlay(self.overlay) self.overlay.setNeedsDisplay() self.displayStyle.underBarColor = self.underBarColor.cgColor self.displayStyle.underBarTextColor = self.underBarTextColor.cgColor self.displayStyle.barColor = self.barColor.cgColor self.displayStyle.barTextColor = self.barTextColor.cgColor if let f = fontName, let font = DSFFont(name: f, size: self.fontSize) { self.displayStyle.font = font } else { self.displayStyle.font = DSFFont.systemFont(ofSize: self.fontSize) } self.overlay.displayStyle = self.displayStyle self.overlay.value = self.value self.overlay.setNeedsDisplay() self.updateDisplay() } func displayUpdate() { self.overlay.displayStyle = self.displayStyle } } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklinePieGraphView+SwiftUI.swift ================================================ // // DSFSparklinePieGraphView+SwiftUI.swift // DSFSparklines // // Created by Darren Ford on 12/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklinePieGraphView { struct SwiftUI { /// Datasource for the graph let dataSource: DSFSparkline.StaticDataSource /// Palette to use when coloring the chart let palette: DSFSparkline.Palette /// Stroke Color let strokeColor: DSFColor? /// Stroke Width let lineWidth: CGFloat /// Should we animate the dataSource changes let animationStyle: DSFSparkline.AnimationStyle? /// Create a sparkline graph that displays dots (like the CPU history graph in Activity Monitor) /// - Parameters: /// - dataSource: The data source for the graph public init( dataSource: DSFSparkline.StaticDataSource, palette: DSFSparkline.Palette = .shared, strokeColor: DSFColor? = nil, lineWidth: CGFloat = 1.0, animationStyle: DSFSparkline.AnimationStyle? = nil ) { self.dataSource = dataSource self.strokeColor = strokeColor self.lineWidth = lineWidth self.palette = palette self.animationStyle = animationStyle } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklinePieGraphView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklinePieGraphView #else public typealias UIViewType = DSFSparklinePieGraphView #endif public class Coordinator: NSObject { let parent: DSFSparklinePieGraphView.SwiftUI init(_ sparkline: DSFSparklinePieGraphView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } func makePieGraph(_: Context) -> DSFSparklinePieGraphView { let view = DSFSparklinePieGraphView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.strokeColor = self.strokeColor view.lineWidth = self.lineWidth view.palette = self.palette view.animationStyle = self.animationStyle view.dataSource = self.dataSource return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklinePieGraphView.SwiftUI { func makeUIView(context: Context) -> DSFSparklinePieGraphView { return self.makePieGraph(context) } func updateUIView(_ view: DSFSparklinePieGraphView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklinePieGraphView.SwiftUI { func makeNSView(context: Context) -> DSFSparklinePieGraphView { return self.makePieGraph(context) } func updateNSView(_ view: DSFSparklinePieGraphView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklinePieGraphView.SwiftUI { func updateView(_ view: DSFSparklinePieGraphView) { UpdateIfNotEqual(result: &view.strokeColor, val: self.strokeColor) UpdateIfNotEqual(result: &view.lineWidth, val: self.lineWidth) UpdateIfNotEqual(result: &view.palette, val: self.palette) UpdateIfNotEqual(result: &view.animationStyle, val: self.animationStyle) UpdateIfNotEqual(result: &view.dataSource, val: self.dataSource) } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklinePieGraphView.swift ================================================ // // DSFSparklinePieGraphView.swift // DSFSparklines // // Created by Darren Ford on 12/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif /// A sparkline that draws a simple pie chart @objc public class DSFSparklinePieGraphView: DSFSparklineSurfaceView { let pieOverlay = DSFSparklineOverlay.Pie() /// The data to be displayed in the pie. /// /// The values become a percentage of the total value stored within the /// dataStore, and as such each value ends up being drawn as a fraction of the total. /// So for example, if you want the pie chart to represent the number of red cars vs. number of /// blue cars, you just set the values directly. @objc public var dataSource = DSFSparkline.StaticDataSource() { didSet { self.pieOverlay.dataSource = self.dataSource } } /// The stroke color for the pie chart @objc public dynamic var strokeColor: DSFColor? { didSet { self.pieOverlay.strokeColor = self.strokeColor?.cgColor } } /// The width of the stroke line @objc public dynamic var lineWidth: CGFloat = 0.5 { didSet { self.pieOverlay.lineWidth = self.lineWidth } } /// Should the pie chart animate in? @objc public var animationStyle: DSFSparkline.AnimationStyle? = nil { didSet { self.pieOverlay.animationStyle = self.animationStyle } } /// The palette to use when drawing the pie chart @objc public var palette = DSFSparkline.Palette.shared { didSet { self.pieOverlay.palette = self.palette } } // MARK: - Initializers public override init(frame: CGRect) { super.init(frame: frame) self.configure() } public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } func configure() { self.addOverlay(self.pieOverlay) self.pieOverlay.setNeedsDisplay() self.pieOverlay.strokeColor = self.strokeColor?.cgColor self.pieOverlay.lineWidth = self.lineWidth self.pieOverlay.animationStyle = self.animationStyle self.pieOverlay.dataSource = self.dataSource } // MARK: - Privates internal var animator = ArbitraryAnimator() internal var fractionComplete: CGFloat = 0 internal var total: CGFloat = 0.0 } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineStackLineGraphView+SwiftUI.swift ================================================ // // DSFSparklineLineGraphView+SwiftUI.swift // DSFSparklines // // Created by Darren Ford on 7/12/20. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineStackLineGraphView { struct SwiftUI { /// Datasource for the graph let dataSource: DSFSparkline.DataSource /// The primary color for the sparkline let graphColor: DSFColor /// Draw a dotted line at the zero point on the y-axis let showZeroLine: Bool /// The drawing definition for the zero line point let zeroLineDefinition: DSFSparkline.ZeroLineDefinition /// The width for the line drawn on the graph let lineWidth: CGFloat /// Should the area below the line be shaded? let lineShading: Bool /// Draw a shadow under the line let shadowed: Bool /// Should the line graph be centered around the zero-line? let centeredAtZeroLine: Bool /// The color used to draw values lower than the zero-line, or nil for the same as the graph color let lowerGraphColor: DSFColor? /// Highlight y-ranges within the graph let highlightDefinitions: [DSFSparkline.HighlightRangeDefinition] /// The grid lines to be drawn on the graph let gridLines: DSFSparkline.GridLinesDefinition? /// Primary fill let primaryFill: DSFSparklineFillable? /// Secondary fill let secondaryFill: DSFSparklineFillable? /// Create a sparkline graph that displays dots (like the CPU history graph in Activity Monitor) /// - Parameters: /// - dataSource: The data source for the graph /// - graphColor: The color of the dots that are set /// - lineWidth: The width of the line /// - lineShading: If true, shades the underside of the drawn line. /// - shadowed: If true, draws a shadow under the line part of the graph. /// - showZeroLine: Show or hide a 'zero line' horizontal line /// - zeroLineDefinition: the settings for drawing the zero line /// - centeredAtZeroLine: Should the line graph be centered around the zero-line? /// - lowerGraphColor: The color used to draw values lower than the zero-line, or nil for the same as the graph color /// - highlightDefinitions: The style of the y-range highlight /// - gridLines: DSFSparkline.GridLinesDefinition? = nil public init( dataSource: DSFSparkline.DataSource, graphColor: DSFColor, lineWidth: CGFloat = 1.5, lineShading: Bool = true, shadowed: Bool = false, showZeroLine: Bool = false, zeroLineDefinition: DSFSparkline.ZeroLineDefinition = .shared, centeredAtZeroLine: Bool = false, lowerGraphColor: DSFColor? = nil, highlightDefinitions: [DSFSparkline.HighlightRangeDefinition] = [], gridLines: DSFSparkline.GridLinesDefinition? = nil, primaryFill: (any DSFSparklineFillable)? = nil, secondaryFill: (any DSFSparklineFillable)? = nil ) { self.dataSource = dataSource self.graphColor = graphColor self.showZeroLine = showZeroLine self.zeroLineDefinition = zeroLineDefinition self.centeredAtZeroLine = centeredAtZeroLine self.lowerGraphColor = lowerGraphColor self.lineWidth = lineWidth self.lineShading = lineShading self.shadowed = shadowed self.highlightDefinitions = highlightDefinitions self.gridLines = gridLines self.primaryFill = primaryFill self.secondaryFill = secondaryFill } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineStackLineGraphView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineStackLineGraphView #else public typealias UIViewType = DSFSparklineStackLineGraphView #endif public class Coordinator: NSObject { let parent: DSFSparklineStackLineGraphView.SwiftUI init(_ sparkline: DSFSparklineStackLineGraphView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } func makeLineGraph(_: Context) -> DSFSparklineStackLineGraphView { let view = DSFSparklineStackLineGraphView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.dataSource = self.dataSource view.graphColor = self.graphColor view.lineWidth = self.lineWidth view.lineShading = self.lineShading view.shadowed = self.shadowed view.zeroLineVisible = self.showZeroLine view.setZeroLineDefinition(self.zeroLineDefinition) view.centeredAtZeroLine = self.centeredAtZeroLine view.lowerGraphColor = self.lowerGraphColor if self.highlightDefinitions.count > 0 { view.highlightRangeVisible = true view.highlightRangeDefinition = self.highlightDefinitions } if let gridLines = self.gridLines { view.setGridLineDefinition(gridLines) } else { view.gridLinesVisible = false } if let pf = self.primaryFill { view.primaryFill = pf } if let sf = self.secondaryFill { view.secondaryFill = sf } return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineStackLineGraphView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineStackLineGraphView { return self.makeLineGraph(context) } func updateUIView(_ view: DSFSparklineStackLineGraphView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineStackLineGraphView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineStackLineGraphView { return self.makeLineGraph(context) } func updateNSView(_ view: DSFSparklineStackLineGraphView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineStackLineGraphView.SwiftUI { func updateView(_ view: DSFSparklineStackLineGraphView) { UpdateIfNotEqual(result: &view.graphColor, val: self.graphColor) UpdateIfNotEqual(result: &view.lineWidth, val: self.lineWidth) UpdateIfNotEqual(result: &view.lineShading, val: self.lineShading) UpdateIfNotEqual(result: &view.shadowed, val: self.shadowed) UpdateIfNotEqual(result: &view.lineShading, val: self.lineShading) UpdateIfNotEqual(result: &view.zeroLineVisible, val: self.showZeroLine) view.setZeroLineDefinition(self.zeroLineDefinition) UpdateIfNotEqual(result: &view.centeredAtZeroLine, val: self.centeredAtZeroLine) UpdateIfNotEqual(result: &view.lowerGraphColor, val: self.lowerGraphColor) if self.highlightDefinitions.count > 0 { view.highlightRangeVisible = true view.highlightRangeDefinition = self.highlightDefinitions } else { view.highlightRangeVisible = false view.highlightRangeDefinition = [] } if let pf = self.primaryFill { view.primaryFill = pf } if let sf = self.secondaryFill { view.secondaryFill = sf } UpdateIfNotEqual(result: &view.dataSource, val: self.dataSource) } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineStackLineGraphView.swift ================================================ // // DSFSparklineStackLineGraphView.swift // DSFSparklines // // Created by Darren Ford on 20/12/19. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif /// A stack line sparkline type @objc public class DSFSparklineStackLineGraphView: DSFSparklineZeroLineGraphView { let overlay = DSFSparklineOverlay.Stackline() /// The width for the line drawn on the graph @objc public dynamic var lineWidth: CGFloat = 1 { didSet { self.updateDisplay() } } /// Shade the area under the line @objc public dynamic var lineShading: Bool = true { didSet { self.overlay.primaryFill = DSFSparkline.Fill.Color(self.graphColor.withAlphaComponent(0.3).cgColor) self.updateDisplay() } } /// Draw a shadow under the line @objc public dynamic var shadowed: Bool = false { didSet { self.overlay.shadow = self.shadowed ? NSShadow.sparklineDefault : nil } } /// Should the graph be centered at the zero line? @objc public dynamic var centeredAtZeroLine: Bool = false { didSet { self.overlay.centeredAtZeroLine = self.centeredAtZeroLine } } /// The primary fill (for the area of the graph ABOVE the zero line) @objc public dynamic var primaryFill: (any DSFSparklineFillable)? { get { self.overlay.primaryFill } set { self.overlay.primaryFill = newValue } } /// The secondary fill (for the area of the graph UNDER the zero line) @objc public dynamic var secondaryFill: (any DSFSparklineFillable)? { get { self.overlay.secondaryFill } set { self.overlay.secondaryFill = newValue } } internal var gradient: CGGradient? internal var lowerGradient: CGGradient? override public init(frame: CGRect) { super.init(frame: frame) self.configure() } public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } func configure() { self.addOverlay(self.overlay) self.overlay.setNeedsDisplay() } override public func colorDidChange() { super.colorDidChange() // Update the gradients to match the color change self.gradient = CGGradient( colorsSpace: nil, colors: [self.graphColor.withAlphaComponent(0.4).cgColor, self.graphColor.withAlphaComponent(0.2).cgColor] as CFArray, locations: [1.0, 0.0] )! let lc = self.lowerColor self.lowerGradient = CGGradient( colorsSpace: nil, colors: [lc.withAlphaComponent(0.4).cgColor, lc.withAlphaComponent(0.2).cgColor] as CFArray, locations: [1.0, 0.0] )! self.overlay.strokeWidth = self.lineWidth self.overlay.primaryStrokeColor = self.graphColor.cgColor self.overlay.secondaryStrokeColor = self.lowerColor.cgColor self.overlay.centeredAtZeroLine = self.centeredAtZeroLine // Backwards compatibility let color = self.graphColor let fill = DSFSparkline.Fill.Gradient(colors: [ color.withAlphaComponent(0.4).cgColor, color.withAlphaComponent(0.2).cgColor, ]) self.overlay.primaryFill = fill if let lowerColor = self.lowerGraphColor { let fill = DSFSparkline.Fill.Gradient(colors: [ lowerColor.withAlphaComponent(0.4).cgColor, lowerColor.withAlphaComponent(0.2).cgColor, ]) self.overlay.secondaryFill = fill } else { // Fallback - if secondary fill not defined the compatibility view is to use the primary fill view self.overlay.secondaryFill = self.overlay.primaryFill } } } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineStripesGraphView+SwiftUI.swift ================================================ // // DSFSparklineDataBarGraphView+SwiftUI.swift // DSFSparklines // // Created by Darren Ford on 12/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI @available(macOS 11, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineStripesGraphView { struct SwiftUI { /// Datasource for the graph let dataSource: DSFSparkline.DataSource let barSpacing: UInt let integral: Bool let gradient: DSFSparkline.GradientBucket static public let shared = DSFSparkline.GradientBucket(posts: [ DSFSparkline.GradientBucket.Post(color: CGColor(red: 1, green: 0, blue: 0, alpha: 1), location: 0), DSFSparkline.GradientBucket.Post(color: CGColor(red: 0, green: 0, blue: 1, alpha: 1), location: 1) ]) /// Create a databar graph /// - Parameters: /// - dataSource: The data source for the graph /// - maximumTotalValue: The maximum _total_ value. If the datasource values total is greater than this value, it clips the display /// - palette: The color palette to use when drawing the graph /// - unsetColor: (optional) the color to use when drawing the background (useful when the maximumValue is also set) /// - strokeColor: The color to draw the separator lines between data points /// - lineWidth: The width of the separator lines /// - animated: If set, animates any datasource value changes /// - animationDuration: The duration for the animate-in animation public init(dataSource: DSFSparkline.DataSource, integral: Bool = false, barSpacing: UInt = 0, gradient: DSFSparkline.GradientBucket = Self.shared) { self.dataSource = dataSource self.integral = integral self.barSpacing = barSpacing self.gradient = gradient } } } @available(macOS 11, iOS 13.0, tvOS 13.0, *) extension DSFSparklineStripesGraphView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineStripesGraphView #else public typealias UIViewType = DSFSparklineStripesGraphView #endif public class Coordinator: NSObject { let parent: DSFSparklineStripesGraphView.SwiftUI init(_ sparkline: DSFSparklineStripesGraphView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } func makeStripesGraph(_: Context) -> DSFSparklineStripesGraphView { let view = DSFSparklineStripesGraphView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.barSpacing = self.barSpacing view.integral = self.integral view.gradient = self.gradient return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineStripesGraphView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineStripesGraphView { return self.makeStripesGraph(context) } func updateUIView(_ view: DSFSparklineStripesGraphView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 11, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineStripesGraphView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineStripesGraphView { return self.makeStripesGraph(context) } func updateNSView(_ view: DSFSparklineStripesGraphView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 11, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineStripesGraphView.SwiftUI { func updateView(_ view: DSFSparklineStripesGraphView) { UpdateIfNotEqual(result: &view.barSpacing, val: self.barSpacing) UpdateIfNotEqual(result: &view.integral, val: self.integral) view.gradient = self.gradient UpdateIfNotEqual(result: &view.dataSource, val: self.dataSource) } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineStripesGraphView.swift ================================================ // // DSFSparklineStripesGraphView.swift // DSFSparklines // // Created by Darren Ford on 15/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif /// A sparkline graph that displays solid color bars with a gradient (like the climate graph) public class DSFSparklineStripesGraphView: DSFSparklineDataSourceView { let overlay = DSFSparklineOverlay.Stripes() @objc public dynamic var integral: Bool = true { didSet { self.colorDidChange() self.updateDisplay() } } /// The spacing (in pixels) between each bar @objc public dynamic var barSpacing: UInt = 0 { didSet { self.colorDidChange() self.updateDisplay() } } /// The color gradient to use when rendering. /// /// Note that transparent gradients display strangely and not as I would expect them to. /// Stick with solid colors in your gradient for the current time. @objc public var gradient: DSFSparkline.GradientBucket? { didSet { self.colorDidChange() self.updateDisplay() } } public override init(frame: CGRect) { super.init(frame: frame) self.configure() } public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } func configure() { self.addOverlay(self.overlay) self.colorDidChange() self.overlay.setNeedsDisplay() } override func colorDidChange() { super.colorDidChange() self.overlay.integral = self.integral self.overlay.barSpacing = self.barSpacing self.overlay.gradient = self.gradient?.copyGradientBucket() ?? DSFSparklineOverlay.Stripes.defaultGradient } } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineTabletGraphView+SwiftUI.swift ================================================ // // DSFSparklineZeroLineDefinition.swift // DSFSparklines // // Created by Darren Ford on 25/01/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI /// A win-loss sparkline. DataSource values > 0 represent a 'win', whereas <= 0 represent a loss. @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineTabletGraphView { struct SwiftUI { /// Datasource for the graph let dataSource: DSFSparkline.DataSource /// The 'win' color for the sparkline let winColor: DSFColor /// The 'loss' color for the sparkline let lossColor: DSFColor /// The line width (in pixels) to use when drawing the border of each tablet let lineWidth: CGFloat /// The spacing (in pixels) between each tablet let barSpacing: CGFloat /// Create a bar graph sparkline /// - Parameters: /// - dataSource: The data source for the graph /// - winColor: The 'win' color for the sparkline /// - lossColor: The 'loss' color for the sparkline /// - lineWidth: The width of the line around each tablet /// - barSpacing: The spacing between the tablets public init(dataSource: DSFSparkline.DataSource, winColor: DSFColor = .systemGreen, lossColor: DSFColor = .systemRed, lineWidth: CGFloat = 1, barSpacing: CGFloat = 1) { self.dataSource = dataSource self.winColor = winColor self.lossColor = lossColor self.lineWidth = lineWidth self.barSpacing = barSpacing } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineTabletGraphView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineTabletGraphView #else public typealias UIViewType = DSFSparklineTabletGraphView #endif public class Coordinator: NSObject { let parent: DSFSparklineTabletGraphView.SwiftUI init(_ sparkline: DSFSparklineTabletGraphView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } private func makeTabletGraph(_: Context) -> DSFSparklineTabletGraphView { let view = DSFSparklineTabletGraphView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.dataSource = self.dataSource view.winColor = self.winColor view.lossColor = self.lossColor view.barSpacing = self.barSpacing view.lineWidth = self.lineWidth return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineTabletGraphView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineTabletGraphView { return self.makeTabletGraph(context) } func updateUIView(_ view: DSFSparklineTabletGraphView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineTabletGraphView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineTabletGraphView { return self.makeTabletGraph(context) } func updateNSView(_ view: DSFSparklineTabletGraphView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineTabletGraphView.SwiftUI { func updateView(_ view: DSFSparklineTabletGraphView) { UpdateIfNotEqual(result: &view.winColor, val: self.winColor) UpdateIfNotEqual(result: &view.lossColor, val: self.lossColor) UpdateIfNotEqual(result: &view.lineWidth, val: self.lineWidth) UpdateIfNotEqual(result: &view.barSpacing, val: self.barSpacing) UpdateIfNotEqual(result: &view.dataSource, val: self.dataSource) } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineTabletGraphView.swift ================================================ // // DSFSparklineTabletGraphView.swift // DSFSparklines // // Created by Darren Ford on 25/01/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif /// A tablet-style sparkline. Similar to win/loss except rendering as a row of filled/unfilled circles @objc public class DSFSparklineTabletGraphView: DSFSparklineDataSourceView { let overlay = DSFSparklineOverlay.Tablet() static let clear: CGColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 0])! /// The width of the stroke for the tablet @objc public dynamic var lineWidth: CGFloat = 1.0 { didSet { self.updateDisplay() } } /// The spacing (in pixels) between each bar @objc public dynamic var barSpacing: CGFloat = 1.0 { didSet { self.updateDisplay() } } #if os(macOS) /// The color to draw the 'win' boxes @objc public dynamic var winColor: NSColor = NSColor.systemGreen { didSet { self.colorDidChange() } } /// The color to draw the 'loss' boxes @objc public dynamic var lossColor: NSColor = NSColor.systemRed { didSet { self.colorDidChange() } } #else /// The color to draw the 'win' boxes @objc public dynamic var winColor: UIColor = UIColor.systemGreen { didSet { self.colorDidChange() } } /// The color to draw the 'loss' boxes @objc public dynamic var lossColor: UIColor = UIColor.systemRed { didSet { self.colorDidChange() } } #endif public override init(frame: CGRect) { super.init(frame: frame) self.configure() } public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } func configure() { self.addOverlay(self.overlay) self.overlay.setNeedsDisplay() } override func colorDidChange() { super.colorDidChange() self.overlay.lineWidth = self.lineWidth self.overlay.tabletSpacing = self.barSpacing // backwards compatible naming here self.overlay.winFill = DSFSparkline.Fill.Color(self.winColor.withAlphaComponent(0.3).cgColor) self.overlay.winStrokeColor = self.winColor.cgColor self.overlay.lossStrokeColor = self.lossColor.cgColor } } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineWinLossGraphView+SwiftUI.swift ================================================ // // DSFSparklineWinLossGraphView+SwiftUI.swift // DSFSparklines // // Created by Darren Ford on 7/12/20. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI /// A win-loss sparkline. DataSource values > 0 represent a 'win', whereas <= 0 represent a loss. @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineWinLossGraphView { struct SwiftUI { /// Datasource for the graph let dataSource: DSFSparkline.DataSource /// The 'win' color for the sparkline let winColor: DSFColor /// The 'loss' color for the sparkline let lossColor: DSFColor /// The 'tie' color for the sparkline. If nil, tie values are not drawn let tieColor: DSFColor? /// The line width (in pixels) to use when drawing the border of each bar let lineWidth: UInt /// The spacing (in pixels) between each bar let barSpacing: UInt /// Draw a dotted line at the zero point on the y-axis let showZeroLine: Bool /// The drawing definition for the zero line point let zeroLineDefinition: DSFSparkline.ZeroLineDefinition /// Create a bar graph sparkline /// - Parameters: /// - dataSource: The data source for the graph /// - winColor: The 'win' color for the sparkline /// - lossColor: The 'loss' color for the sparkline /// - tieColor: The 'tie' color for the sparkline /// - lineWidth: The width of the line around each bar /// - barSpacing: The spacing between the bars /// - showZeroLine: Show or hide a 'zero line' horizontal line /// - zeroLineDefinition: the settings for drawing the zero line public init(dataSource: DSFSparkline.DataSource, winColor: DSFColor = .systemGreen, lossColor: DSFColor = .systemRed, tieColor: DSFColor? = nil, lineWidth: UInt = 1, barSpacing: UInt = 1, showZeroLine: Bool = false, zeroLineDefinition: DSFSparkline.ZeroLineDefinition = .shared) { self.dataSource = dataSource self.winColor = winColor self.lossColor = lossColor self.tieColor = tieColor self.lineWidth = lineWidth self.barSpacing = barSpacing self.showZeroLine = showZeroLine self.zeroLineDefinition = zeroLineDefinition } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineWinLossGraphView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineWinLossGraphView #else public typealias UIViewType = DSFSparklineWinLossGraphView #endif public class Coordinator: NSObject { let parent: DSFSparklineWinLossGraphView.SwiftUI init(_ sparkline: DSFSparklineWinLossGraphView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } private func makeWinLossGraph(_: Context) -> DSFSparklineWinLossGraphView { let view = DSFSparklineWinLossGraphView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.dataSource = self.dataSource view.winColor = self.winColor view.lossColor = self.lossColor view.tieColor = self.tieColor view.barSpacing = self.barSpacing view.lineWidth = self.lineWidth view.centerlineColor = self.showZeroLine ? self.zeroLineDefinition.color : nil view.centerlineWidth = self.zeroLineDefinition.lineWidth view.centerlineDashStyle = self.zeroLineDefinition.lineDashStyle return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineWinLossGraphView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineWinLossGraphView { return self.makeWinLossGraph(context) } func updateUIView(_ view: DSFSparklineWinLossGraphView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineWinLossGraphView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineWinLossGraphView { return self.makeWinLossGraph(context) } func updateNSView(_ view: DSFSparklineWinLossGraphView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineWinLossGraphView.SwiftUI { func updateView(_ view: DSFSparklineWinLossGraphView) { UpdateIfNotEqual(result: &view.winColor, val: self.winColor) UpdateIfNotEqual(result: &view.lossColor, val: self.lossColor) UpdateIfNotEqual(result: &view.tieColor, val: self.tieColor) UpdateIfNotEqual(result: &view.lineWidth, val: self.lineWidth) UpdateIfNotEqual(result: &view.barSpacing, val: self.barSpacing) UpdateIfNotEqual(result: &view.centerlineColor, val: self.showZeroLine ? self.zeroLineDefinition.color : nil) UpdateIfNotEqual(result: &view.centerlineWidth, val: self.zeroLineDefinition.lineWidth) UpdateIfNotEqual(result: &view.centerlineDashStyle, val: self.zeroLineDefinition.lineDashStyle) UpdateIfNotEqual(result: &view.dataSource, val: self.dataSource) } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineWinLossGraphView.swift ================================================ // // DSFSparklineWinLossGraphView.swift // DSFSparklines // // Created by Darren Ford on 20/12/19. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif @objc public class DSFSparklineWinLossGraphView: DSFSparklineDataSourceView { let overlay = DSFSparklineOverlay.WinLossTie() /// The line width (in pixels) to use when drawing the border of each bar @objc public dynamic var lineWidth: UInt = 1 { didSet { self.updateDisplay() } } /// The spacing (in pixels) between each bar @objc public dynamic var barSpacing: UInt = 1 { didSet { self.updateDisplay() } } #if os(macOS) /// The color to draw the 'win' boxes @objc public dynamic var winColor: NSColor = NSColor.systemGreen { didSet { self.colorDidChange() } } /// The color to draw the 'loss' boxes @objc public dynamic var lossColor: NSColor = NSColor.systemRed { didSet { self.colorDidChange() } } /// The color to draw the 'tie' boxes @objc public dynamic var tieColor: NSColor? = nil { didSet { self.colorDidChange() } } #else /// The color to draw the 'win' boxes @objc public dynamic var winColor: UIColor = UIColor.systemGreen { didSet { self.colorDidChange() } } /// The color to draw the 'loss' boxes @objc public dynamic var lossColor: UIColor = UIColor.systemRed { didSet { self.colorDidChange() } } /// The color to draw the 'tie' boxes @objc public dynamic var tieColor: UIColor? = nil { didSet { self.colorDidChange() } } #endif // MARK: - Centerline Definitions /// The centerline color. If nil, then no centerline is drawn #if os(macOS) @objc public dynamic var centerlineColor: NSColor? = nil { didSet { self.colorDidChange() } } #else @objc public dynamic var centerlineColor: UIColor? = nil { didSet { self.colorDidChange() } } #endif // The width of the zero line @objc public dynamic var centerlineWidth: CGFloat = 1 { didSet { self.colorDidChange() } } /// The pattern for drawing the line var centerlineDashStyle: [CGFloat] = [1,1] /// A string representation of the line dash lengths for the center line, eg. "1,3,4,2". /// If you want a solid line, specify "-" /// /// Primarily used for Interface Builder integration @objc public dynamic var centerlineDashStyleString: String = "1,1" { didSet { if self.centerlineDashStyleString == "-" { // Solid line self.centerlineDashStyle = [] } else { let components = self.centerlineDashStyleString.extractCGFloats() if components.count >= 2 { self.centerlineDashStyle = components } else { Swift.print("ERROR: Zero Line Style string format is incompatible (\(self.centerlineDashStyleString) -> \(components))") self.centerlineDashStyle = [] } } self.colorDidChange() } } // MARK: - Initializers public override init(frame: CGRect) { super.init(frame: frame) self.configure() } public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } func configure() { self.addOverlay(self.overlay) self.overlay.setNeedsDisplay() } override func colorDidChange() { super.colorDidChange() self.overlay.lineWidth = self.lineWidth self.overlay.barSpacing = self.barSpacing self.overlay.winFill = DSFSparkline.Fill.Color(self.winColor.withAlphaComponent(0.3).cgColor) self.overlay.winStroke = self.winColor.cgColor self.overlay.lossFill = DSFSparkline.Fill.Color(self.lossColor.withAlphaComponent(0.3).cgColor) self.overlay.lossStroke = self.lossColor.cgColor self.overlay.tieStroke = self.tieColor?.cgColor if let tc = self.tieColor { self.overlay.tieFill = DSFSparkline.Fill.Color(tc.withAlphaComponent(0.3).cgColor) } if let color = self.centerlineColor { self.overlay.centerLine = .init(color: color, lineWidth: self.centerlineWidth, lineDashStyle: self.centerlineDashStyle) } else { self.overlay.centerLine = nil } self.updateDisplay() } } ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineWiperGaugeGraphView+SwiftUI.swift ================================================ // // DSFSparklineWiperGaugeGraphView+SwiftUI.swift // // Copyright © 2023 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if canImport(SwiftUI) import SwiftUI @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineWiperGaugeGraphView { /// The SwiftUI percent bar graph struct SwiftUI { /// Palette to use when coloring the chart let valueColor: DSFSparkline.ValueBasedFill /// The 'background' color for the value let valueBackgroundColor: DSFColor? /// The color of the upper arc for the control let upperArcColor: DSFColor? /// The color of the pointer let pointerColor: DSFColor? /// The background color for the gauge let backgroundColor: DSFColor? /// The value to display in the chart let value: Double /// Should changes to value be animated? let animationStyle: DSFSparkline.AnimationStyle? /// Create a sparkline graph that displays a 0 ... 1 value as a gauge public init( valueColor: DSFSparkline.ValueBasedFill, value: Double, valueBackgroundColor: DSFColor? = nil, upperArcColor: DSFColor? = nil, pointerColor: DSFColor? = nil, backgroundColor: DSFColor? = nil, animationStyle: DSFSparkline.AnimationStyle? = nil ) { self.valueColor = valueColor self.valueBackgroundColor = valueBackgroundColor self.upperArcColor = upperArcColor self.pointerColor = pointerColor self.backgroundColor = backgroundColor self.value = value self.animationStyle = animationStyle } /// Create a sparkline graph that displays a 0 ... 1 value as a gauge public init( valueColor: DSFColor, value: Double, valueBackgroundColor: DSFColor? = nil, upperArcColor: DSFColor? = nil, pointerColor: DSFColor? = nil, backgroundColor: DSFColor? = nil, animationStyle: DSFSparkline.AnimationStyle? = nil ) { self.valueColor = DSFSparkline.ValueBasedFill(flatColor: valueColor.cgColor) self.valueBackgroundColor = valueBackgroundColor self.upperArcColor = upperArcColor self.pointerColor = pointerColor self.backgroundColor = backgroundColor self.value = value self.animationStyle = animationStyle } } } @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) extension DSFSparklineWiperGaugeGraphView.SwiftUI: DSFViewRepresentable { #if os(macOS) public typealias NSViewType = DSFSparklineWiperGaugeGraphView #else public typealias UIViewType = DSFSparklineWiperGaugeGraphView #endif public class Coordinator: NSObject { let parent: DSFSparklineWiperGaugeGraphView.SwiftUI init(_ sparkline: DSFSparklineWiperGaugeGraphView.SwiftUI) { self.parent = sparkline } } public func makeCoordinator() -> Coordinator { Coordinator(self) } func makeWiperGauge(_: Context) -> DSFSparklineWiperGaugeGraphView { let view = DSFSparklineWiperGaugeGraphView(frame: .zero) self.updateView(view) return view } } // MARK: - iOS/tvOS Specific @available(iOS 13.0, tvOS 13.0, macOS 9999.0, *) public extension DSFSparklineWiperGaugeGraphView.SwiftUI { func makeUIView(context: Context) -> DSFSparklineWiperGaugeGraphView { return self.makeWiperGauge(context) } func updateUIView(_ view: DSFSparklineWiperGaugeGraphView, context _: Context) { self.updateView(view) } } // MARK: - macOS Specific @available(macOS 10.15, iOS 9999.0, tvOS 9999.0, *) public extension DSFSparklineWiperGaugeGraphView.SwiftUI { func makeNSView(context: Context) -> DSFSparklineWiperGaugeGraphView { return self.makeWiperGauge(context) } func updateNSView(_ view: DSFSparklineWiperGaugeGraphView, context _: Context) { self.updateView(view) } } // MARK: - Common updates @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public extension DSFSparklineWiperGaugeGraphView.SwiftUI { func updateView(_ view: DSFSparklineWiperGaugeGraphView) { view.valueColor = self.valueColor if let c = self.valueBackgroundColor { view.valueBackgroundColor = c } if let c = self.upperArcColor { view.gaugeUpperArcColor = c } if let c = self.pointerColor { view.gaugePointerColor = c } if view.animationStyle != self.animationStyle { view.animationStyle = self.animationStyle } view.value = CGFloat(self.value) } } #endif ================================================ FILE: Sources/DSFSparkline/prebuilt/DSFSparklineWiperGaugeGraphView.swift ================================================ // // DSFSparklineWiperGaugeGraphView.swift // // Copyright © 2023 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // #if os(macOS) import AppKit #else import UIKit #endif /// A sparkline that draws a percent bar @objc public class DSFSparklineWiperGaugeGraphView: DSFSparklineSurfaceView { // MARK: - Value /// The initial value to display in the percent bar @objc public dynamic var value: CGFloat = 0.2 { didSet { self.overlay.value = self.value } } @objc public var valueColor = DSFSparkline.ValueBasedFill.sharedPalette { didSet { self.overlay.valueColor = valueColor } } @objc public var animationStyle: DSFSparkline.AnimationStyle? = nil { didSet { self.overlay.animationStyle = animationStyle } } #if os(macOS) @objc public dynamic var gaugeUpperArcColor: NSColor = .textColor { didSet { self.overlay.gaugeUpperArcColor = gaugeUpperArcColor.cgColor } } @objc public dynamic var valueBackgroundColor: NSColor = .quaternaryLabelColor { didSet { self.overlay.valueBackgroundColor = valueBackgroundColor.cgColor } } /// The color of the pointer component of the gauge @objc public dynamic var gaugePointerColor: NSColor = .textColor { didSet { self.overlay.gaugePointerColor = gaugePointerColor.cgColor } } /// The color of the pointer component of the gauge @objc public dynamic var gaugeBackgroundColor: NSColor? = nil { didSet { self.overlay.gaugeBackgroundColor = gaugeBackgroundColor?.cgColor } } #else @objc public dynamic var gaugeUpperArcColor: UIColor = .label { didSet { self.overlay.gaugeUpperArcColor = gaugeUpperArcColor.cgColor } } @objc public dynamic var valueBackgroundColor: UIColor = .quaternaryLabel { didSet { self.overlay.valueBackgroundColor = valueBackgroundColor.cgColor } } /// The color of the pointer component of the gauge @objc public dynamic var gaugePointerColor: UIColor = .label { didSet { self.overlay.gaugePointerColor = gaugePointerColor.cgColor } } /// The color of the pointer component of the gauge @objc public dynamic var gaugeBackgroundColor: UIColor? = nil { didSet { self.overlay.gaugeBackgroundColor = gaugeBackgroundColor?.cgColor } } #endif // MARK: - Control /// Initializer public override init(frame: CGRect) { super.init(frame: frame) self.configure() } /// Initializer public required init?(coder: NSCoder) { super.init(coder: coder) self.configure() } // The overlay private let overlay = DSFSparklineOverlay.WiperGauge() } extension DSFSparklineWiperGaugeGraphView { #if os(macOS) public override func updateLayer() { // Captured to handle dark/light mode changes super.updateLayer() self.updateColors() } #endif } private extension DSFSparklineWiperGaugeGraphView { func configure() { self.addOverlay(self.overlay) self.overlay.value = self.value self.overlay.animationStyle = animationStyle self.updateColors() } func updateColors() { self.overlay.valueColor = self.valueColor self.overlay.valueBackgroundColor = valueBackgroundColor.cgColor self.overlay.gaugePointerColor = gaugePointerColor.cgColor self.overlay.gaugeUpperArcColor = gaugeUpperArcColor.cgColor self.overlay.gaugeBackgroundColor = gaugeBackgroundColor?.cgColor } } ================================================ FILE: Sources/DSFSparkline/prebuilt/ui/DSFSparklineDataSourceView.swift ================================================ // // DSFSparklineDataSourceView.swift // DSFSparklines // // Created by Darren Ford on 16/12/19. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif /// A Sparkline View with an attached datasource. @objc public class DSFSparklineDataSourceView: DSFSparklineSurfaceView { // Listen for changes in the data and update appropriately private var dataObserver: NSObjectProtocol? /// The source of data for the sparkline @objc weak public var dataSource: DSFSparkline.DataSource? { didSet { if self.windowSizeSetInXib { self.dataSource?.windowSize = self.graphWindowSize } self.updateDataObserver() self.rootLayer.sublayers?.forEach { layer in if let l = layer as? DSFSparklineOverlay.DataSource { l.dataSource = self.dataSource } } self.updateDisplay() } } deinit { self.dataObserver = nil } /// The primary color for the sparkline #if os(macOS) @objc public dynamic var graphColor: NSColor = NSColor.black { didSet { self.colorDidChange() } } #else @objc public dynamic var graphColor: UIColor = UIColor.black { didSet { self.colorDidChange() } } #endif #if os(macOS) /// Force an update when the view moves to the window public override func viewWillMove(toSuperview newSuperview: DSFView?) { super.viewWillMove(toSuperview: newSuperview) self.colorDidChange() } #endif /// The size of the sparkline window /// /// This member is purely IBDesignable display related /// `windowSize` on the dataSource @objc public dynamic var graphWindowSize: UInt = 20 { didSet { self.windowSizeSetInXib = true self.dataSource?.windowSize = self.graphWindowSize self.updateDisplay() } } private var windowSizeSetInXib = false } extension DSFSparklineDataSourceView { private func updateDataObserver() { self.dataObserver = nil if self.dataSource != nil { self.dataObserver = NotificationCenter.default.addObserver( forName: DSFSparkline.DataSource.DataChangedNotification, object: self.dataSource!, queue: nil, using: { [weak self] (notification) in self?.updateDisplay() }) } } /// Override in inherited classes to be notified when the color changes @objc func colorDidChange() { } #if os(macOS) override public func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) } #else public override func draw(_ rect: CGRect) { super.draw(rect) } #endif } ================================================ FILE: Sources/DSFSparkline/prebuilt/ui/DSFSparklineZeroLinedGraphView.swift ================================================ // // DSFSparklineZeroLinedGraphView.swift // DSFSparklines // // Created by Darren Ford on 20/12/19. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import SwiftUI #if os(macOS) import Cocoa #else import UIKit #endif /// A view that can draw a zero-point line. Should never be used directly, just to inherit from for other graph types @objc public class DSFSparklineZeroLineGraphView: DSFSparklineDataSourceView { // MARK: Zero-line display // The zero-line overlay let zerolineOverlay = DSFSparklineOverlay.ZeroLine() /// Draw a dotted line at the zero point on the y-axis @objc public dynamic var zeroLineVisible: Bool = false { didSet { self.updateZeroLineSettings() } } /// The color of the dotted line at the zero point on the y-axis #if os(macOS) @objc public dynamic var zeroLineColor = NSColor.gray { didSet { self.updateZeroLineSettings() } } #else @objc public dynamic var zeroLineColor: UIColor = .systemGray { didSet { self.updateZeroLineSettings() } } #endif /// The width of the dotted line at the zero point on the y-axis @objc public dynamic var zeroLineWidth: CGFloat = 1.0 { didSet { self.updateZeroLineSettings() } } /// The line style for the dotted line. Use [] to specify a solid line. @objc public var zeroLineDashStyle: [CGFloat] = [1.0, 1.0] { didSet { self.updateZeroLineSettings() } } /// A string representation of the line dash lengths for the zero line, eg. "1,3,4,2". If you want a solid line, specify "-" /// /// Primarily used for Interface Builder integration @objc public dynamic var zeroLineDashStyleString: String = "1,1" { didSet { self.handleZeroLineString() } } // MARK: Zero-line centering /// The color used to draw values below the zero line. If nil, is the same as the graph color @objc public dynamic var lowerGraphColor: DSFColor? { didSet { self.colorDidChange() } } /// The 'lowerColor' represents the 'negativeColor' if it is set, otherwise its the same as the graphColor internal var lowerColor: DSFColor { return self.lowerGraphColor ?? self.graphColor } // MARK: - Highlight ranges // A fixed interface-builder defined highlight let ibHighlightOverlay = DSFSparklineOverlay.RangeHighlight() // An array of additional highlights var highlightOverlay: [DSFSparklineOverlay.RangeHighlight] = [] /// Draw a highlight for a range on the graph @objc public dynamic var highlightRangeVisible: Bool = false { didSet { self.updateHighlightSettings() } } /// The color of the highlight to be used #if os(macOS) @objc public dynamic var highlightRangeColor = NSColor.gray { didSet { self.updateHighlightSettings() } } #else @objc public dynamic var highlightRangeColor: UIColor = .systemGray { didSet { self.updateHighlightSettings() } } #endif /// A string of the format "0.1,0.7" @objc public dynamic var highlightRangeString: String? = nil { didSet { self.updateHighlightSettings() } } // MARK: - Grid lines support // A fixed interface-builder defined highlight let ibGridLinesOverlay = DSFSparklineOverlay.GridLines() /// Draw a dotted line at the zero point on the y-axis @objc public dynamic var gridLinesVisible: Bool = false { didSet { self.updateGridLinesSettings() } } /// The y-values on the graph with a grid line @objc public var gridLinesValues: [CGFloat] = [] { didSet { self.updateGridLinesSettings() } } /// A string of the format "0.1,0.7" @objc public dynamic var gridLinesValuesString: String? = nil { didSet { // Dash style let floats = self.gridLinesValuesString?.extractCGFloats() ?? [] self.gridLinesValues = floats } } #if os(macOS) @objc public dynamic var gridLinesColor = NSColor.systemGray.withAlphaComponent(0.5) { didSet { self.updateGridLinesSettings() } } #else @objc public dynamic var gridLinesColor: UIColor = .systemGray.withAlphaComponent(0.5) { didSet { self.updateGridLinesSettings() } } #endif /// The width of the dotted line at the zero point on the y-axis @objc public dynamic var gridLinesWidth: CGFloat = 1.0 { didSet { self.updateGridLinesSettings() } } /// The line style for the dotted line. Use [] to specify a solid line. @objc public dynamic var gridLinesDashStyle: [CGFloat] = [1.0, 1.0] { didSet { self.updateGridLinesSettings() } } /// A string representation of the line dash lengths for grid lines, eg. "1,3,4,2". If you want a solid line, specify "-" /// /// Primarily used for Interface Builder integration @objc public dynamic var gridLinesDashStyleString: String = "1.0,1.0" { didSet { self.updateGridLinesSettings() } } /// Set the grid line definition for the graph @objc public func setGridLineDefinition(_ gridLineDefinition: DSFSparkline.GridLinesDefinition) { self.gridLinesVisible = true self.gridLinesColor = gridLineDefinition.color self.gridLinesWidth = gridLineDefinition.width self.gridLinesDashStyle = gridLineDefinition.dashStyle self.gridLinesValues = gridLineDefinition.values } // MARK: - Init public override init(frame: CGRect) { super.init(frame: frame) self.setup() } public required init?(coder: NSCoder) { super.init(coder: coder) self.setup() } private func setup() { // The Zero line overlay self.addOverlay(self.zerolineOverlay) self.zerolineOverlay.zPosition = -5 self.updateZeroLineSettings() // Highlight self.addOverlay(self.ibHighlightOverlay) self.ibHighlightOverlay.zPosition = -10 self.ibHighlightOverlay.dataSource = self.dataSource self.updateHighlightSettings() // Grid lines self.addOverlay(self.ibGridLinesOverlay) self.ibGridLinesOverlay.isHidden = self.gridLinesVisible self.ibGridLinesOverlay.zPosition = -10 self.ibGridLinesOverlay.dataSource = self.dataSource self.updateGridLinesSettings() } @objc public var highlightRangeDefinition: [DSFSparkline.HighlightRangeDefinition] = [] { willSet { self.highlightOverlay.forEach { $0.removeFromSuperlayer() } self.highlightOverlay = [] } didSet { self.highlightRangeDefinition.forEach { r in let item = DSFSparklineOverlay.RangeHighlight() item.highlightRange = r.range item.dataSource = self.dataSource item.fill = r.fill item.zPosition = -10 self.addOverlay(item) self.highlightOverlay.append(item) } self.updateDisplay() } } } public extension DSFSparklineZeroLineGraphView { /// Configure the zero line using the ZeroLineDefinition func setZeroLineDefinition(_ definition: DSFSparkline.ZeroLineDefinition) { self.zeroLineWidth = definition.lineWidth self.zeroLineColor = definition.color self.zeroLineDashStyle = definition.lineDashStyle } } private extension DSFSparklineZeroLineGraphView { func updateGridLinesSettings() { self.ibGridLinesOverlay.isHidden = !self.gridLinesVisible self.ibGridLinesOverlay.strokeWidth = self.gridLinesWidth self.ibGridLinesOverlay.strokeColor = self.gridLinesColor.cgColor self.ibGridLinesOverlay.dashStyle = self.gridLinesDashStyle self.ibGridLinesOverlay.floatValues = self.gridLinesValues self.updateDisplay() } /// private func handleZeroLineString() { if self.zeroLineDashStyleString == "-" { // Solid line self.zeroLineDashStyle = [] } else { let components = self.zeroLineDashStyleString.extractCGFloats() if components.count >= 2 { self.zeroLineDashStyle = components } else { Swift.print("ERROR: Zero Line Style string format is incompatible (\(self.zeroLineDashStyleString) -> \(components))") } } } private func updateZeroLineSettings() { self.zerolineOverlay.isHidden = !self.zeroLineVisible self.zerolineOverlay.strokeColor = self.zeroLineColor.cgColor self.zerolineOverlay.strokeWidth = self.zeroLineWidth self.zerolineOverlay.dashStyle = self.zeroLineDashStyle self.updateDisplay() } private func updateHighlightSettings() { self.ibHighlightOverlay.isHidden = !self.highlightRangeVisible self.ibHighlightOverlay.fill = DSFSparkline.Fill.Color(self.highlightRangeColor.cgColor) let floats = self.highlightRangeString?.extractCGFloats() ?? [] if floats.count == 2, floats[0] < floats[1] { self.ibHighlightOverlay.highlightRange = floats[0] ..< floats[1] } else if floats.count == 0 { // No ranges. This is fine self.highlightRangeDefinition = [] } else { self.highlightRangeDefinition = [] Swift.print("ERROR: Highlight range string format is incompatible (\(self.zeroLineDashStyleString) -> \(floats))") } self.updateDisplay() } } ================================================ FILE: Sources/DSFSparkline/util/Angle.swift ================================================ // // Copyright © 2024 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation /// An angle /// /// An angle value represents either a radians or a degrees angle, with functions to easily convert between the two public enum Angle { /// Radians angle value case radians(T) /// Degrees angle value case degrees(T) /// The radians value for the angle @inlinable public var radians: T { switch self { case let .radians(value): return value case let .degrees(value): return value * T.pi / 180.0 } } /// The degrees value for the angle @inlinable public var degrees: T { switch self { case let .radians(value): return value * 180.0 / T.pi case let .degrees(value): return value } } /// Return a guaranteed radians angle representation @inlinable public var asRadians: Angle { .radians(self.radians) } /// Return a guaranteed degrees angle representation @inlinable public var asDegrees: Angle { .degrees(self.degrees) } /// Add two angle values @inlinable public static func +(_ left: Angle, _ right: Angle) -> Angle { .radians(left.radians + right.radians) } /// Subtract two angle values @inlinable public static func -(_ left: Angle, _ right: Angle) -> Angle { .radians(left.radians - right.radians) } } ================================================ FILE: Sources/DSFSparkline/util/ArbitraryAnimator.swift ================================================ // // ArbitraryAnimator.swift // DSFSparklines // // Created by Darren Ford on 17/2/21. // Copyright © 2021 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import QuartzCore extension DSFSparkline { /// Animation function types @objc public enum AnimatorFunctionType: Int { case linear case easeInEaseOut internal var function: ArbitraryAnimatorFunction { switch self { case .linear: return ArbitraryAnimator.Function.Linear() case .easeInEaseOut: return ArbitraryAnimator.Function.EaseInEaseOut() default: fatalError() } } } /// An animation style @objc public class AnimationStyle: NSObject { @objc public let duration: CGFloat @objc public let function: AnimatorFunctionType @objc public init(duration: CGFloat = 0.25, function: AnimatorFunctionType = .easeInEaseOut) { self.duration = duration self.function = function } } struct AnimationTransition { let start: CGFloat let stop: CGFloat var distance: CGFloat { self.stop - self.start } } } func ==(_ left: DSFSparkline.AnimationStyle, _ right: DSFSparkline.AnimationStyle) -> Bool { left.duration == right.duration && left.function == right.function } func !=(_ left: DSFSparkline.AnimationStyle, _ right: DSFSparkline.AnimationStyle) -> Bool { left.duration != right.duration || left.function != right.function } ////// protocol ArbitraryAnimatorFunction { func evaluate(linearPosition: Double) -> Double } class ArbitraryAnimator { class Function {} #if os(macOS) var displayLink: CVDisplayLink? #else var displayLink: CADisplayLink? #endif var progress: Double = 0.0 var duration: CFTimeInterval = 1 var startTime: CFTimeInterval = 0 var endTime: CFTimeInterval = 0 var animationFunction: ArbitraryAnimatorFunction = Function.Linear() var isActive: Bool = false var progressBlock: ((Double) -> Void)? init() { #if os(macOS) CVDisplayLinkCreateWithActiveCGDisplays(&self.displayLink) let displayLinkOutputCallback: CVDisplayLinkOutputCallback = { (_: CVDisplayLink, _: UnsafePointer, _: UnsafePointer, _: CVOptionFlags, _: UnsafeMutablePointer, displayLinkContext: UnsafeMutableRawPointer?) -> CVReturn in let view = unsafeBitCast(displayLinkContext, to: ArbitraryAnimator.self) view.perform() return CVReturn() } CVDisplayLinkSetOutputCallback( displayLink!, displayLinkOutputCallback, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) ) #else self.displayLink = CADisplayLink(target: self, selector: #selector(self.callback(displaylink:))) #endif } func start() { self.startTime = CACurrentMediaTime() self.endTime = self.startTime + self.duration self.isActive = true #if os(macOS) guard let displayLink = self.displayLink else { fatalError() } CVDisplayLinkStart(displayLink) #else guard let displayLink = self.displayLink else { fatalError() } displayLink.add(to: RunLoop.main, forMode: .default) #endif } func stop() { if !self.isActive { return } self.isActive = false #if os(macOS) guard let link = self.displayLink else { fatalError() } CVDisplayLinkStop(link) #else guard let displayLink = self.displayLink else { fatalError() } displayLink.remove(from: RunLoop.main, forMode: .default) #endif // Force the animation to complete self.progress = 1.0 self.progressBlock?(self.progress) } #if !os(macOS) @objc func callback(displaylink _: CADisplayLink) { self.perform() } #endif func perform() { DispatchQueue.main.async { [weak self] in guard let `self` = self else { return } let current = CACurrentMediaTime() if current >= self.endTime { self.progress = 1.0 self.stop() } else { let linearPosition = (current - self.startTime) / self.duration let evaluatedPosition = self.animationFunction.evaluate(linearPosition: linearPosition) self.progress = evaluatedPosition } self.progressBlock?(self.progress) } } } extension ArbitraryAnimator.Function { class EaseInEaseOut: ArbitraryAnimatorFunction { let firstControlPoint: Double let secondControlPoint: Double init(firstControlPoint: Double = 0, secondControlPoint: Double = 1) { self.firstControlPoint = firstControlPoint self.secondControlPoint = secondControlPoint } func evaluate(linearPosition: Double) -> Double { return 3 * linearPosition * (1 - linearPosition) * (1 - linearPosition) * self.firstControlPoint + 3 * linearPosition * linearPosition * (1 - linearPosition) * self.secondControlPoint + linearPosition * linearPosition * linearPosition * 1.0 } } class Linear: ArbitraryAnimatorFunction { func evaluate(linearPosition: Double) -> Double { return linearPosition } } } ================================================ FILE: Sources/DSFSparkline/util/CGColor+BackwardsCompatibility.swift ================================================ // // Copyright © 2024 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // // Some basic support routines to abstract away differences between OS versions import CoreGraphics import Foundation extension CGColor { /// Creates a color in the Generic gray color space /// - Parameters: /// - gray: A grayscale value (0.0 - 1.0) /// - alpha: An alpha value (0.0 - 1.0) /// - Returns: A color object @inlinable static func gray(_ gray: CGFloat, _ a: CGFloat = 1.0) -> CGColor { if #available(iOS 13, tvOS 13, watchOS 6, *) { return CGColor(gray: gray, alpha: a) } return CGColor(colorSpace: _grayColorSpace, components: [gray, a]) ?? .standard.black } /// Creates a color in the Generic RGB color space /// - Parameters: /// - red: A red component value (0.0 - 1.0) /// - green: A green component value (0.0 - 1.0) /// - blue: A blue component value (0.0 - 1.0) /// - alpha: An alpha value (0.0 - 1.0) /// - Returns: A color object @inlinable static func RGBA(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat, _ alpha: CGFloat = 1.0) -> CGColor { if #available(iOS 13, tvOS 13, watchOS 6, *) { return CGColor(red: red, green: green, blue: blue, alpha: alpha) } return CGColor(colorSpace: _rgbaColorSpace, components: [red, green, blue, alpha]) ?? .standard.black } /// Creates a color in the sRGB color space. /// - Parameters: /// - red: A red component value (0.0 - 1.0) /// - green: A green component value (0.0 - 1.0) /// - blue: A blue component value (0.0 - 1.0) /// - alpha: An alpha value (0.0 - 1.0) /// - Returns: A color object @inlinable static func sRGBA(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat, _ alpha: CGFloat = 1.0) -> CGColor { if #available(macOS 15, iOS 13, tvOS 13, watchOS 6, *) { return CGColor(srgbRed: red, green: green, blue: blue, alpha: alpha) } return CGColor(colorSpace: _sRGBAColorSpace, components: [red, green, blue, alpha]) ?? .standard.black } } // cached colorspaces @usableFromInline internal let _sRGBAColorSpace = CGColorSpace(name: CGColorSpace.sRGB)! @usableFromInline internal let _rgbaColorSpace = CGColorSpace(name: CGColorSpace.genericRGBLinear)! @usableFromInline internal let _grayColorSpace = CGColorSpace(name: CGColorSpace.genericGrayGamma2_2)! // MARK: - Standard CGColors extension CGColor { @usableFromInline internal struct Colors { @usableFromInline internal let white = CGColor.gray(1) @usableFromInline internal let black = CGColor.gray(0) @usableFromInline internal let clear = CGColor.gray(0, 0) @usableFromInline internal let red = CGColor.sRGBA(1, 0, 0) @usableFromInline internal let green = CGColor.sRGBA(0, 1, 0) @usableFromInline internal let blue = CGColor.sRGBA(0, 0, 1) @usableFromInline internal let cyan = CGColor.sRGBA(0, 1, 1) @usableFromInline internal let yellow = CGColor.sRGBA(1, 1, 0) @usableFromInline internal let magenta = CGColor.sRGBA(1, 0, 1) } @usableFromInline internal static var standard: Colors { Colors() } } ================================================ FILE: Sources/DSFSparkline/util/CGContext+extensions.swift ================================================ // // CGContext+extensions.swift // // Copyright © 2024 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics.CGContext extension CGContext { /// Execute the supplied block within a `saveGState() / restoreGState()` pair, providing a context /// to draw in during the execution of the block /// /// - Parameter stateBlock: The block to execute within the new graphics state /// - Parameter context: The context to draw into within the block /// /// Example usage: /// ``` /// context.usingGState { (ctx) in /// ctx.addPath(unsetBackground) /// ctx.setFillColor(bgc1.cgColor) /// ctx.fillPath(using: .evenOdd) /// } /// ``` @inlinable func usingGState(stateBlock: (_ context: CGContext) throws -> Void) rethrows { self.saveGState() defer { self.restoreGState() } try stateBlock(self) } /// Wrap the drawing commands in `block` within a transparency layer @inlinable func usingTransparencyLayer(auxiliaryInfo: CFDictionary? = nil, _ block: () -> Void) { self.beginTransparencyLayer(auxiliaryInfo: auxiliaryInfo) defer { self.endTransparencyLayer() } block() } } ================================================ FILE: Sources/DSFSparkline/util/CGPath+Hermite.swift ================================================ // // CGPath+Hermite.swift // DSFSparklines // // Created by Darren Ford on 20/6/19. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import CoreGraphics import Foundation extension CGMutablePath { @discardableResult func addPathWithPoints(_ points: [CGPoint], smoothed: Bool = false) -> CGPath { if smoothed { return self.addPathByExtrapolatingPoints(points) } else { return self.addPathWithPoints(points) } } private func addPathByExtrapolatingPoints(_ interpolationPoints: [CGPoint]) -> CGPath { self.interpolatePointsWithHermite(interpolationPoints: interpolationPoints) return self } private func addPathWithPoints(_ points: [CGPoint]) -> CGPath { assert(points.count > 1) self.move(to: points.first!) for point in points.dropFirst() { self.addLine(to: point) } return self } } extension CGPath { func fit(verticallyIn rect: CGRect) -> CGPath? { let boundingBox = self.boundingBox let dy = rect.height / boundingBox.height let scaleTransform = CGAffineTransform.identity var tr = scaleTransform.scaledBy(x: 1.0, y: dy) return self.copy(using: &tr) } static func pathWithPoints(_ points: [CGPoint], smoothed: Bool = false) -> CGPath { if smoothed { return CGPath.byExtrapolatingPoints(points) } else { return CGPath.byPathWithPoints(points) } } private static func byExtrapolatingPoints(_ interpolationPoints: [CGPoint]) -> CGPath { let newPath = CGMutablePath() newPath.interpolatePointsWithHermite(interpolationPoints: interpolationPoints) return newPath } private static func byPathWithPoints(_ points: [CGPoint]) -> CGPath { assert(points.count > 1) let path = CGMutablePath() path.move(to: points.first!) for point in points.dropFirst() { path.addLine(to: point) } return path } } extension CGMutablePath { func interpolatePointsWithHermite(interpolationPoints: [CGPoint]) { let n = interpolationPoints.count - 1 for ii in 0 ..< n { var currentPoint = interpolationPoints[ii] if ii == 0 { self.move(to: interpolationPoints[0]) } var nextii = (ii + 1) % interpolationPoints.count var previi = (ii - 1 < 0 ? interpolationPoints.count - 1 : ii - 1) var previousPoint = interpolationPoints[previi] var nextPoint = interpolationPoints[nextii] let endPoint = nextPoint var mx: CGFloat = 0.0 var my: CGFloat = 0.0 if ii > 0 { mx = (nextPoint.x - currentPoint.x) * 0.5 + (currentPoint.x - previousPoint.x) * 0.5 my = (nextPoint.y - currentPoint.y) * 0.5 + (currentPoint.y - previousPoint.y) * 0.5 } else { mx = (nextPoint.x - currentPoint.x) * 0.5 my = (nextPoint.y - currentPoint.y) * 0.5 } let controlPoint1 = CGPoint(x: currentPoint.x + mx / 3.0, y: currentPoint.y + my / 3.0) currentPoint = interpolationPoints[nextii] nextii = (nextii + 1) % interpolationPoints.count previi = ii previousPoint = interpolationPoints[previi] nextPoint = interpolationPoints[nextii] if ii < n - 1 { mx = (nextPoint.x - currentPoint.x) * 0.5 + (currentPoint.x - previousPoint.x) * 0.5 my = (nextPoint.y - currentPoint.y) * 0.5 + (currentPoint.y - previousPoint.y) * 0.5 } else { mx = (currentPoint.x - previousPoint.x) * 0.5 my = (currentPoint.y - previousPoint.y) * 0.5 } let controlPoint2 = CGPoint(x: currentPoint.x - mx / 3.0, y: currentPoint.y - my / 3.0) self.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) } } } extension CGPath { /// Fit a bezier path through all of the points. static func InterpolatePointsUsingBezier(points: [CGPoint], f: CGFloat = 0.3, t: CGFloat = 0.6) -> CGPath? { // See: https://www.geeksforgeeks.org/how-to-draw-smooth-curve-through-multiple-points-using-javascript/ let path = CGMutablePath() guard let first: CGPoint = points.first, points.count > 1 else { return nil } path.move(to: first) let remaining = [CGPoint](points.dropFirst()) let pairs: [(CGPoint, CGPoint?)] = remaining.enumerated().map { point in let next: CGPoint? = point.offset < remaining.count - 1 ? remaining[point.offset + 1] : nil return (point.element, next) } var m: CGFloat = 0 var dx1: CGFloat = 0 var dy1: CGFloat = 0 var preP = first for pair in pairs { let curP = pair.0 let nexP = pair.1 // This is nil for the last point in the graph let dx2: CGFloat let dy2: CGFloat if let next = nexP { m = gradient(preP, next) dx2 = (next.x - curP.x) * -f dy2 = dx2 * m * t } else { dx2 = 0 dy2 = 0 } path.addCurve(to: curP, control1: CGPoint(x: preP.x - dx1, y: preP.y - dy1), control2: CGPoint(x: curP.x + dx2, y: curP.y + dy2)) dx1 = dx2 dy1 = dy2 preP = curP } return path.copy() } } ================================================ FILE: Sources/DSFSparkline/util/CGPath+innerShadow.swift ================================================ // // CGPath+innerShadow.swift // // Copyright © 2024 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import CoreGraphics import Foundation public extension CGContext { /// Draw an inner shadow in the path /// - Parameters: /// - path: The path to apply the inner shadow to /// - shadowColor: Specifies the color of the shadow, which may contain a non-opaque alpha value. If NULL, then shadowing is disabled. /// - offset: Specifies a translation in base-space. /// - blurRadius: A non-negative number specifying the amount of blur. /// /// **Inner Shadows in Quartz: Helftone** /// [Blog Article](https://blog.helftone.com/demystifying-inner-shadows-in-quartz/) /// [(Archived)](https://web.archive.org/web/20221206132428/https://blog.helftone.com/demystifying-inner-shadows-in-quartz/) func drawInnerShadow(in path: CGPath, shadowColor: CGColor?, offset: CGSize, blurRadius: CGFloat) { guard let shadowColor = shadowColor, let opaqueShadowColor = shadowColor.copy(alpha: 1.0) else { return } self.usingGState { ctx in ctx.addPath(path) ctx.clip() ctx.setAlpha(shadowColor.alpha) ctx.usingTransparencyLayer { ctx.setShadow(offset: offset, blur: blurRadius, color: opaqueShadowColor) ctx.setBlendMode(.sourceOut) ctx.setFillColor(opaqueShadowColor) ctx.addPath(path) ctx.fillPath() } } } } ================================================ FILE: Sources/DSFSparkline/util/DSFSparkline+Shadow.swift ================================================ // // CGContext+extensions.swift // // Copyright © 2024 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation #if os(macOS) import AppKit #else import UIKit #endif public extension DSFSparkline { /// A shadow object @objc class Shadow: NSObject { /// The shadow public let shadow: NSShadow /// Is the shadow an inner shadow? public let isInner: Bool /// Create a new shadow object @objc public init(_ shadow: NSShadow, isInner: Bool = false) { self.shadow = shadow self.isInner = isInner super.init() } /// Create a new shadow object @objc convenience public init(blurRadius: CGFloat, offset: CGSize, color: CGColor, isInner: Bool = false) { #if os(macOS) let color: DSFColor = DSFColor(cgColor: color) ?? .black #else let color: DSFColor = DSFColor(cgColor: color) #endif self.init( NSShadow(blurRadius: blurRadius, offset: offset, color: color), isInner: isInner ) } /// Shadow offset @inlinable @objc public var offset: CGSize { get { shadow.shadowOffset } set { shadow.shadowOffset = newValue } } #if os(macOS) /// Shadow color @inlinable @objc public var color: CGColor? { get { shadow.shadowColor?.cgColor } set { shadow.shadowColor = newValue != nil ? NSColor(cgColor: newValue!) : nil } } #else /// Shadow color @inlinable @objc public var color: CGColor? { get { (shadow.shadowColor as? DSFColor)?.cgColor } set { shadow.shadowColor = (newValue != nil) ? UIColor(cgColor: newValue!) : nil } } #endif /// Shadow blur radius @inlinable @objc public var blurRadius: CGFloat { get { shadow.shadowBlurRadius} set { shadow.shadowBlurRadius = newValue } } /// Calculate the amount of inset required to cater for drawing a shadow @inlinable internal var requiredShadowInset: CGFloat { if self.isInner { return 0 } let dx = (abs(self.offset.width) + self.blurRadius) * 2 let dy = (abs(self.offset.height) + self.blurRadius) * 2 return max(dx, dy) } } } ================================================ FILE: Sources/DSFSparkline/util/LayerInvalidating.swift ================================================ // // LayerInvalidating.swift // // Copyright © 2023 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation #if os(macOS) import AppKit #else import UIKit #endif public struct LayerInvalidatingType: OptionSet { public let rawValue: UInt /// Call 'setNeedsDisplay()' on the layer whenever the property value changes public static let display = LayerInvalidatingType(rawValue: 1 << 0) /// Call 'setNeedsLayout()' on the layer whenever the property value changes public static let layout = LayerInvalidatingType(rawValue: 1 << 1) public static let all: LayerInvalidatingType = [.display, .layout] public init(rawValue: UInt) { self.rawValue = rawValue } } /// A property wrapper for CAlayer member properties to force a `setNeedsDisplay()` on the layer /// whenever the property changes @propertyWrapper public struct LayerInvalidating { // Stored value private var valueType: Value // The invalidation types for the property private let invalidationType: LayerInvalidatingType /// Wrapped value public var wrappedValue: Value { get { self.valueType } set { self.valueType = newValue } } /// Initialize with a built-in invalidating type(s). public init(wrappedValue: Value, _ invalidationType: LayerInvalidatingType) { self.valueType = wrappedValue self.invalidationType = invalidationType } public static subscript( _enclosingInstance object: EnclosingSelf, wrapped _: ReferenceWritableKeyPath, storage storageKeyPath: ReferenceWritableKeyPath> ) -> Value { get { return object[keyPath: storageKeyPath].wrappedValue } set { object[keyPath: storageKeyPath].updateLayerInvalidatingPropertyWrapper(newValue, object) } } mutating func updateLayerInvalidatingPropertyWrapper(_ value: Value, _ layer: CALayer) { guard self.wrappedValue != value else { return } // Update the wrapped value self.wrappedValue = value // And trigger the invalidations associated with the propertywrapper if self.invalidationType.contains(.display) { layer.setNeedsDisplay() } if self.invalidationType.contains(.layout) { layer.setNeedsLayout() } } } ================================================ FILE: Sources/DSFSparkline/util/NSShadow+extensions.swift ================================================ // // NSShadow+extensions.swift // DSFSparklines // // Created by Darren Ford on 20/12/19. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics #if os(macOS) import AppKit #else import UIKit #endif public extension NSShadow { @objc convenience init(blurRadius: CGFloat, offset: CGSize, color: DSFColor) { self.init() self.shadowBlurRadius = blurRadius self.shadowOffset = offset self.shadowColor = color } } extension CGContext { @inlinable func setShadow(_ shadow: NSShadow) { #if os(macOS) let color = shadow.shadowColor #else let color = shadow.shadowColor as? UIColor #endif self.setShadow( offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius, color: color?.cgColor) } } // Static definition of the 'default' shadow. private let _NSShadowDefaultValue = NSShadow( blurRadius: 1.0, offset: CGSize(width: 0.5, height: 0.5), color: DSFColor.black.withAlphaComponent(0.3) ) internal extension NSShadow { /// The default shadow @objc static let sparklineDefault = _NSShadowDefaultValue } ================================================ FILE: Sources/DSFSparkline/util/Utilities.swift ================================================ // // Utilities.swift // DSFSparklines // // Created by Darren Ford on 20/12/19. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import Foundation import CoreGraphics.CGContext #if os(macOS) import AppKit public typealias DSFColor = NSColor public typealias DSFView = NSView public typealias DSFFont = NSFont public typealias DSFEdgeInsets = NSEdgeInsets public typealias DSFImage = NSImage #else import UIKit public typealias DSFColor = UIColor public typealias DSFView = UIView public typealias DSFFont = UIFont public typealias DSFEdgeInsets = UIEdgeInsets public typealias DSFImage = UIImage #endif #if canImport(SwiftUI) import SwiftUI #if os(macOS) @available(macOS 10.15, *) typealias DSFViewRepresentable = NSViewRepresentable #else @available(iOS 13.0, tvOS 13.0, *) typealias DSFViewRepresentable = UIViewRepresentable #endif #endif extension ExpressibleByIntegerLiteral where Self: Comparable { /// Clamp a value to a closed range /// - Parameter range: the range to clamp to @inlinable func clamped(to range: ClosedRange) -> Self { return min(max(self, range.lowerBound), range.upperBound) } } public extension CGGradient { static func Create(_ definition: [(position: CGFloat, color: CGColor)], colorSpace: CGColorSpace? = nil) -> CGGradient { return CGGradient( colorsSpace: colorSpace, colors: definition.map { $0.1 } as CFArray, // [c1, c2] as CFArray, locations: definition.map { $0.0 } //[1.0, 0.0] )! } } extension DSFView { #if os(macOS) @objc func retinaScale() -> CGFloat { return self.window?.screen?.backingScaleFactor ?? 1.0 } @inlinable func setNeedsDisplay() { self.needsDisplay = true } #else @objc func retinaScale() -> CGFloat { return self.window?.screen.scale ?? 1.0 } #endif } extension CGPoint { @inlinable func offsettingX(by value: CGFloat) -> CGPoint { return CGPoint(x: self.x + value, y: self.y) } @inlinable func offsettingY(by value: CGFloat) -> CGPoint { return CGPoint(x: self.x, y: self.y + value) } } /// Return the slope of the line joining points a and b @inlinable func gradient(_ a: CGPoint, _ b: CGPoint) -> CGFloat { return (b.y - a.y) / (b.x - a.x) } // MARK: - NSView/UIView snapshot #if os(macOS) public extension NSView { @objc func snapshot() -> NSImage? { guard let bitmapRep = self.bitmapImageRepForCachingDisplay(in: self.bounds) else { return nil } self.cacheDisplay(in: self.bounds, to: bitmapRep) let image = NSImage() image.addRepresentation(bitmapRep) return image } } #else public extension UIView { @objc func snapshot() -> UIImage { if #available(iOS 10.0, tvOS 10.0, *) { let renderer = UIGraphicsImageRenderer(bounds: bounds) return renderer.image { rendererContext in layer.render(in: rendererContext.cgContext) } } else { UIGraphicsBeginImageContext(self.frame.size) self.layer.render(in:UIGraphicsGetCurrentContext()!) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return UIImage(cgImage: image!.cgImage!) } } } #endif // MARK: - @discardableResult @inlinable internal func UpdateIfNotEqual(result: inout T, val: T) -> Bool where T: Equatable { if result != val { result = val return true } return false } // MARK: - Debugging utils // Simple method to draw a rectangle in a context (usually for testing) func DrawRect(primary: CGContext, rect: CGRect, color: CGColor = DSFColor.systemRed.cgColor) { primary.usingGState { (outer) in outer.addRect(rect) outer.setLineWidth(0.5) outer.setStrokeColor(color) outer.strokePath() } } #if !os(macOS) extension CGColor { static var black: CGColor { CGColor(gray: 0, alpha: 1) } static var white: CGColor { CGColor(gray: 1, alpha: 1) } static var clear: CGColor { CGColor(gray: 0, alpha: 0) } } #endif extension String { func extractCGFloats() -> [CGFloat] { let components = self.split(separator: ",") let floats: [CGFloat] = components .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } // Convert to string array .compactMap { Float($0) } // Convert to float array if possible .compactMap { CGFloat($0) } // Convert to CGFloat array return floats } } // Returns the first item in the specified array which is non-nil. If all elements are nil, returns nil @inlinable internal func firstNotNil(_ items: [T?]) -> T? { if let first = items.first(where: { $0 != nil }) { return first } return nil } // A CATextLayer that vertically centers its content class LCTextLayer : CATextLayer { // REF: http://lists.apple.com/archives/quartz-dev/2008/Aug/msg00016.html // CREDIT: David Hoerl - https://github.com/dhoerl // USAGE: To fix the vertical alignment issue that currently exists within the CATextLayer class. Change made to the yDiff calculation. override func draw(in context: CGContext) { let height = self.bounds.size.height let fontSize = self.fontSize let yDiff = (height-fontSize)/2 - fontSize/10 context.saveGState() context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default) super.draw(in: context) context.restoreGState() } } extension CGColor { /// Returns a black or white contrasting color for this color /// - Parameter defaultColor: If the color cannot be converted to the genericRGB colorspace, or the input color is .clear, the fallback color /// - Returns: black or white depending on which provides the greatest contrast to this color func flatContrastColor(defaultColor: CGColor = CGColor.DefaultBlack) -> CGColor { guard self != CGColor.clear, let rgbColor = self.converted(to: CGColorSpace(name: CGColorSpace.sRGB)!, intent: .perceptual, options: nil) else { return defaultColor } let r = 0.299 * rgbColor.components![0] let g = 0.587 * rgbColor.components![1] let b = 0.114 * rgbColor.components![2] let avgGray: CGFloat = 1 - (r + g + b) return (avgGray >= 0.45) ? CGColor.DefaultWhite : CGColor.DefaultBlack } } extension CGColor { static let DefaultWhite = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 1, 1, 1])! static let DefaultBlack = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 1])! static let DefaultClear = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 1, 1, 0])! } // MARK: - CATextLayer extensions extension CATextLayer { func font() -> DSFFont? { if let f = self.font as? DSFFont { return f //return DSFFont(name: f.fontName, size: self.fontSize) } else if let s = self.font as? String { return DSFFont(name: s, size: self.fontSize) } else { return nil } } func attributedString() -> NSAttributedString { if let asv = self.string as? NSAttributedString { return asv } else if let s = self.string as? String { let attrs = [NSAttributedString.Key.font: self.font() as Any] return NSAttributedString(string: s, attributes: attrs) } else { fatalError() } } /// Get the required bounds for the text layer content given `size` constraints func textBounds(for size: CGSize) -> CGSize { #if os(macOS) let options: NSString.DrawingOptions = [NSString.DrawingOptions.usesFontLeading, NSString.DrawingOptions.usesLineFragmentOrigin] #else let options: NSStringDrawingOptions = [NSStringDrawingOptions.usesFontLeading, NSStringDrawingOptions.usesLineFragmentOrigin] #endif let ttt = self.attributedString() let br = ttt.boundingRect( with: CGSize(width: size.width, height: size.height), options: options, context: nil) return br.size } } extension CATransaction { /// Perform 'body' without implicit animations /// /// - Parameters: /// - body: The block to execute without implicit animations /// /// Example :- /// ``` /// CATransaction.withDisabledActions { /// self.checkerboardLayer.image.frame = self.bounds /// ... /// } /// ``` class func withDisabledActions(_ body: () throws -> T) rethrows -> T { CATransaction.begin() CATransaction.setDisableActions(true) defer { CATransaction.commit() } return try body() } } #if os(macOS) extension CGRect { /// Inset this rect by the amount specified in `insets`. @inlinable func inset(by insets: NSEdgeInsets) -> CGRect { var result = self result.origin.x += insets.left result.origin.y += insets.top result.size.width -= (insets.left + insets.right) result.size.height -= (insets.top + insets.bottom) return result } } #endif extension DSFEdgeInsets { /// A default zero inset static let zero = DSFEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) /// Calculate the maximum insets between the two edgeinset objects @inlinable func combineMaximum(using other: DSFEdgeInsets) -> DSFEdgeInsets { return DSFEdgeInsets(top: max(self.top, other.top), left: max(self.left, other.left), bottom: max(self.bottom, other.bottom), right: max(self.right, other.right)) } } extension Array { /// Return a copy of this array with 'element' appended @inlinable func appending(_ element: Element) -> Self { var result = self result.append(element) return result } /// Return a copy of this array with an array of 'element' appended @inlinable func appending(contentsOf elements: [Element]) -> Self { var result = self result.append(contentsOf: elements) return result } } extension CGImage { func flipped() -> CGImage? { let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) let flipped = CGContext( data: nil, width: self.width, height: self.height, bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo.rawValue )! // Flip the context flipped.translateBy(x: 0, y: CGFloat(height)) flipped.scaleBy(x: 1.0, y: -1.0) // Draw the image into the context flipped.draw(self, in: CGRect(x: 0, y: 0, width: self.width, height: self.height)) // Create a new CGImage from the context return flipped.makeImage() } } ================================================ FILE: Tests/DSFSparklineTests/CircularGaugeTests.swift ================================================ // // CircularGaugeTests.swift // // // Created by Darren Ford on 12/3/2024. // import XCTest @testable import DSFSparkline import SwiftImageReadWrite private let outputFolder = try! testResultsContainer.subfolder(with: "circular-gauge") private let imagesFolder = try! outputFolder.subfolder(with: "images") private let imageStore = ImageOutput(imagesFolder) private var markdownText = "# Circular Gauge\n\n" final class CircularGaugeTests: XCTestCase { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDownWithError() throws { try! outputFolder.write(markdownText, to: "circular-gauge.md", encoding: .utf8) } func testSizing() throws { markdownText += "## Sizing\n\n" try [16, 32, 48].forEach { sz in try stride(from: 0, through: 1, by: 0.1).forEach { value in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() a1.value = value b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: sz, height: sz), scale: 2)) let filename = "circular-gauge-ex-\(sz)-\(value).png" let link = try imageStore.store(image.representation.png(), filename: filename) markdownText += "\n" } markdownText += "\n" } } func testTrackInset() throws { markdownText += "## Track inset\n\n" let tfc = DSFSparklineOverlay.CircularGauge.TrackStyle( width: 10, fillColor: DSFSparkline.Fill.Color(srgbRed: 0, green: 0, blue: 1, alpha: 0.1) ) let vfc = DSFSparklineOverlay.CircularGauge.TrackStyle( width: 6, fillColor: DSFSparkline.Fill.Color(srgbRed: 0.2, green: 0.2, blue: 0.9), strokeColor: CGColor.sRGBA(0, 0, 1) ) try [false, true].forEach { hasShadow in if hasShadow { markdownText += "### Shadowed\n\n" } else { markdownText += "### No Shadow\n\n" } try [10, 8, 6, 4].forEach { tw in try stride(from: 0, through: 1, by: 0.1).forEach { value in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() a1.value = value a1.trackStyle = tfc vfc.width = CGFloat(tw) a1.lineStyle = vfc if hasShadow { vfc.shadow = .init(blurRadius: 3, offset: CGSize(width: 2, height: -3), color: .black) } b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 48, height: 48), scale: 2)) let filename = "circular-gauge-inset-\(hasShadow)-\(tw)-\(value).png" let link = try imageStore.store(image.representation.png(), filename: filename) markdownText += "\n" } markdownText += "\n" } } } func testTrackShadow() throws { markdownText += "## Shadowing\n\n" markdownText += "\n### Track/Value shadows\n\n" markdownText += "| none | track only | value only | both |\n" markdownText += "|---------|--------------|--------------|---------|\n" markdownText += "|" try [false, true].forEach { (lineShadow: Bool) in try [false, true].forEach { (trackShadow: Bool) in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() a1.value = 0.65 a1.trackStyle.width = 10 a1.lineStyle.width = 4 if trackShadow { a1.trackStyle.shadow = .init(blurRadius: 2, offset: CGSize(width: 1, height: -1), color: CGColor.sRGBA(0, 0, 1)) } if lineShadow { a1.lineStyle.shadow = .init(blurRadius: 2, offset: CGSize(width: 1, height: -1), color: CGColor.sRGBA(1, 0, 0)) } b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 48, height: 48), scale: 2)) let filename = "circular-gauge-inset-\(trackShadow)-\(lineShadow).png" let link = try imageStore.store(image.representation.png(), filename: filename) markdownText += "|" } } markdownText += "|\n\n" markdownText += "### Shadow Offset\n\n" markdownText += "| -2, -2 | -2, 2 | 2, -2 | 2, 2 | 4, 0 | 0, 4 |\n" markdownText += "|---------|---------|---------|---------|--------|--------|\n" markdownText += "|" try [(-2, -2), (-2, 2), (2, -2), (2, 2), (4, 0), (0, 4)].forEach { offset in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() let sh = DSFSparkline.Shadow(blurRadius: 3, offset: CGSize(width: offset.0, height: offset.1), color: .black) a1.value = 0.65 a1.trackStyle.width = 20 a1.trackStyle.shadow = sh a1.lineStyle.width = 10 a1.lineStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 1, blue: 0) a1.lineStyle.shadow = sh b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 64, height: 64), scale: 2)) let filename = "circular-gauge-offset-standardshadow-\(offset).png" let link = try imageStore.store(image.representation.png(), filename: filename) markdownText += "" markdownText += "|" } markdownText += "|\n\n" } func testTrackShadowInset() throws { markdownText += "## Inner Shadows\n\n" let innerShadow = DSFSparkline.Shadow(blurRadius: 3, offset: CGSize(width: 2, height: -2), color: .black, isInner: true) let outerShadow = DSFSparkline.Shadow(blurRadius: 3, offset: CGSize(width: 2, height: -2), color: .black, isInner: false) markdownText += "\n### Track/Value shadows\n\n" markdownText += "| none | track only | value only | both | in/out |\n" markdownText += "|---------|---------|---------|---------|---------|\n" markdownText += "|" enum OTS: CaseIterable { case none case track case value case all } try OTS.allCases.forEach { useTrackInner in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() a1.value = 0.65 a1.trackStyle.width = 20 if useTrackInner == .track || useTrackInner == .all { a1.trackStyle.shadow = innerShadow } a1.lineStyle.width = 10 a1.lineStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 1, blue: 0) a1.lineStyle.strokeColor = CGColor.standard.yellow a1.lineStyle.strokeWidth = 0.1 if useTrackInner == .value || useTrackInner == .all { a1.lineStyle.shadow = innerShadow } b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 64, height: 64), scale: 2)) let filename = "circular-gauge-inner-\(useTrackInner).png" let link = try imageStore.store(image.representation.png(), filename: filename) markdownText += "|" } do { let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() a1.value = 0.65 a1.trackStyle.width = 20 a1.trackStyle.shadow = innerShadow a1.lineStyle.width = 10 a1.lineStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 1, blue: 0) a1.lineStyle.shadow = outerShadow b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 64, height: 64), scale: 2)) let filename = "circular-gauge-inner-inout.png" let link = try imageStore.store(image.representation.png(), filename: filename) markdownText += "|\n\n" } markdownText += "\n### Shadow Offset\n\n" markdownText += "| -2, -2 | -2, 2 | 2, -2 | 2, 2 |\n" markdownText += "|---------|---------|---------|---------|\n" markdownText += "|" try [(-2, -2), (-2, 2), (2, -2), (2, 2)].forEach { offset in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() let sh = DSFSparkline.Shadow(blurRadius: 3, offset: CGSize(width: offset.0, height: offset.1), color: .black, isInner: true) a1.value = 0.65 a1.trackStyle.width = 20 a1.trackStyle.shadow = sh a1.lineStyle.width = 10 a1.lineStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 1, blue: 0) a1.lineStyle.shadow = sh b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 64, height: 64), scale: 2)) let filename = "circular-gauge-offset-\(offset).png" let link = try imageStore.store(image.representation.png(), filename: filename) markdownText += "" markdownText += "|" } markdownText += "|\n\n" } func testTrackShadowInset2() throws { let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() let innerShadow = DSFSparkline.Shadow(blurRadius: 3, offset: CGSize(width: 2, height: -2), color: .black, isInner: true) a1.value = 0.65 a1.trackStyle.width = 20 a1.lineStyle.width = 15 a1.lineStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 1, blue: 0) a1.lineStyle.shadow = innerShadow b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 64, height: 64), scale: 2)) Swift.print(image) } } ================================================ FILE: Tests/DSFSparklineTests/CircularProgressTests.swift ================================================ // // CircularProgressTests.swift // // // Created by Darren Ford on 1/3/2024. // import XCTest @testable import DSFSparkline import SwiftImageReadWrite private let outputFolder = try! testResultsContainer.subfolder(with: "circular-progress") private let imagesFolder = try! outputFolder.subfolder(with: "images") private let imageStore = ImageOutput(imagesFolder) private var markdownText = "# Circular Progress\n\n" final class CircularProgressTests: XCTestCase { override class func tearDown() { try! outputFolder.write(markdownText, to: "circular-progress.md", encoding: .utf8) } override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } func testBasic() throws { do { markdownText += "### Inline documentation\n\n" try stride(from: 0, through: 1, by: 0.333).forEach { value in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularProgress() a1.fillStyle = primaryPoke a1.trackColor = primaryPoke.color.copy(alpha: 0.3)! a1.trackWidth = 3 a1.value = value b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 16, height: 16), scale: 2)) let filename = "circular-poke-\(value).png" let link = try imageStore.store(image.representation.png(scale: 2), filename: filename) markdownText += "\n" } markdownText += "\n" do { markdownText += "### Flat\n\n" var count = 1 try stride(from: 0, through: 2, by: 0.2).forEach { value in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularProgress() a1.fillStyle = DSFSparkline.Fill.Color(srgbRed: 0.5, green: 0, blue: 1, alpha: 1) a1.trackColor = CGColor(srgbRed: 0.5, green: 0, blue: 1, alpha: 0.2) a1.value = value b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 48, height: 48), scale: 2)) let filename = "circular-\(count).png" let link = try imageStore.store(image.representation.png(scale: 2), filename: filename) markdownText += "\n" count += 1 } } markdownText += "\n" do { markdownText += "### Gradient\n\n" let gradient = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 1.0), CGColor(srgbRed: 0.891, green: 0.000, blue: 0.090, alpha: 1.0), ] ) var count = 1 try stride(from: 0, through: 2, by: 0.2).forEach { value in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularProgress() a1.fillStyle = gradient a1.trackColor = CGColor(gray: 0.5, alpha: 0.2) a1.value = value b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 48, height: 48), scale: 2)) let filename = "circular-radial-\(count).png" let link = try imageStore.store(image.representation.png(scale: 2), filename: filename) markdownText += "\n" count += 1 } } markdownText += "\n" do { markdownText += "### Nested\n\n" try [48, 64, 80].forEach { dimension in var count = 1 try stride(from: 0, through: 1, by: 0.2).forEach { value in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularProgress() a1.fillStyle = DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 0, alpha: 1) a1.trackColor = CGColor(gray: 0.5, alpha: 0.2) a1.value = value a1.trackWidth = 5 b1.addOverlay(a1) let a2 = DSFSparklineOverlay.CircularProgress() a2.fillStyle = DSFSparkline.Fill.Color(srgbRed: 0, green: 0, blue: 1, alpha: 1) a2.trackColor = CGColor(gray: 0.5, alpha: 0.2) a2.trackWidth = 5 a2.padding = 7 a2.value = min(value + 0.1, 1.0) b1.addOverlay(a2) let a3 = DSFSparklineOverlay.CircularProgress() a3.fillStyle = DSFSparkline.Fill.Color(srgbRed: 0, green: 1, blue: 0, alpha: 1) a3.trackColor = CGColor(gray: 0.5, alpha: 0.2) a3.trackWidth = 5 a3.padding = 14 a3.value = min(value + 0.05, 1.0) b1.addOverlay(a3) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: dimension, height: dimension), scale: 2)) let filename = "circular-radial-2overlay\(count)-\(dimension).png" let link = try imageStore.store(image.representation.png(scale: 2), filename: filename) markdownText += "\n" count += 1 } markdownText += "\n" } } markdownText += "\n" do { markdownText += "### Track width\n\n" try stride(from: 2, through: 12, by: 2).forEach { width in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularProgress() a1.fillStyle = DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 0, alpha: 1) a1.trackColor = CGColor(gray: 0.5, alpha: 0.2) a1.value = 0.65 a1.trackWidth = CGFloat(width) b1.addOverlay(a1) let a2 = DSFSparklineOverlay.CircularProgress() a2.fillStyle = DSFSparkline.Fill.Color(srgbRed: 0, green: 0, blue: 1, alpha: 1) a2.trackColor = CGColor(gray: 0.5, alpha: 0.2) a2.trackWidth = width a2.padding = 7 a2.value = 0.25 b1.addOverlay(a2) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 48, height: 48), scale: 2)) let filename = "circular-trackwidth-\(width).png" let link = try imageStore.store(image.representation.png(scale: 2), filename: filename) markdownText += "\n" } } markdownText += "\n" do { markdownText += "### Track padding\n\n" try stride(from: 0, through: 10, by: 2).forEach { padding in let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularProgress() a1.fillStyle = DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 0, alpha: 1) a1.trackColor = CGColor(gray: 0.5, alpha: 0.2) a1.value = 0.65 a1.trackWidth = 4 b1.addOverlay(a1) let a2 = DSFSparklineOverlay.CircularProgress() a2.fillStyle = DSFSparkline.Fill.Color(srgbRed: 0, green: 0, blue: 1, alpha: 1) a2.trackColor = CGColor(gray: 0.5, alpha: 0.2) a2.trackWidth = 4 a2.padding = padding a2.value = 0.25 b1.addOverlay(a2) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 48, height: 48), scale: 2)) let filename = "circular-trackpadding-\(padding).png" let link = try imageStore.store(image.representation.png(scale: 2), filename: filename) markdownText += "\n" } } markdownText += "\n" do { markdownText += "### Health rings\n\n" let g = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 1.0), CGColor(srgbRed: 0.891, green: 0.000, blue: 0.090, alpha: 1.0), ] ) let g1 = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 1.0), CGColor(srgbRed: 0.601, green: 1.000, blue: 0.009, alpha: 1.0), ] ) let g2 = DSFSparkline.Fill.Gradient( colors: [ CGColor(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 1.0), CGColor(srgbRed: 0.015, green: 0.847, blue: 1.000, alpha: 1.0), ] ) try [0.0, 0.33, 0.66, 1.0].forEach { index in let b1 = DSFSparklineSurface.Bitmap() do { let a1 = DSFSparklineOverlay.CircularProgress() a1.value = index a1.fillStyle = g a1.trackWidth = 10 a1.trackColor = .init(srgbRed: 0.977, green: 0.221, blue: 0.520, alpha: 0.1) b1.addOverlay(a1) } do { let a1 = DSFSparklineOverlay.CircularProgress() a1.value = index * 0.8 a1.fillStyle = g1 a1.trackWidth = 10 a1.padding = 12 a1.trackColor = .init(srgbRed: 0.849, green: 1.000, blue: 0.000, alpha: 0.1) b1.addOverlay(a1) } do { let a1 = DSFSparklineOverlay.CircularProgress() a1.value = index * 0.6 a1.fillStyle = g2 a1.trackWidth = 10 a1.padding = 24 a1.trackColor = .init(srgbRed: 0.000, green: 1.000, blue: 0.663, alpha: 0.1) b1.addOverlay(a1) } let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 96, height: 96), scale: 2)) let filename = "circular-health-\(index).png" let link = try imageStore.store(image.representation.png(scale: 2), filename: filename) markdownText += "\n" } } markdownText += "\n" } } } ================================================ FILE: Tests/DSFSparklineTests/DSFSparklineTests.swift ================================================ // // DSFSparklinesTests.swift // DSFSparklinesTests // // Created by Darren Ford on 20/6/19. // Copyright © 2019 Darren Ford. All rights reserved. // // MIT license // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated // documentation files (the "Software"), to deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or substantial // portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS // OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // import XCTest @testable import DSFSparkline // A temporary file container to hold results internal let testResultsContainer = try! TestFilesContainer(named: "DSFSparkline_Tests") class DSFSparklineTests: XCTestCase { override func setUp() { // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. } func testFixForWindowSizeSmallerThanInitialDataIssues() { // Testing for https://github.com/dagronf/DSFSparkline/issues/3 let ds = SparklineWindow(windowSize: 5, dataRange: (-10 ... 10)) ds.push(values: [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]) XCTAssertEqual(ds.raw.count, 5) } func testBlah() { let dd = SparklineWindow(windowSize: 10, dataRange: (-100 ... 100)) // Default values for the initializer XCTAssertEqual(dd.raw.count, 10) XCTAssertEqual(dd.normalized.count, 10) XCTAssertEqual(dd.raw, [-100, -100, -100, -100, -100, -100, -100, -100, -100, -100]) XCTAssertEqual(dd.normalized, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) XCTAssertTrue(dd.push(value: 0)) XCTAssertTrue(dd.push(value: 1)) XCTAssertEqual(dd.raw.count, 10) XCTAssertEqual(dd.normalized.count, 10) XCTAssertEqual(dd.normalized, [0, 0, 0, 0, 0, 0, 0, 0, 0.5, 0.505]) XCTAssertTrue(dd.push(value: 100)) } func testDynamicallyRanged() { let dd = SparklineWindow(windowSize: 10) // Default values for the initializer XCTAssertEqual(dd.raw.count, 10) XCTAssertEqual(dd.normalized.count, 10) XCTAssertEqual(dd.raw, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) XCTAssertEqual(dd.normalized, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) XCTAssertTrue(dd.push(value: 10)) XCTAssertEqual(dd.normalized, [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]) XCTAssertTrue(dd.push(value: 20)) XCTAssertEqual(dd.normalized, [0, 0, 0, 0, 0, 0, 0, 0, 0.5, 1]) XCTAssertTrue(dd.push(value: -20)) XCTAssertEqual(dd.normalized, [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.75, 1.0, 0.0]) XCTAssertTrue(dd.push(value: -10)) XCTAssertEqual(dd.normalized, [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.75, 1.0, 0.0, 0.25]) } func testResizing() { let dd = SparklineWindow(windowSize: 10) XCTAssertEqual(dd.raw.count, 10) dd.set(values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) XCTAssertEqual(dd.normalized, [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) dd.set(values: [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]) XCTAssertEqual(dd.normalized, [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) /// Resizing dd.set(values: [-5, -4, -3, -2, -1, 0]) XCTAssertEqual(dd.normalized, [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]) dd.set(values: [-5, -4, -3, -2, -1, 0, 1, 2]) XCTAssertEqual(dd.normalized, [0.0, 0.14285714285714285, 0.2857142857142857, 0.42857142857142855, 0.5714285714285714, 0.7142857142857143, 0.8571428571428571, 1.0]) dd.reset() dd.windowSize = 3 XCTAssertEqual(0, dd.counter) XCTAssertTrue(dd.push(value: 1)) XCTAssertEqual(dd.raw, [0.0, 0.0, 1.0]) XCTAssertEqual(dd.normalized, [0.0, 0.0, 1.0]) XCTAssertEqual(1, dd.counter) XCTAssertTrue(dd.push(value: 2)) XCTAssertEqual(dd.raw, [0.0, 1.0, 2.0]) XCTAssertEqual(dd.normalized, [0.0, 0.5, 1.0]) XCTAssertEqual(2, dd.counter) XCTAssertTrue(dd.push(value: 3)) XCTAssertEqual(dd.raw, [1.0, 2.0, 3.0]) XCTAssertEqual(dd.normalized, [0.0, 0.5, 1.0]) XCTAssertEqual(3, dd.counter) XCTAssertTrue(dd.push(value: 4)) XCTAssertEqual(dd.raw, [2.0, 3.0, 4.0]) XCTAssertEqual(dd.normalized, [0.0, 0.5, 1.0]) XCTAssertEqual(4, dd.counter) } func testWindowSizeChanging() { let dd = SparklineWindow(windowSize: 20) dd.set(values: [1.0, 2.0, 3.0, 4.0]) dd.windowSize = 7 XCTAssertEqual([0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0], dd.raw) dd.windowSize = 3 XCTAssertEqual([2.0, 3.0, 4.0], dd.raw) } func testDataSource() { // Check that truncating to range works let ds = DSFSparkline.DataSource(windowSize: 10, range: -10 ... 10) XCTAssertTrue(ds.push(value: 5)) XCTAssertTrue(ds.push(value: 50)) XCTAssertEqual(ds.data, [0, 0, 0, 0, 0, 0, 0, 0, 5, 10]) XCTAssertEqual(ds.normalized, [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.75, 1.0]) // With no range, adding 5 here makes the implicit range to 0 ... 5 let ds2 = DSFSparkline.DataSource(windowSize: 5) XCTAssertTrue(ds2.push(value: 5)) XCTAssertEqual(ds2.data, [0, 0, 0, 0, 5]) XCTAssertEqual(ds2.normalized, [0, 0, 0, 0, 1]) // With no range, adding -5 here makes the implicit range to -5 ... 5 XCTAssertTrue(ds2.push(value: -5)) XCTAssertEqual(ds2.data, [0, 0, 0, 5, -5]) XCTAssertEqual(ds2.normalized, [0.5, 0.5, 0.5, 1, 0]) // With no range, adding -5 here makes the implicit range to -5 ... 5 XCTAssertTrue(ds2.push(value: 10)) XCTAssertTrue(ds2.push(value: -3)) XCTAssertEqual(ds2.data, [0, 5, -5, 10, -3]) XCTAssertEqual(ds2.normalized, [0.3333333333333333, 0.6666666666666666, 0.0, 1.0, 0.13333333333333333]) ds2.windowSize = 3 XCTAssertEqual(ds2.data, [-5, 10, -3]) XCTAssertEqual(ds2.normalized, [0.0, 1.0, 0.13333333333333333]) ds2.windowSize = 10 XCTAssertEqual(ds2.data, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -5.0, 10.0, -3.0]) XCTAssertEqual(ds2.normalized, [0.3333333333333333, 0.3333333333333333, 0.3333333333333333, 0.3333333333333333, 0.3333333333333333, 0.3333333333333333, 0.3333333333333333, 0.0, 1.0, 0.13333333333333333]) ds2.reset() XCTAssertEqual(ds2.data, [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) ds2.set(values: [0, 1, 2, 3, 4, 5, 6]) XCTAssertEqual(7, ds2.windowSize) XCTAssertEqual(ds2.data, [0, 1, 2, 3, 4, 5, 6]) } func testAddValues() { let ds = DSFSparkline.DataSource(windowSize: 5) ds.push(values: [1.1, 2.2, 3.3]) XCTAssertEqual(ds.data, [0, 0, 1.1, 2.2, 3.3]) XCTAssertTrue(ds.push(value: 1.2)) XCTAssertEqual(ds.data, [0, 1.1, 2.2, 3.3, 1.2]) ds.push(values: [10, 11, 12]) XCTAssertEqual(ds.data, [3.3, 1.2, 10.0, 11.0, 12.0]) // Check what happens if greater than window. Should truncate to the last 5 values in the array // Equivalent to push 1, push 2, push 3, push 4 etc. ds.push(values: [1, 2, 3, 4, 5, 6, 7, 8]) XCTAssertEqual(ds.data, [4, 5, 6, 7, 8]) } func testCircularProgress() throws { let l1 = DSFSparklineOverlay.CircularProgress() l1.value = 1.8 l1.trackWidth = 10 let l2 = DSFSparklineOverlay.CircularProgress() l2.value = 1.4 l2.trackWidth = 10 l2.padding = 12 l2.fillStyle = DSFSparkline.Fill.Gradient(colors: [ CGColor.init(red: 1, green: 0, blue: 0, alpha: 1), CGColor.init(red: 0, green: 1, blue: 0, alpha: 1), CGColor.init(red: 0, green: 0, blue: 1, alpha: 1), ]) // Solid white let l3 = DSFSparklineOverlay.CircularProgress() l3.value = 0.35 l3.trackWidth = 10 l3.padding = 24 l3.fillStyle = DSFSparkline.Fill.Gradient(colors: [ CGColor.init(red: 1, green: 0, blue: 0, alpha: 1), CGColor.init(red: 0, green: 1, blue: 0, alpha: 1), CGColor.init(red: 0, green: 0, blue: 1, alpha: 1), ]) let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface bitmap.addOverlay(l1) // And add the overlay to the surface. bitmap.addOverlay(l2) // And add the overlay to the surface. bitmap.addOverlay(l3) // And add the overlay to the surface. let image = bitmap.image(width: 100, height: 100, scale: 2) Swift.print(image) } func testGradientPeek() throws { let gr = CGGradient( colorsSpace: nil, colors: [ CGColor(red: 1, green: 0, blue: 0, alpha: 1), CGColor(red: 0, green: 0, blue: 1, alpha: 1), ] as CFArray, locations: [0.0, 1.0] )! let f1 = DSFSparkline.Fill.Gradient(gradient: gr) let c1 = f1.color(at: 0) let c2 = f1.color(at: 0.5) let c3 = f1.color(at: 1) Swift.print([c1, c2, c3]) } } ================================================ FILE: Tests/DSFSparklineTests/LineGraphTests.swift ================================================ // // LineGraphTests.swift // // // Created by Darren Ford on 1/3/2024. // import XCTest @testable import DSFSparkline import SwiftImageReadWrite private let outputFolder = try! testResultsContainer.subfolder(with: "line-graph") private let imagesFolder = try! outputFolder.subfolder(with: "images") private let imageStore = ImageOutput(imagesFolder) private var markdownText = "# Line Graph\n\n" final class LineGraphTests: XCTestCase { override class func tearDown() { try! outputFolder.write(markdownText, to: "line-graph.md", encoding: .utf8) } override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } func testBasic() throws { markdownText += "## Inline documentation\n\n" let source = DSFSparkline.DataSource(values: [4, 1, 8, 7, 5, 9, 3], range: 0 ... 10) try [0, 5].forEach { zeroline in source.zeroLineValue = zeroline markdownText += "| line | interpolated | line with markers | line with markers interpolated |\n" markdownText += "|------|------|------|------|\n" try [false, true].forEach { interpolated in markdownText += "|" let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let line = DSFSparklineOverlay.Line() // Create a line overlay line.interpolated = interpolated line.strokeWidth = 1 line.primaryStrokeColor = primaryStroke line.primaryFill = primaryFill line.dataSource = source bitmap.addOverlay(line) if zeroline != 0 { bitmap.addOverlay(DSFSparklineOverlay.ZeroLine(dataSource: source)) } // Generate an image with retina scale let image = try XCTUnwrap(bitmap.cgImage(size: CGSize(width: 50, height: 25), scale: 2)) let filename = "line-simple-small-interpolated(\(interpolated))-zeroline(\(zeroline)).png" let link = try imageStore.store(image.representation.png(scale: 2), filename: filename) markdownText += "" } try [false, true].forEach { interpolated in markdownText += "|" let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let line = DSFSparklineOverlay.Line() // Create a line overlay line.strokeWidth = 1 line.primaryStrokeColor = primaryStroke line.primaryFill = primaryFill line.markerSize = 3 line.interpolated = interpolated line.dataSource = source // Assign the datasource to the overlay bitmap.addOverlay(line) // And add the overlay to the surface. if zeroline != 0 { bitmap.addOverlay(DSFSparklineOverlay.ZeroLine(dataSource: source)) } // Generate an image with retina scale let image3 = try XCTUnwrap(bitmap.cgImage(size: CGSize(width: 50, height: 25), scale: 2)) let filename3 = "line-simple-attributed-string-inline-interpolated(\(interpolated))-zeroline(\(zeroline)).png" let link3 = try imageStore.store(image3.representation.png(scale: 2), filename: filename3) markdownText += "" } markdownText += "|\n\n" } } } ================================================ FILE: Tests/DSFSparklineTests/PresentationTests.swift ================================================ // // PresentationTests.swift // // // Created by Darren Ford on 27/2/2024. // import XCTest @testable import DSFSparkline import SwiftImageReadWrite class ImageOutput { let _imagesFolder: TestFilesContainer.Subfolder init(_ folder: TestFilesContainer.Subfolder) { _imagesFolder = folder } func store(_ data: Data, filename: String) throws -> String { try _imagesFolder.write(data, to: filename) return "./images/\(filename)" } func store(_ string: String, filename: String) throws -> String { try _imagesFolder.write(string, to: filename) return "./images/\(filename)" } } private let outputFolder = try! testResultsContainer.subfolder(with: "generation") private let imagesFolder = try! outputFolder.subfolder(with: "images") private let imageStore = ImageOutput(imagesFolder) private var markdownText = "# Sparklines\n\n" final class PresentationTests: XCTestCase { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } override class func tearDown() { try! outputFolder.write(markdownText, to: "sparklines.md", encoding: .utf8) } } let primaryPoke = DSFSparkline.Fill.Color(srgbRed: 0.934, green: 0.000, blue: 0.000, alpha: 1.0) let baseColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.033, 0.277, 0.650, 1.000])! let primaryStroke = baseColor // (gray: 0.0, alpha: 0.3)) let primaryFill = DSFSparkline.Fill.Color(baseColor.copy(alpha: 0.3)!) extension PresentationTests { func testCircularGraph() throws { markdownText += "## Circular Grid\n\n" do { try [0, 0.33, 0.66, 1.0].forEach { value in let surface = DSFSparklineSurface.Bitmap() let a = DSFSparklineOverlay.CircularGauge() let baseColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0.033, 0.277, 0.650, 1.000])! let primaryFill = DSFSparkline.Fill.Color(baseColor.copy(alpha: 0.3)!) a.trackStyle = DSFSparklineOverlay.CircularGauge.TrackStyle(width: 10, fillColor: primaryFill) a.lineStyle = DSFSparklineOverlay.CircularGauge.TrackStyle(width: 5, fillColor: DSFSparkline.Fill.Color(baseColor)) a.value = value surface.addOverlay(a) let filename = "circular-gauge-small-\(value).png" let bitmap = surface.image(width: 40, height: 40, scale: 2)! let link = try imageStore.store(bitmap.representation.png(scale: 2), filename: filename) markdownText += "\n" } } do { let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() let innerShadow = DSFSparkline.Shadow(blurRadius: 3, offset: CGSize(width: 2, height: -2), color: .black, isInner: true) let outerShadow = DSFSparkline.Shadow(blurRadius: 3, offset: CGSize(width: 2, height: -2), color: .black, isInner: false) a1.value = 0.65 a1.trackStyle.width = 20 a1.trackStyle.shadow = innerShadow a1.lineStyle.width = 10 a1.lineStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 1, blue: 0) a1.lineStyle.shadow = outerShadow b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 64, height: 64), scale: 2)) let filename = "circular-gauge-inner-inout.png" let link = try imageStore.store(image.representation.png(), filename: filename) markdownText += "|\n\n" } do { let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() a1.value = 0.35 a1.trackStyle.width = 20 a1.trackStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 0, alpha: 0.1) a1.lineStyle.width = 10 a1.lineStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 0, alpha: 1) b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 64, height: 64), scale: 2)) let filename = "circular-gauge-basic.png" let link = try imageStore.store(image.representation.png(), filename: filename) markdownText += "|\n\n" } do { let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() a1.value = 0.35 a1.trackStyle.width = 20 a1.trackStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 0, alpha: 0.1) a1.lineStyle.width = 10 a1.lineStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 0, alpha: 1) b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 64, height: 64), scale: 2)) let filename = "circular-gauge-basic.png" let link = try imageStore.store(image.representation.png(), filename: filename) markdownText += "|\n\n" } do { let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() a1.value = 0.55 a1.trackStyle.width = 10 a1.trackStyle.shadow = DSFSparkline.Shadow(blurRadius: 3, offset: CGSize(width: 2, height: -2), color: .black, isInner: true) a1.trackStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 1, alpha: 0.1) a1.lineStyle.width = 8 a1.lineStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 1, green: 0, blue: 1, alpha: 1) b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 30, height: 30), scale: 2)) let filename = "circular-gauge-slightly.png" let link = try imageStore.store(image.representation.png(scale: 2), filename: filename) markdownText += "|\n\n" } do { let b1 = DSFSparklineSurface.Bitmap() let a1 = DSFSparklineOverlay.CircularGauge() let gr = DSFSparkline.Fill.Gradient(colors: [ CGColor.sRGBA(0, 0, 1), CGColor.sRGBA(0, 1, 1), ]) a1.value = 0.87 a1.trackStyle.width = 15 a1.trackStyle.fillColor = DSFSparkline.Fill.Color(srgbRed: 0, green: 0, blue: 1, alpha: 0.1) a1.lineStyle.width = 7 a1.lineStyle.fillColor = gr a1.lineStyle.shadow = .init( blurRadius: 3, offset: .init(width: 2, height: -2), color: CGColor(gray: 0, alpha: 0.8) ) b1.addOverlay(a1) let image = try XCTUnwrap(b1.cgImage(size: CGSize(width: 64, height: 64), scale: 2)) let filename = "circular-gauge-gradient.png" let link = try imageStore.store(image.representation.png(), filename: filename) markdownText += "|\n\n" } } func testActivityGrid() throws { markdownText += "## Activity Grid\n\n" do { markdownText += "### Inline documentation\n\n" let surface = DSFSparklineSurface.Bitmap() let grid = DSFSparklineOverlay.ActivityGrid() grid.dataSource = DSFSparkline.StaticDataSource((0 ... 1000).map { _ in CGFloat.random(in: 0 ... 1) }, range: 0 ... 1) grid.verticalCellCount = 3 grid.cellStyle = .init( fillScheme: DSFSparkline.ActivityGrid.CellStyle.DefaultLight, borderColor: .black, borderWidth: 0.5, cellDimension: 5, cellSpacing: 0.5, cornerRadius: 1 ) surface.addOverlay(grid) let bitmap = surface.image(width: 150, height: 17, scale: 2)! // Generate an image with retina scale let filename = "activity-grid-mini.png" let link = try imageStore.store(bitmap.representation.png(scale: 2), filename: filename) markdownText += "\n" } markdownText += "\n" do { let bitmap = DSFSparklineSurface.Bitmap() // Create a bitmap surface let activity = DSFSparklineOverlay.ActivityGrid() let data: [CGFloat] = (0 ... 100).map { _ in CGFloat.random(in: 0...100) } activity.dataSource = .init(data) bitmap.addOverlay(activity) // And add the overlay to the surface. // Generate an image with retina scale let image = bitmap.image(width: 300, height: 100, scale: 2)! let filename = "activity-basic-1.png" let link = try imageStore.store(image.representation.png(scale: 2), filename: filename) markdownText += "\n" activity.verticalCellCount = 10 activity.cellDimension = 6 activity.cellSpacing = 1 activity.cellFillScheme = DSFSparkline.ActivityGrid.CellStyle.DefaultLight let image2 = bitmap.image(width: 300, height: 100, scale: 2)! let filename2 = "activity-basic-2.png" let link2 = try imageStore.store(image2.representation.png(scale: 2), filename: filename2) markdownText += "\n" } } } ================================================ FILE: Tests/DSFSparklineTests/utils/TestFilesContainer.swift ================================================ // // File.swift // // // Created by Darren Ford on 12/4/2023. // import Foundation class TestFilesContainer { // Note: DateFormatter is thread safe // See https://developer.apple.com/documentation/foundation/dateformatter#1680059 private static let iso8601Formatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX ISO8601 dateFormatter.dateFormat = "yyyy-MM-dd'T'HHmmssZ" return dateFormatter }() private let root: Subfolder var rootFolder: URL { self.root.folder } init(named name: String) throws { let baseURL = FileManager.default.temporaryDirectory.appendingPathComponent(name) let url = baseURL.appendingPathComponent(Self.iso8601Formatter.string(from: Date())) try? FileManager.default.removeItem(at: url) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) self.root = Subfolder(url) Swift.print("TestContainer(\(name) - Generated files at: \(url)") let latest = baseURL.appendingPathComponent("_latest") try? FileManager.default.removeItem(at: latest) try! FileManager.default.createSymbolicLink(at: latest, withDestinationURL: url) } func subfolder(with components: String...) throws -> Subfolder { var subfolder = self.rootFolder components.forEach { subfolder.appendPathComponent($0) } try FileManager.default.createDirectory(at: subfolder, withIntermediateDirectories: true) return Subfolder(subfolder) } class Subfolder { let folder: URL init(_ parent: URL) { self.folder = parent } init(named name: String, parent: URL) throws { let subf = parent.appendingPathComponent(name) try FileManager.default.createDirectory(at: subf, withIntermediateDirectories: true) self.folder = subf } func subfolder(with components: String...) throws -> Subfolder { var subfolder = self.folder components.forEach { subfolder.appendPathComponent($0) } try FileManager.default.createDirectory(at: subfolder, withIntermediateDirectories: true) return Subfolder(subfolder) } @discardableResult func write( _ data: Data, to file: String ) throws -> URL { let tempURL = self.folder.appendingPathComponent(file) try data.write(to: tempURL) return tempURL } @discardableResult func write( _ string: String, to file: String, encoding: String.Encoding = .utf8 ) throws -> URL { let tempURL = self.folder.appendingPathComponent(file) try string.write(to: tempURL, atomically: true, encoding: encoding) return tempURL } } } ================================================ FILE: Tools/sanity-check.sh ================================================ #!/bin/sh ## ## sanity-check.sh ## DSFSparklines ## ## Created by Darren Ford on 26/2/21. ## Copyright © 2021 Darren Ford. All rights reserved. ## ## MIT license ## ## Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated ## documentation files (the "Software"), to deal in the Software without restriction, including without limitation the ## rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to ## permit persons to whom the Software is furnished to do so, subject to the following conditions: ## ## The above copyright notice and this permission notice shall be included in all copies or substantial ## portions of the Software. ## ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE ## WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS ## OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR ## OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## # A simple script to build all the supported platforms to make sure I havent overlooked an issue on one of them. pushd . cd .. echo "Cleaning builds..." swift package clean ## ## iOS versions ## echo "Building iOS targets..." # Simulator xcrun swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk iphonesimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-ios11-simulator" # Latest iOS arm64 xcrun swift build -Xswiftc "-sdk" -Xswiftc $(xcrun --sdk iphoneos --show-sdk-path) -Xswiftc "-target" -Xswiftc "arm64-apple-ios`xcrun --sdk iphoneos --show-sdk-version`" # Earliest iOS xcrun swift build -Xswiftc "-sdk" -Xswiftc $(xcrun --sdk iphoneos --show-sdk-path) -Xswiftc "-target" -Xswiftc "arm64-apple-ios11" ## ## macOS versions ## echo "Building mac targets..." # Current macOS xcrun swift build -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx`xcrun --sdk macosx --show-sdk-version`" # Earliest macOS xcrun swift build -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.11" # Arm64 macOS xcrun swift build -Xswiftc "-target" -Xswiftc "arm64-apple-macosx`xcrun --sdk macosx --show-sdk-version`" ## ## tvOS versions ## echo "Building tvOS targets..." # Simulator xcrun swift build -Xswiftc "-sdk" -Xswiftc "`xcrun --sdk appletvsimulator --show-sdk-path`" -Xswiftc "-target" -Xswiftc "x86_64-apple-tvos`xcrun --sdk iphoneos --show-sdk-version`-simulator" # Latest xcrun swift build -Xswiftc "-sdk" -Xswiftc $(xcrun --sdk appletvos --show-sdk-path) -Xswiftc -target -Xswiftc "arm64-apple-tvos`xcrun --sdk iphoneos --show-sdk-version`" # Earliest iOS (v11) xcrun swift build -Xswiftc "-sdk" -Xswiftc $(xcrun --sdk appletvos --show-sdk-path) -Xswiftc -target -Xswiftc "arm64-apple-tvos11" # xcrun --sdk iphoneos --show-sdk-version # xcrun --sdk macosx --show-sdk-version # xcrun --sdk appletvos --show-sdk-version popd