{"id":15787131,"url":"https://github.com/aunnnn/rhlineplot","last_synced_at":"2025-06-28T15:35:31.715Z","repository":{"id":56921594,"uuid":"254593468","full_name":"aunnnn/RHLinePlot","owner":"aunnnn","description":"Line plot like in Robinhood app in SwiftUI","archived":false,"fork":false,"pushed_at":"2020-07-24T22:03:11.000Z","size":17540,"stargazers_count":280,"open_issues_count":3,"forks_count":20,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-05-07T08:49:52.736Z","etag":null,"topics":["lineplot","plot","robinhood","stock","swift","swiftui","ticker"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aunnnn.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-04-10T09:17:19.000Z","updated_at":"2025-01-27T07:20:26.000Z","dependencies_parsed_at":"2022-08-21T04:50:42.976Z","dependency_job_id":null,"html_url":"https://github.com/aunnnn/RHLinePlot","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aunnnn%2FRHLinePlot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aunnnn%2FRHLinePlot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aunnnn%2FRHLinePlot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aunnnn%2FRHLinePlot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aunnnn","download_url":"https://codeload.github.com/aunnnn/RHLinePlot/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252903095,"owners_count":21822370,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["lineplot","plot","robinhood","stock","swift","swiftui","ticker"],"created_at":"2024-10-04T21:05:28.807Z","updated_at":"2025-05-07T15:23:02.742Z","avatar_url":"https://github.com/aunnnn.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RHLinePlot\nLine plot like in Robinhood app, in SwiftUI\n\n![Demo](https://raw.githubusercontent.com/aunnnn/RHLinePlot/master/rhlineplot-demo.gif)\n\n*Looking for how to do the **moving price label effect**? [Another repo here.](https://github.com/aunnnn/MovingNumbersView)*\n\n*P.S. Of course this is not, in anyway, affiliated with Robinhood officially. This is just an attempt to replicate its UI and I don't own any of this design.*\n\nDemo stock API is from [Alphavantage](https://www.alphavantage.co).\n\n## Table of Contents\n- [Installation](#installation)\n- [APIs](#apis)\n  * [Without any interaction](#without-any-interaction)\n  * [With interactive elements](#with-interactive-elements)\n- [Configuration via Environment](#configuration-via-environment)\n- [TODO](#todo)\n- [Fun Solved Problems](#fun-solved-problems)\n  * [Drag gesture consumes all the drag](#drag-gesture-consumes-all-the-drag)\n  * [Indicator label must stick at the edge of plot](#indicator-label-must-stick-at-the-edge-of-plot)\n  * [Laser mode is unresponsive to segment highlighting](#laser-mode-is-unresponsive-to-segment-highlighting)\n  * [The blurry effect is clipped off at the edge of the plot frame with `drawingGroup()`](#the-blurry-effect-is-clipped-off-at-the-edge-of-the-plot-frame-with-drawinggroup)\n\n## Features :sparkles:\n- Support drag interaction, highlight active segment\n- Support glowing indicator, i.e. for real-time data\n- Customize animation duration, glowing size, labels etc.\n- Laser mode!\n\nPlay around with the example app to see possible customizations and the Robinhood-style view shown in the demo.\n\n## Installation\n### Cocoapods\n`pod install RHLinePlot`\n\nOr just use the source however you like. The library is in folder `RHLinePlot`.\n\n## APIs\n### Without any interaction\n```swift\nRHLinePlot(\n    values: valuesToPlot,\n    occupyingRelativeWidth: 0.8,\n    showGlowingIndicator: true,\n    lineSegmentStartingIndices: segments,\n    activeSegment: 2,\n    customLatestValueIndicator: {\n      // Return a custom glowing indicator if you want\n    }\n)\n```\n\nNotes:\n- `segments` is the beginning indices of each segment. I.e. `values = [1,2,3,4,3,2,1,2,3,4]` and `segments = [0,4,8]` means there are three segments in this line plot: 0-3, 4-7, 8-9.\n- `occupyingRelativeWidth = 0.8` is to plot 80% of the plot canvas. This is useful to simulate realtime data. I.e. compute the current hour of the day relative to the 24-hour timeframe and use that ratio. By default this is 1.0.\n\n### With interactive elements\n```swift\nRHInteractiveLinePlot(\n    values: values,\n    occupyingRelativeWidth: 0.8,\n    showGlowingIndicator: true,\n    lineSegmentStartingIndices: segments,\n    didSelectValueAtIndex: { index in\n      // Do sth useful with index...\n},\n    customLatestValueIndicator: {\n      // Custom indicator...\n},\n    valueStickLabel: { value in\n      // Label above the value stick...\n})\n```\n## Configuration via Environment\n\nTo customize:\n```swift\nYourView\n.environment(\\.rhLinePlotConfig, RHLinePlotConfig.default.custom(f: { (c) in\n    c.useLaserLightLinePlotStyle = isLaserModeOn\n}))\n```\n\nFull config:\n```swift\npublic struct RHLinePlotConfig {\n\n    /// Width of the rectangle holding the glowing indicator (i.e. not `radius`, but rather `glowingIndicatorWidth = 2*radius`). Default is `8.0`\n    public var glowingIndicatorWidth: CGFloat = 8.0\n    \n    /// Line width of the line plot. Default is `1.5`\n    public var plotLineWidth: CGFloat = 1.5\n    \n    /// If all values are equal, we will draw a straight line. Default is 0.5 which draws a line at the middle.\n    public var relativeYForStraightLine: CGFloat = 0.5\n    \n    /// Opacity of unselected segment. Default is `0.3`.\n    public var opacityOfUnselectedSegment: Double = 0.3\n    \n    /// Animation duration of opacity on select/unselect a segment. Default is `0.1`.\n    public var segmentSelectionAnimationDuration: Double = 0.1\n    \n    /// Scale the fading background of glowing indicator to specified value. Default is `5` (scale to 5 times bigger before disappear)\n    public var glowingIndicatorBackgroundScaleEffect: CGFloat = 5\n    \n    public var glowingIndicatorDelayBetweenGlow: Double = 0.5\n    public var glowingIndicatorGlowAnimationDuration: Double = 0.8\n    \n    /// Use laser stroke mode to plot lines.\n    ///\n    /// Note that your plot will be automatically shrinked so that the blurry part fits inside the canvas.\n    public var useLaserLightLinePlotStyle: Bool = false\n    \n    /// Use drawing group for laser light mode.\n    ///\n    /// This will increase responsiveness if there's a lot of segments.\n    /// **But, the blurry parts will be clipped off the canvas bounds.**\n//    public var useDrawingGroupForLaserLightLinePlotStyle: Bool = false\n    \n    /// The edges to fit the line strokes within canvas. This interacts with `plotLineWidth`. Default is `[]`.\n    ///\n    /// By default only the line skeletons (*paths*) exactly fits in the canvas,** without considering the `plotLineWidth`**.\n    /// So when you increase the line width, the edge of the extreme values could go out of the canvas.\n    /// You can provide a set of edges to consider to adjust to fit in canvas.\n    public var adjustedEdgesToFitLineStrokeInCanvas: Edge.Set = []\n    \n    // MARK:- RHInteractiveLinePlot\n    \n    public var valueStickWidth: CGFloat = 1.2\n    public var valueStickColor: Color = .gray\n    \n    /// Padding from the highest point of line plot to value stick. If `0`, the top of value stick will be at the same level of the highest point in plot.\n    public var valueStickTopPadding: CGFloat = 28\n    \n    /// Padding from the lowest point of line plot to value stick. If `0`, the end of value stick will be at the same level of the lowest point in plot.\n    public var valueStickBottomPadding: CGFloat = 28\n    \n    public var spaceBetweenValueStickAndStickLabel: CGFloat = 8\n\n    /// Duration of long press before the value stick is activated and draggable.\n    ///\n    /// The more it is, the less likely the interactive part is activated accidentally on scroll view. Default is `0.1`.\n    ///\n    /// There's some lower-bound on this value that I guess coming from delaysContentTouches of\n    /// the ScrollView. So if this is `0`, iit won't immediately activate the long press (but quickly horizontal pan will).\n    public var minimumPressDurationToActivateInteraction: Double = 0.1\n    \n    public static let `default` = RHLinePlotConfig()\n    \n    public func custom(f: (inout RHLinePlotConfig) -\u003e Void) -\u003e RHLinePlotConfig {\n        var new = self\n        f(\u0026new)\n        return new\n    }\n}\n```\n## TODO\n- Support two finger drag to compare between two values on the plot.\n- ~Dragging in the interactive plot consumes all the gestures. If you put it in a `ScrollView`, you can't scroll the scroll view in the interactive plot area, you'd be interacting with the plot instead.~ - Fixed by using a clear [proxy view](https://github.com/aunnnn/RHLinePlot/blob/master/RHLinePlot/PressAndHorizontalDragGesture.swift) to handle gestures\n\n## Fun Solved Problems\n\n### Drag gesture consumes all the drag\n\u003e Problem: So you can't put the plot in a scroll view and scroll down on the plot. I tried adding `LongPressGesture` like in Apple's tutorial, but looks like it too consumes gesture exclusively if put under a scroll view.\n\nSolution: This is currently fixed by putting a [proxy view](https://github.com/aunnnn/RHLinePlot/blob/master/RHLinePlot/PressAndHorizontalDragGesture.swift) that implements custom long press gesture detection.\n\n### Indicator label must stick at the edge of plot\n\n\u003e Problem: To stick the indicator label (`valueStickLabel`) translation at the horizontal edge of the plot, we need to know the label width. However its content is dynamic, it could be anything a user set.\n\nSolution: This is fixed by having two `valueStickLabel`s. First one is used for sizing and hidden away. The second one is overlaid on the first with `GeometryReader`, so we know the final size of the label, ready to calculate the translation next (where we could clamp its offset with the width). \n\n```swift\n// Indicator Label\n//\n// HACK: Get a dynamic size of the indicator label with `overlay` + `GeometryReader`.\n// Hide the bottom one (just use it for sizing), then show the overlaid one.\nvalueStickLabel.opacity(0)\n    .overlay(\n        GeometryReader { labelProxy in\n            valueStickLabel\n                .transformEffect(labelTranslation(labelProxy: labelProxy))\n        }.opacity(stickAndLabelOpacity))\n```\n\n![StickylabelDemo](https://raw.githubusercontent.com/aunnnn/RHLinePlot/master/rhlineplot-stickylabeldemo.gif)\n\n### Laser mode is unresponsive to segment highlighting\n\u003e Problem: The laser mode puts 3 blur effects on each segment of the line plot, so it can be unresponsive to drag around fast and animate opacity of different parts.\n\nSolution: Just use `drawingGroup()`. This helps a lot. However, this introduces the next issue:\n\n### The blurry effect is clipped off at the edge of the plot frame with `drawingGroup()`\n\u003e Problem: Using `drawingGroup()` seems to apply the `clipsToBounds`-like effect on the blurry part, and it doesn't look nice.\n\n![BlurryProblemDemo](https://raw.githubusercontent.com/aunnnn/RHLinePlot/master/rhlineplot-blurryproblemdemo.gif)\n\nSolution: [Inset the plot canvas](https://github.com/aunnnn/RHLinePlot/blob/master/RHLinePlot/RHLinePlot%2BSegmented.swift) relative to the `plotLineWidth` config (the larger the value, the larger the blurry blob) so that `drawingGroup` has more space to draw and cache image:\n```swift\nlet adjustedEachBorderDueToBlur: CGFloat = {\n    if rhLinePlotConfig.useLaserLightLinePlotStyle {\n        return 7.5 * rhLinePlotConfig.plotLineWidth // Magic number accounts for blurring\n    } else {\n        return 0\n    }\n}()\nlet largerCanvas = canvasFrame.insetBy(dx: -adjustedEachBorderDueToBlur, dy: -adjustedEachBorderDueToBlur)\n```\n![BlurryFixedDemo](https://raw.githubusercontent.com/aunnnn/RHLinePlot/master/rhlineplot-blurryfixeddemo.gif)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faunnnn%2Frhlineplot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faunnnn%2Frhlineplot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faunnnn%2Frhlineplot/lists"}