diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..1bc6bb7 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,37 @@ +name: Documentation + +on: + pull_request: + paths: + - ".github/workflows/docs.yml" + - "CHANGELOG.md" + - "Package.swift" + - "README.md" + - "Sources/**" + - "docs/**" + push: + branches: + - main + paths: + - ".github/workflows/docs.yml" + - "CHANGELOG.md" + - "Package.swift" + - "README.md" + - "Sources/**" + - "docs/**" + +jobs: + docbuild: + runs-on: macos-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Build DocC catalog + run: | + xcodebuild docbuild \ + -scheme SimpleChart \ + -destination 'platform=macOS' \ + -derivedDataPath .build/DerivedData \ + CODE_SIGNING_ALLOWED=NO diff --git a/CHANGELOG.md b/CHANGELOG.md index 956f484..27b1bb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ - Public interaction helper types: `SCChartSelectionState`, `SCChartInspectionOverlay`, `SCChartScrollBehavior`, `SCChartGestureConfiguration`, and `SCChartHoverState`. - Viewport coordination helpers for centered, clamped, and zoomed visible-window updates. - Focused tests for the interaction-helper slice, including interaction helper state, wrapper configuration, and viewport utilities. +- Zoomable navigation helpers: `SCChartTimeViewport`, `SCChartZoomBehavior`, and `SCChartNavigationCoordinator`. +- Viewport-driven zoom support for `SCScrollableLineChart` and `SCScrollableTimeSeriesChart`, including focused tests for indexed and time-series navigation state. - Helper-style annotation presets for caption, badge, and formatted value-label rendering via `SCChartAnnotation`. - Availability-gated `SCSelectableSectorChart` and `SCSelectableDonutChart` wrappers for `SectorMark` angle-selection workflows on newer OS versions. - Focused tests for selectable sector/donut wrapper configuration and annotation helper presets. @@ -33,6 +35,8 @@ - Visible-domain and scroll-behavior presets for analytics- and finance-style windows. - Focused tests for inspection-wrapper configuration and time-series selection state. - A new `docs/tutorials/` learning path with sequenced tutorials for first chart setup, helper-first data construction, interactions, time-series, composed charts, and legacy migration. +- A first-party `SimpleChart.docc` catalog with package overview and focused articles for getting started, wrapper selection, interactive charts, and legacy migration. +- A contributor-facing `docs/editor-support.md` guide covering Xcode Quick Help, local DocC builds, and `sourcekit-lsp` usage. - Availability-gated vectorized plot wrappers: `SCNativeLinePlotChart`, `SCNativeAreaPlotChart`, `SCNativeBarPlotChart`, `SCNativePointPlotChart`, and `SCNativeRectanglePlotChart`. - Availability-gated function and parametric plot wrappers: `SCNativeFunctionLinePlotChart`, `SCNativeParametricLinePlotChart`, and `SCNativeFunctionAreaPlotChart`. - Availability-gated 3D wrappers and helpers: `SCChart3DPoint`, `SCChart3DPoseStyle`, `SCChart3DSeriesStyle`, `SCNative3DPointChart`, `SCNative3DRectangleChart`, `SCNative3DRuleChart`, and `SCNativeSurfacePlotChart`. @@ -47,7 +51,8 @@ ### Changed -- Added Xcode Quick Help documentation to the first-discovery public API surface so the core models, styles, composition entrypoints, and primary wrappers are easier to explore directly from Xcode. +- Added Xcode Quick Help documentation across the full exposed package API surface, including the native wrapper/helper layer and the deprecated compatibility layer, so every public type and entry point is easier to discover directly from Xcode. +- Added richer Quick Help coverage for the zoom/navigation surface, including `SCChartTimeViewport`, `SCChartScrollBehavior`, `SCChartZoomBehavior`, and the scrollable interactive wrappers. - Raised minimum supported platform versions to Swift Charts baselines: - iOS 16+ - macOS 13+ @@ -66,7 +71,9 @@ - Refactored composed-chart annotation rendering and sector-selection overlays to use the shared `SCChartAnnotationLabelView` helper path. - Refactored selection and hover inspection rendering to share the same callout and value-label helper path. - Reworked the documentation entrypoint around a real quick-start flow, with dedicated getting-started and chart-selection guides for new users. +- Added repository-level documentation discovery links for the DocC catalog and editor support guide, plus a CI docbuild path for keeping the package documentation healthy. - Expanded the README wrapper catalog and coverage/status sections to include the vectorized plot and 3D chart surface. +- Updated the scrolling and time-series docs to show viewport-based zoom configuration for indexed and date-based interactive wrappers. ### Deprecated diff --git a/README.md b/README.md index feb0a31..b25a9ea 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,10 @@ If you are new to the package, start here instead of reading the full README top 3. Use the focused guides below when you need the next step - [Getting Started](docs/getting-started.md) +- [DocC Catalog Source](Sources/SimpleChart/SimpleChart.docc/SimpleChart.md) - [Tutorials](docs/tutorials/README.md) - [Chart Selection Guide](docs/chart-selection-guide.md) +- [Editor Support](docs/editor-support.md) - [Migration from the legacy API](#migration-from-the-legacy-api) ## Platform requirements @@ -41,10 +43,10 @@ The native layer is built around a small shared model surface: - `SCChartSectorSegment` for sector and donut charts - `SCChartBarGroup` and `SCChartStackSegment` for grouped and stacked bars - `SCChartTimePoint` for time-series datasets -- `SCChartVisibleDomain` and `SCChartViewport` for scroll-window helpers +- `SCChartVisibleDomain`, `SCChartViewport`, and `SCChartTimeViewport` for scroll-window helpers - `SCChartNumericValueFormat` and `SCChartDateValueFormat` for helper-style axis formatting - `SCChartSelection` for wrapper-managed chart selection state -- `SCChartSelectionState`, `SCChartInspectionOverlay`, `SCChartScrollBehavior`, `SCChartGestureConfiguration`, and `SCChartHoverState` for reusable interaction configuration +- `SCChartSelectionState`, `SCChartInspectionOverlay`, `SCChartScrollBehavior`, `SCChartZoomBehavior`, `SCChartGestureConfiguration`, and `SCChartHoverState` for reusable interaction configuration - `SCChartMark` for composed mark-based chart definitions - `SCChartAnnotationStyle`, `SCChartAnnotation`, `SCChartOverlay`, `SCChartScale`, and `SCChartComposition` for reusable composed-chart helpers @@ -163,6 +165,14 @@ let scrollBehavior = SCChartScrollBehavior.continuous(.points(7)) let timeWindow = SCChartScrollBehavior.timeWindow(hours: 24) let analyticsScroll = SCChartScrollBehavior.analytics(points: 21) let financeScroll = SCChartScrollBehavior.finance(tradingDays: 10) +let timeViewport = SCChartTimeViewport.starting( + at: Date(timeIntervalSince1970: 1_700_000_000), + duration: 60 * 60 * 24 +) +let zoomBehavior = SCChartZoomBehavior( + minimumVisibleLength: 3, + maximumVisibleLength: 14 +) let gestures = SCChartGestureConfiguration.interactive let compositionScale = SCChartScale( xVisibleDomain: .points(6), @@ -356,7 +366,10 @@ struct RevenueExplorer: View { @State private var selectionState = SCChartSelectionState() @State private var hoverState: SCChartHoverState? @State private var viewport = SCChartViewport.starting(at: 0, length: 7) - @State private var scrollPosition = Date(timeIntervalSince1970: 1_700_000_000) + @State private var timeViewport = SCChartTimeViewport.starting( + at: Date(timeIntervalSince1970: 1_700_000_000), + duration: 7_200 + ) let points = SCChartPoint.make( labeledValues: [("Mon", 12), ("Tue", 18), ("Wed", 15), ("Thu", 20)] @@ -394,14 +407,16 @@ struct RevenueExplorer: View { points: points, viewport: $viewport, scrollBehavior: .continuous(.points(3)), + zoomBehavior: .init(minimumVisibleLength: 2, maximumVisibleLength: 7), gestureConfiguration: .interactive, yAxisFormat: .number(precision: 0) ) SCScrollableTimeSeriesChart( points: history, - scrollPosition: $scrollPosition, + viewport: $timeViewport, scrollBehavior: .timeWindow(seconds: 7_200), + zoomBehavior: .init(minimumVisibleLength: 1_800, maximumVisibleLength: 14_400), xAxisFormat: .hourMinute, gestureConfiguration: .scrollOnly, yAxisFormat: .compact diff --git a/Sources/SimpleChart/Native/Charts/SCHoverableCharts.swift b/Sources/SimpleChart/Native/Charts/SCHoverableCharts.swift index 0a1c624..903eaa9 100644 --- a/Sources/SimpleChart/Native/Charts/SCHoverableCharts.swift +++ b/Sources/SimpleChart/Native/Charts/SCHoverableCharts.swift @@ -9,6 +9,7 @@ import Charts import SwiftUI @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A line chart wrapper with pointer-hover inspection support. public struct SCHoverableLineChart: View { public let points: [SCChartPoint] public let seriesStyle: SCChartSeriesStyle @@ -20,6 +21,7 @@ public struct SCHoverableLineChart: View { @Binding private var hoverState: SCChartHoverState? + /// Creates a hoverable line chart bound to external hover state. public init( points: [SCChartPoint], hoverState: Binding, @@ -153,6 +155,7 @@ public struct SCHoverableLineChart: View { } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A bar chart wrapper with pointer-hover inspection support. public struct SCHoverableBarChart: View { public let points: [SCChartPoint] public let seriesStyle: SCChartSeriesStyle @@ -163,6 +166,7 @@ public struct SCHoverableBarChart: View { @Binding private var hoverState: SCChartHoverState? + /// Creates a hoverable bar chart bound to external hover state. public init( points: [SCChartPoint], hoverState: Binding, @@ -270,6 +274,7 @@ public struct SCHoverableBarChart: View { } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A scatter chart wrapper with pointer-hover inspection support. public struct SCHoverableScatterChart: View { public let points: [SCChartScatterPoint] public let seriesStyle: SCChartSeriesStyle @@ -280,6 +285,7 @@ public struct SCHoverableScatterChart: View { @Binding private var hoverState: SCChartHoverState? + /// Creates a hoverable scatter chart bound to external hover state. public init( points: [SCChartScatterPoint], hoverState: Binding, diff --git a/Sources/SimpleChart/Native/Charts/SCInspectionWrappers.swift b/Sources/SimpleChart/Native/Charts/SCInspectionWrappers.swift index a215f26..c627ac9 100644 --- a/Sources/SimpleChart/Native/Charts/SCInspectionWrappers.swift +++ b/Sources/SimpleChart/Native/Charts/SCInspectionWrappers.swift @@ -313,6 +313,7 @@ public struct SCCrosshairScatterChart: View { } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A time-series wrapper that always shows inspector-style selection callouts. public struct SCInspectorTimeSeriesChart: View { public let points: [SCChartTimePoint] public let seriesStyle: SCChartSeriesStyle @@ -326,6 +327,7 @@ public struct SCInspectorTimeSeriesChart: View { @Binding private var selection: SCChartSelection? + /// Creates an inspector-style time-series wrapper bound directly to an optional selection. public init( points: [SCChartTimePoint], selection: Binding, @@ -367,6 +369,7 @@ public struct SCInspectorTimeSeriesChart: View { } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A time-series wrapper that always shows crosshair-style selection guides. public struct SCCrosshairTimeSeriesChart: View { public let points: [SCChartTimePoint] public let seriesStyle: SCChartSeriesStyle @@ -381,6 +384,7 @@ public struct SCCrosshairTimeSeriesChart: View { @Binding private var selection: SCChartSelection? + /// Creates a crosshair-style time-series wrapper bound directly to an optional selection. public init( points: [SCChartTimePoint], selection: Binding, diff --git a/Sources/SimpleChart/Native/Charts/SCNative3DCharts.swift b/Sources/SimpleChart/Native/Charts/SCNative3DCharts.swift index 4c3756f..c1701e7 100644 --- a/Sources/SimpleChart/Native/Charts/SCNative3DCharts.swift +++ b/Sources/SimpleChart/Native/Charts/SCNative3DCharts.swift @@ -7,14 +7,17 @@ import SwiftUI +#if compiler(>=6.3) @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) +/// A 3D point chart wrapper built on the package's composed 3D mark layer. public struct SCNative3DPointChart: View { public let points: [SCChart3DPoint] public let style: SCChart3DSeriesStyle public let pose: SCChart3DPoseStyle + /// Creates a 3D point chart from prebuilt 3D points. public init( points: [SCChart3DPoint], style: SCChart3DSeriesStyle = .init(), @@ -36,11 +39,13 @@ public struct SCNative3DPointChart: View { @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) +/// A 3D rectangle chart wrapper built on the package's composed 3D mark layer. public struct SCNative3DRectangleChart: View { public let points: [SCChart3DPoint] public let style: SCChart3DSeriesStyle public let pose: SCChart3DPoseStyle + /// Creates a 3D rectangle chart from prebuilt 3D points. public init( points: [SCChart3DPoint], style: SCChart3DSeriesStyle = .init(), @@ -62,11 +67,13 @@ public struct SCNative3DRectangleChart: View { @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) +/// A 3D rule chart wrapper built on the package's composed 3D mark layer. public struct SCNative3DRuleChart: View { public let points: [SCChart3DPoint] public let style: SCChart3DSeriesStyle public let pose: SCChart3DPoseStyle + /// Creates a 3D rule chart from prebuilt 3D points. public init( points: [SCChart3DPoint], style: SCChart3DSeriesStyle = .init(), @@ -88,6 +95,7 @@ public struct SCNative3DRuleChart: View { @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) +/// A 3D surface chart wrapper that samples a function across the rendered surface. public struct SCNativeSurfacePlotChart: View { public let xTitle: String public let yTitle: String @@ -96,6 +104,7 @@ public struct SCNativeSurfacePlotChart: View { public let pose: SCChart3DPoseStyle public let function: @Sendable (Double, Double) -> Double + /// Creates a 3D surface chart from axis titles, style, and a z-value function. public init( xTitle: String, yTitle: String, @@ -127,3 +136,4 @@ public struct SCNativeSurfacePlotChart: View { ) } } +#endif diff --git a/Sources/SimpleChart/Native/Charts/SCNativeAreaChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeAreaChart.swift index b81b090..da26a43 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeAreaChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeAreaChart.swift @@ -7,6 +7,7 @@ import SwiftUI +/// A ready-made single-series categorical area chart backed by composed marks. public struct SCNativeAreaChart: View { public let points: [SCChartPoint] public let seriesStyle: SCChartSeriesStyle @@ -14,6 +15,7 @@ public struct SCNativeAreaChart: View { public let domain: SCChartDomain? public let referenceLines: [SCChartReferenceLine] + /// Creates an area chart from prebuilt categorical points. public init( points: [SCChartPoint], seriesStyle: SCChartSeriesStyle = .area(), @@ -28,6 +30,7 @@ public struct SCNativeAreaChart: View { self.referenceLines = referenceLines } + /// Creates an area chart from floating-point values and optional labels. public init( values: [T], labels: [String]? = nil, diff --git a/Sources/SimpleChart/Native/Charts/SCNativeBandChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeBandChart.swift index 0d25c3a..83288d8 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeBandChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeBandChart.swift @@ -8,6 +8,7 @@ import Charts import SwiftUI +/// A chart that renders one or more highlighted y-range bands across shared categories. public struct SCNativeBandChart: View { public let categories: [String] public let bands: [SCChartBand] @@ -18,6 +19,7 @@ public struct SCNativeBandChart: View { public let plotStyle: SCChartPlotStyle public let domain: SCChartDomain? + /// Creates a band chart from explicit categories and prebuilt bands. public init( categories: [String], bands: [SCChartBand], @@ -44,6 +46,7 @@ public struct SCNativeBandChart: View { ) } + /// Creates a band chart from floating-point band tuples. public init( categories: [String], bands: [(String, T, T)], @@ -72,6 +75,7 @@ public struct SCNativeBandChart: View { ) } + /// Creates a band chart from integer band tuples. public init( categories: [String], bands: [(String, T, T)], diff --git a/Sources/SimpleChart/Native/Charts/SCNativeGoalChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeGoalChart.swift index d29868b..2ab05a8 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeGoalChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeGoalChart.swift @@ -7,6 +7,7 @@ import SwiftUI +/// A composed chart that overlays goal reference lines on top of bar values. public struct SCNativeGoalChart: View { public let points: [SCChartPoint] public let goal: SCChartReferenceLine @@ -15,6 +16,7 @@ public struct SCNativeGoalChart: View { public let axesStyle: SCChartAxesStyle public let domain: SCChartDomain? + /// Creates a goal chart from prebuilt categorical points and goal lines. public init( points: [SCChartPoint], goal: SCChartReferenceLine, @@ -32,6 +34,7 @@ public struct SCNativeGoalChart: View { self.domain = domain ?? .auto(values: points.map(\.value) + referenceValues, baseZero: true) } + /// Creates a goal chart from floating-point values, optional labels, and goal lines. public init( values: [T], labels: [String]? = nil, diff --git a/Sources/SimpleChart/Native/Charts/SCNativeGroupedAreaChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeGroupedAreaChart.swift index ff9d8f9..ac146d5 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeGroupedAreaChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeGroupedAreaChart.swift @@ -8,6 +8,7 @@ import Charts import SwiftUI +/// A multi-series grouped area chart built from named line-series models. public struct SCNativeGroupedAreaChart: View { public let series: [SCChartLineSeries] public let axesStyle: SCChartAxesStyle @@ -16,6 +17,7 @@ public struct SCNativeGroupedAreaChart: View { public let foregroundStyleScale: SCChartForegroundStyleScale public let referenceLines: [SCChartReferenceLine] + /// Creates a grouped area chart from prebuilt line-series values. public init( series: [SCChartLineSeries], axesStyle: SCChartAxesStyle = .standard(), diff --git a/Sources/SimpleChart/Native/Charts/SCNativeGroupedBarChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeGroupedBarChart.swift index 60d907b..eb014bc 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeGroupedBarChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeGroupedBarChart.swift @@ -8,6 +8,7 @@ import Charts import SwiftUI +/// A grouped bar chart that compares multiple series within each category. public struct SCNativeGroupedBarChart: View { public let groups: [SCChartBarGroup] public let axesStyle: SCChartAxesStyle @@ -15,6 +16,7 @@ public struct SCNativeGroupedBarChart: View { public let legend: SCChartLegend public let foregroundStyleScale: SCChartForegroundStyleScale + /// Creates a grouped bar chart from prebuilt grouped-bar models. public init( groups: [SCChartBarGroup], axesStyle: SCChartAxesStyle = .standard(), @@ -31,6 +33,7 @@ public struct SCNativeGroupedBarChart: View { self.foregroundStyleScale = foregroundStyleScale ?? .categorical(seriesNames, palette: palette) } + /// Creates a grouped bar chart from floating-point grouped tuples. public init( groups: [(String, [(String, T)])], axesStyle: SCChartAxesStyle = .standard(), @@ -49,6 +52,7 @@ public struct SCNativeGroupedBarChart: View { ) } + /// Creates a grouped bar chart from integer grouped tuples. public init( groups: [(String, [(String, T)])], axesStyle: SCChartAxesStyle = .standard(), diff --git a/Sources/SimpleChart/Native/Charts/SCNativeHistogramChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeHistogramChart.swift index a2f79ad..9afd19a 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeHistogramChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeHistogramChart.swift @@ -8,12 +8,14 @@ import Charts import SwiftUI +/// A histogram chart that can render either pre-binned or raw numeric input. public struct SCNativeHistogramChart: View { public let bins: [SCHistogramBin] public let seriesStyle: SCChartSeriesStyle public let axesStyle: SCChartAxesStyle public let domain: SCChartDomain? + /// Creates a histogram chart from precomputed bins. public init( bins: [SCHistogramBin], seriesStyle: SCChartSeriesStyle = SCChartSeriesStyle(), @@ -26,6 +28,7 @@ public struct SCNativeHistogramChart: View { self.domain = domain } + /// Creates a histogram chart by binning raw floating-point values. public init( values: [Double], binCount: Int = 10, @@ -42,6 +45,7 @@ public struct SCNativeHistogramChart: View { ) } + /// Creates a histogram chart by binning raw integer values. public init( values: [T], binCount: Int = 10, diff --git a/Sources/SimpleChart/Native/Charts/SCNativeMultiLineChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeMultiLineChart.swift index 963c799..a655a61 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeMultiLineChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeMultiLineChart.swift @@ -8,12 +8,14 @@ import Charts import SwiftUI +/// A multi-line chart that renders multiple named series on a shared categorical axis. public struct SCNativeMultiLineChart: View { public let series: [SCChartLineSeries] public let axesStyle: SCChartAxesStyle public let domain: SCChartDomain? public let referenceLines: [SCChartReferenceLine] + /// Creates a multi-line chart from prebuilt line-series values. public init( series: [SCChartLineSeries], axesStyle: SCChartAxesStyle = .standard(), diff --git a/Sources/SimpleChart/Native/Charts/SCNativePlotCharts.swift b/Sources/SimpleChart/Native/Charts/SCNativePlotCharts.swift index b74ff6e..b203c18 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativePlotCharts.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativePlotCharts.swift @@ -9,12 +9,14 @@ import Charts import SwiftUI @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +/// A vectorized line-plot wrapper for numeric x/y point series. public struct SCNativeLinePlotChart: View { public let points: [SCChartPlotPoint] public let seriesStyle: SCChartSeriesStyle public let axesStyle: SCChartAxesStyle public let domain: SCChartDomain? + /// Creates a line plot from prebuilt numeric plot points. public init( points: [SCChartPlotPoint], seriesStyle: SCChartSeriesStyle = .line(), @@ -27,6 +29,7 @@ public struct SCNativeLinePlotChart: View { self.domain = domain ?? .auto(values: points.map(\.y)) } + /// Creates a line plot from floating-point `(x, y)` tuples. public init( points: [(T, U)], seriesName: String? = nil, @@ -73,6 +76,7 @@ public struct SCNativeLinePlotChart: View { } @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +/// A vectorized area-plot wrapper for numeric x/y point series. public struct SCNativeAreaPlotChart: View { public let points: [SCChartPlotPoint] public let seriesStyle: SCChartSeriesStyle @@ -80,6 +84,7 @@ public struct SCNativeAreaPlotChart: View { public let domain: SCChartDomain? public let stacking: SCChartPlotStacking + /// Creates an area plot from prebuilt numeric plot points. public init( points: [SCChartPlotPoint], seriesStyle: SCChartSeriesStyle = .area(), @@ -124,6 +129,7 @@ public struct SCNativeAreaPlotChart: View { } @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +/// A vectorized bar-plot wrapper for point, span, or range plot input. public struct SCNativeBarPlotChart: View { public let points: [SCChartPlotPoint] public let spans: [SCChartPlotSpan] @@ -135,6 +141,7 @@ public struct SCNativeBarPlotChart: View { public let height: SCChartPlotDimension public let stacking: SCChartPlotStacking + /// Creates a bar plot from point-based plot input. public init( points: [SCChartPlotPoint], seriesStyle: SCChartSeriesStyle = .bar(), @@ -155,6 +162,7 @@ public struct SCNativeBarPlotChart: View { self.stacking = stacking } + /// Creates a bar plot from span-based plot input. public init( spans: [SCChartPlotSpan], seriesStyle: SCChartSeriesStyle = .bar(), @@ -173,6 +181,7 @@ public struct SCNativeBarPlotChart: View { self.stacking = .standard } + /// Creates a bar plot from range-based plot input. public init( ranges: [SCChartPlotRange], seriesStyle: SCChartSeriesStyle = .bar(), @@ -239,12 +248,14 @@ public struct SCNativeBarPlotChart: View { } @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +/// A vectorized point-plot wrapper for numeric x/y point series. public struct SCNativePointPlotChart: View { public let points: [SCChartPlotPoint] public let seriesStyle: SCChartSeriesStyle public let axesStyle: SCChartAxesStyle public let domain: SCChartDomain? + /// Creates a point plot from prebuilt numeric plot points. public init( points: [SCChartPlotPoint], seriesStyle: SCChartSeriesStyle = .scatter(), @@ -275,6 +286,7 @@ public struct SCNativePointPlotChart: View { } @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +/// A vectorized rectangle-plot wrapper for explicit numeric rectangles. public struct SCNativeRectanglePlotChart: View { public let rectangles: [SCChartPlotRectangle] public let seriesStyle: SCChartSeriesStyle @@ -283,6 +295,7 @@ public struct SCNativeRectanglePlotChart: View { public let width: SCChartPlotDimension public let height: SCChartPlotDimension + /// Creates a rectangle plot from prebuilt plot rectangles. public init( rectangles: [SCChartPlotRectangle], seriesStyle: SCChartSeriesStyle = .bar(), @@ -318,6 +331,7 @@ public struct SCNativeRectanglePlotChart: View { } @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +/// A function-based line-plot wrapper that samples a scalar function over x. public struct SCNativeFunctionLinePlotChart: View { public let xTitle: String public let yTitle: String @@ -326,6 +340,7 @@ public struct SCNativeFunctionLinePlotChart: View { public let axesStyle: SCChartAxesStyle public let function: @Sendable (Double) -> Double + /// Creates a function line plot from axis titles and a scalar function. public init( xTitle: String, yTitle: String, @@ -356,6 +371,7 @@ public struct SCNativeFunctionLinePlotChart: View { } @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +/// A function-based line-plot wrapper for parametric curves. public struct SCNativeParametricLinePlotChart: View { public let xTitle: String public let yTitle: String @@ -365,6 +381,7 @@ public struct SCNativeParametricLinePlotChart: View { public let axesStyle: SCChartAxesStyle public let function: @Sendable (Double) -> (x: Double, y: Double) + /// Creates a parametric line plot from axis titles, parameter domain, and a curve function. public init( xTitle: String, yTitle: String, @@ -403,6 +420,7 @@ public struct SCNativeParametricLinePlotChart: View { } @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) +/// A function-based area-plot wrapper for single-value or band-value functions. public struct SCNativeFunctionAreaPlotChart: View { public let xTitle: String public let yTitle: String @@ -414,6 +432,7 @@ public struct SCNativeFunctionAreaPlotChart: View { public let singleFunction: (@Sendable (Double) -> Double)? public let bandFunction: (@Sendable (Double) -> (yStart: Double, yEnd: Double))? + /// Creates a function area plot from a scalar function sampled over x. public init( xTitle: String, yTitle: String, @@ -433,6 +452,7 @@ public struct SCNativeFunctionAreaPlotChart: View { self.bandFunction = nil } + /// Creates a function area plot from a band function sampled over x. public init( xTitle: String, yStartTitle: String, diff --git a/Sources/SimpleChart/Native/Charts/SCNativeQuadCurveChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeQuadCurveChart.swift index fb4d9de..f92ff16 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeQuadCurveChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeQuadCurveChart.swift @@ -7,6 +7,7 @@ import SwiftUI +/// A ready-made single-series quadratic-curve chart backed by Swift Charts. public struct SCNativeQuadCurveChart: View { public let points: [SCChartPoint] public let seriesStyle: SCChartSeriesStyle @@ -14,6 +15,7 @@ public struct SCNativeQuadCurveChart: View { public let domain: SCChartDomain? public let referenceLines: [SCChartReferenceLine] + /// Creates a quadratic-curve chart from prebuilt categorical points. public init( points: [SCChartPoint], seriesStyle: SCChartSeriesStyle = SCChartSeriesStyle(interpolation: .catmullRom), @@ -28,6 +30,7 @@ public struct SCNativeQuadCurveChart: View { self.referenceLines = referenceLines } + /// Creates a quadratic-curve chart from floating-point values and optional labels. public init( values: [T], labels: [String]? = nil, @@ -48,6 +51,7 @@ public struct SCNativeQuadCurveChart: View { ) } + /// Creates a quadratic-curve chart from integer values and optional labels. public init( values: [T], labels: [String]? = nil, @@ -68,6 +72,7 @@ public struct SCNativeQuadCurveChart: View { ) } + /// Creates a quadratic-curve chart from labeled floating-point values. public init( labeledValues: [(String, T)], seriesStyle: SCChartSeriesStyle = SCChartSeriesStyle(interpolation: .catmullRom), @@ -87,6 +92,7 @@ public struct SCNativeQuadCurveChart: View { ) } + /// Creates a quadratic-curve chart from labeled integer values. public init( labeledValues: [(String, T)], seriesStyle: SCChartSeriesStyle = SCChartSeriesStyle(interpolation: .catmullRom), diff --git a/Sources/SimpleChart/Native/Charts/SCNativeRangeChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeRangeChart.swift index d177d5b..e0b17a2 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeRangeChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeRangeChart.swift @@ -8,12 +8,14 @@ import Charts import SwiftUI +/// A chart that renders lower and upper y-bounds for each category. public struct SCNativeRangeChart: View { public let points: [SCChartRangePoint] public let seriesStyle: SCChartSeriesStyle public let axesStyle: SCChartAxesStyle public let domain: SCChartDomain? + /// Creates a range chart from prebuilt range points. public init( points: [SCChartRangePoint], seriesStyle: SCChartSeriesStyle = SCChartSeriesStyle(), @@ -26,6 +28,7 @@ public struct SCNativeRangeChart: View { self.domain = domain } + /// Creates a range chart from floating-point lower and upper arrays. public init( ranges: [(T, T)], labels: [String]? = nil, @@ -44,6 +47,7 @@ public struct SCNativeRangeChart: View { ) } + /// Creates a range chart from integer lower and upper arrays. public init( ranges: [(T, T)], labels: [String]? = nil, @@ -62,6 +66,7 @@ public struct SCNativeRangeChart: View { ) } + /// Creates a range chart from labeled floating-point lower and upper tuples. public init( labeledRanges: [(String, T, T)], seriesStyle: SCChartSeriesStyle = .rangeFill(), @@ -79,6 +84,7 @@ public struct SCNativeRangeChart: View { ) } + /// Creates a range chart from labeled integer lower and upper tuples. public init( labeledRanges: [(String, T, T)], seriesStyle: SCChartSeriesStyle = .rangeFill(), diff --git a/Sources/SimpleChart/Native/Charts/SCNativeRectangleChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeRectangleChart.swift index cdabfe0..f04cb3c 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeRectangleChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeRectangleChart.swift @@ -8,6 +8,7 @@ import Charts import SwiftUI +/// A chart that renders explicit rectangle marks across numeric x and y bounds. public struct SCNativeRectangleChart: View { public let rectangles: [SCChartRectangle] public let seriesStyle: SCChartSeriesStyle @@ -18,6 +19,7 @@ public struct SCNativeRectangleChart: View { public let plotStyle: SCChartPlotStyle public let domain: SCChartDomain? + /// Creates a rectangle chart from prebuilt rectangle models. public init( rectangles: [SCChartRectangle], seriesStyle: SCChartSeriesStyle = .bar(), @@ -44,6 +46,7 @@ public struct SCNativeRectangleChart: View { ) } + /// Creates a rectangle chart from floating-point rectangle tuples. public init( rectangles: [(T, T, U, U)], colors: [Color] = [], @@ -71,6 +74,7 @@ public struct SCNativeRectangleChart: View { ) } + /// Creates a rectangle chart from integer rectangle tuples. public init( rectangles: [(T, T, U, U)], colors: [Color] = [], diff --git a/Sources/SimpleChart/Native/Charts/SCNativeRuleChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeRuleChart.swift index a49541b..9bfc8ae 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeRuleChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeRuleChart.swift @@ -8,6 +8,7 @@ import Charts import SwiftUI +/// A chart that renders one or more reference-rule overlays without a primary series. public struct SCNativeRuleChart: View { public let referenceLines: [SCChartReferenceLine] public let axesStyle: SCChartAxesStyle @@ -17,6 +18,7 @@ public struct SCNativeRuleChart: View { public let plotStyle: SCChartPlotStyle public let domain: SCChartDomain? + /// Creates a rule chart from prebuilt reference lines. public init( referenceLines: [SCChartReferenceLine], axesStyle: SCChartAxesStyle = .minimal, @@ -37,6 +39,7 @@ public struct SCNativeRuleChart: View { self.domain = domain ?? .auto(values: referenceLines.map(\.value), baseZero: baseZero, paddingRatio: paddingRatio) } + /// Creates a rule chart from raw y-values using a shared title and color. public init( referenceLine: SCChartReferenceLine, axesStyle: SCChartAxesStyle = .minimal, diff --git a/Sources/SimpleChart/Native/Charts/SCNativeScatterChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeScatterChart.swift index 36339db..1477643 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeScatterChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeScatterChart.swift @@ -8,12 +8,14 @@ import Charts import SwiftUI +/// A ready-made scatter chart backed by explicit x/y points. public struct SCNativeScatterChart: View { public let points: [SCChartScatterPoint] public let seriesStyle: SCChartSeriesStyle public let axesStyle: SCChartAxesStyle public let domain: SCChartDomain? + /// Creates a scatter chart from prebuilt scatter points. public init( points: [SCChartScatterPoint], seriesStyle: SCChartSeriesStyle = .scatter(), @@ -26,6 +28,7 @@ public struct SCNativeScatterChart: View { self.domain = domain ?? .auto(values: points.map(\.y)) } + /// Creates a scatter chart from floating-point `(x, y)` tuples. public init( points: [(T, U)], labels: [String]? = nil, @@ -41,6 +44,7 @@ public struct SCNativeScatterChart: View { ) } + /// Creates a scatter chart from labeled floating-point scatter tuples. public init( labeledPoints: [(String, T, U)], seriesStyle: SCChartSeriesStyle = .scatter(), diff --git a/Sources/SimpleChart/Native/Charts/SCNativeSectorCharts.swift b/Sources/SimpleChart/Native/Charts/SCNativeSectorCharts.swift index 39499f6..de532df 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeSectorCharts.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeSectorCharts.swift @@ -9,10 +9,12 @@ import Charts import SwiftUI @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A sector chart wrapper for the first-party `SectorMark` surface. public struct SCNativeSectorChart: View { public let segments: [SCChartSectorSegment] public let palette: [Color] + /// Creates a sector chart from prebuilt segment models. public init( segments: [SCChartSectorSegment], palette: [Color] = [.accentColor, .blue, .orange, .green, .pink, .purple] @@ -21,6 +23,7 @@ public struct SCNativeSectorChart: View { self.palette = palette } + /// Creates a sector chart from floating-point `(title, value)` tuples. public init( segments: [(String, T)], colors: [Color] = [], @@ -32,6 +35,7 @@ public struct SCNativeSectorChart: View { ) } + /// Creates a sector chart from integer `(title, value)` tuples. public init( segments: [(String, T)], colors: [Color] = [], @@ -55,11 +59,13 @@ public struct SCNativeSectorChart: View { } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A donut chart wrapper for the first-party `SectorMark` surface. public struct SCNativeDonutChart: View { public let segments: [SCChartSectorSegment] public let innerRadiusRatio: Double public let palette: [Color] + /// Creates a donut chart from prebuilt segment models. public init( segments: [SCChartSectorSegment], innerRadiusRatio: Double = 0.6, @@ -70,6 +76,7 @@ public struct SCNativeDonutChart: View { self.palette = palette } + /// Creates a donut chart from floating-point `(title, value)` tuples. public init( segments: [(String, T)], innerRadiusRatio: Double = 0.6, @@ -83,6 +90,7 @@ public struct SCNativeDonutChart: View { ) } + /// Creates a donut chart from integer `(title, value)` tuples. public init( segments: [(String, T)], innerRadiusRatio: Double = 0.6, diff --git a/Sources/SimpleChart/Native/Charts/SCNativeStackedAreaChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeStackedAreaChart.swift index 6005f85..690be4f 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeStackedAreaChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeStackedAreaChart.swift @@ -9,6 +9,7 @@ import Charts import Foundation import SwiftUI +/// A stacked area chart that accumulates multiple named series over a shared category axis. public struct SCNativeStackedAreaChart: View { public let series: [SCChartLineSeries] public let axesStyle: SCChartAxesStyle @@ -17,6 +18,7 @@ public struct SCNativeStackedAreaChart: View { public let foregroundStyleScale: SCChartForegroundStyleScale public let referenceLines: [SCChartReferenceLine] + /// Creates a stacked area chart from prebuilt line-series values. public init( series: [SCChartLineSeries], axesStyle: SCChartAxesStyle = .standard(), diff --git a/Sources/SimpleChart/Native/Charts/SCNativeStackedBarChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeStackedBarChart.swift index c7108d6..d4b92a7 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeStackedBarChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeStackedBarChart.swift @@ -8,6 +8,7 @@ import Charts import SwiftUI +/// A stacked bar chart that accumulates segment values within each category. public struct SCNativeStackedBarChart: View { public let segments: [SCChartStackSegment] public let axesStyle: SCChartAxesStyle @@ -15,6 +16,7 @@ public struct SCNativeStackedBarChart: View { public let legend: SCChartLegend public let foregroundStyleScale: SCChartForegroundStyleScale + /// Creates a stacked bar chart from prebuilt stack-segment models. public init( segments: [SCChartStackSegment], axesStyle: SCChartAxesStyle = .standard(), @@ -31,6 +33,7 @@ public struct SCNativeStackedBarChart: View { self.foregroundStyleScale = foregroundStyleScale ?? .categorical(segmentNames, palette: palette) } + /// Creates a stacked bar chart from explicit segment values and category order. public init( groups: [SCChartBarGroup], axesStyle: SCChartAxesStyle = .standard(), @@ -49,6 +52,7 @@ public struct SCNativeStackedBarChart: View { ) } + /// Creates a stacked bar chart from floating-point tuples of category, segment, and value. public init( groups: [(String, [(String, T)])], axesStyle: SCChartAxesStyle = .standard(), @@ -67,6 +71,7 @@ public struct SCNativeStackedBarChart: View { ) } + /// Creates a stacked bar chart from integer tuples of category, segment, and value. public init( groups: [(String, [(String, T)])], axesStyle: SCChartAxesStyle = .standard(), diff --git a/Sources/SimpleChart/Native/Charts/SCNativeThresholdChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeThresholdChart.swift index 31c5781..d36437c 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeThresholdChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeThresholdChart.swift @@ -7,6 +7,7 @@ import SwiftUI +/// A composed chart that overlays threshold reference lines on top of line values. public struct SCNativeThresholdChart: View { public let points: [SCChartPoint] public let threshold: SCChartReferenceLine @@ -14,6 +15,7 @@ public struct SCNativeThresholdChart: View { public let axesStyle: SCChartAxesStyle public let domain: SCChartDomain? + /// Creates a threshold chart from prebuilt categorical points and threshold lines. public init( points: [SCChartPoint], threshold: SCChartReferenceLine, @@ -28,6 +30,7 @@ public struct SCNativeThresholdChart: View { self.domain = domain ?? .auto(values: points.map(\.value) + [threshold.value], baseZero: true) } + /// Creates a threshold chart from floating-point values, optional labels, and threshold lines. public init( values: [T], labels: [String]? = nil, diff --git a/Sources/SimpleChart/Native/Charts/SCNativeTimeSeriesChart.swift b/Sources/SimpleChart/Native/Charts/SCNativeTimeSeriesChart.swift index 2d9e91a..6f2f6a6 100644 --- a/Sources/SimpleChart/Native/Charts/SCNativeTimeSeriesChart.swift +++ b/Sources/SimpleChart/Native/Charts/SCNativeTimeSeriesChart.swift @@ -8,6 +8,7 @@ import Charts import SwiftUI +/// A ready-made time-series line chart with date-aware axis formatting. public struct SCNativeTimeSeriesChart: View { public let points: [SCChartTimePoint] public let seriesStyle: SCChartSeriesStyle @@ -17,6 +18,7 @@ public struct SCNativeTimeSeriesChart: View { public let xAxisFormat: SCChartDateValueFormat public let yAxisFormat: SCChartNumericValueFormat + /// Creates a time-series chart from prebuilt time points. public init( points: [SCChartTimePoint], seriesStyle: SCChartSeriesStyle = .line(), @@ -35,6 +37,7 @@ public struct SCNativeTimeSeriesChart: View { self.yAxisFormat = yAxisFormat } + /// Creates a time-series chart from floating-point `(date, value)` tuples. public init( values: [(Date, T)], seriesStyle: SCChartSeriesStyle = .line(), @@ -55,6 +58,7 @@ public struct SCNativeTimeSeriesChart: View { ) } + /// Creates a time-series chart from integer `(date, value)` tuples. public init( values: [(Date, T)], seriesStyle: SCChartSeriesStyle = .line(), diff --git a/Sources/SimpleChart/Native/Charts/SCScrollableCharts.swift b/Sources/SimpleChart/Native/Charts/SCScrollableCharts.swift index 6c4e743..7ef5f0d 100644 --- a/Sources/SimpleChart/Native/Charts/SCScrollableCharts.swift +++ b/Sources/SimpleChart/Native/Charts/SCScrollableCharts.swift @@ -9,6 +9,7 @@ import Charts import SwiftUI @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A categorical line chart wrapper with scrollable x-domain support. public struct SCScrollableLineChart: View { public let points: [SCChartPoint] public let seriesStyle: SCChartSeriesStyle @@ -16,15 +17,23 @@ public struct SCScrollableLineChart: View { public let domain: SCChartDomain? public let referenceLines: [SCChartReferenceLine] public let scrollBehavior: SCChartScrollBehavior + /// The zoom policy applied to the visible x-domain window. + public let zoomBehavior: SCChartZoomBehavior public let gestureConfiguration: SCChartGestureConfiguration public let yAxisFormat: SCChartNumericValueFormat @Binding private var viewport: SCChartViewport + @State private var lastMagnification: CGFloat = 1 + @State private var prefersViewportVisibleDomain = false + /// Creates a scrollable line chart bound to an explicit viewport value. + /// + /// Bind a full ``SCChartViewport`` when the surrounding view needs to read or update the current indexed window programmatically. public init( points: [SCChartPoint], viewport: Binding, scrollBehavior: SCChartScrollBehavior = .continuous(.points(6)), + zoomBehavior: SCChartZoomBehavior = .standard, seriesStyle: SCChartSeriesStyle = .line(), axesStyle: SCChartAxesStyle = .standard(), domain: SCChartDomain? = nil, @@ -35,6 +44,7 @@ public struct SCScrollableLineChart: View { self.points = points self._viewport = viewport self.scrollBehavior = scrollBehavior + self.zoomBehavior = zoomBehavior self.seriesStyle = seriesStyle self.axesStyle = axesStyle self.domain = domain ?? .auto(points: points, baseZero: false) @@ -43,10 +53,14 @@ public struct SCScrollableLineChart: View { self.yAxisFormat = yAxisFormat } + /// Creates a scrollable line chart from a visible-domain helper instead of a full scroll behavior. + /// + /// Use this initializer when the chart should still expose viewport state externally but the initial window is easier to describe as a visible-domain helper. public init( points: [SCChartPoint], viewport: Binding, visibleDomain: SCChartVisibleDomain = .points(6), + zoomBehavior: SCChartZoomBehavior = .standard, seriesStyle: SCChartSeriesStyle = .line(), axesStyle: SCChartAxesStyle = .standard(), domain: SCChartDomain? = nil, @@ -58,6 +72,7 @@ public struct SCScrollableLineChart: View { points: points, viewport: viewport, scrollBehavior: .continuous(visibleDomain), + zoomBehavior: zoomBehavior, seriesStyle: seriesStyle, axesStyle: axesStyle, domain: domain, @@ -95,42 +110,98 @@ public struct SCScrollableLineChart: View { } .scChartScrollableX( enabled: gestureConfiguration.allowsScrolling, - visibleDomain: scrollBehavior.visibleDomain, + visibleDomain: renderedVisibleDomain, position: scrollPositionBinding ) .scChartDomain(domain) .scChartIndexedXAxis(labels: points.map(\.plottedXValue), axesStyle: axesStyle) .scChartNumericYAxis(axesStyle, format: yAxisFormat) } + .simultaneousGesture(zoomGesture) + .onChange(of: viewport) { _, newViewport in + guard !prefersViewportVisibleDomain else { return } + if abs(newViewport.length - scrollBehavior.visibleDomain.length) > 0.0001 { + prefersViewportVisibleDomain = true + } + } } private var indexedPoints: [SCIndexedScrollablePoint] { points.enumerated().map { SCIndexedScrollablePoint(index: $0.offset, point: $0.element) } } - private var effectiveVisibleLength: Double { - max(viewport.length, scrollBehavior.visibleDomain.length) + private var bounds: ClosedRange { + 0...max(Double(max(points.count, 1)), 0.0001) + } + + private var effectiveViewport: SCChartViewport { + SCChartNavigationCoordinator.clampedViewport( + viewport, + zoomBehavior: zoomBehavior, + bounds: bounds + ) } private var scrollPositionBinding: Binding { Binding( - get: { viewport.lowerBound }, + get: { effectiveViewport.lowerBound }, set: { newLowerBound in guard gestureConfiguration.allowsScrolling else { return } - viewport = SCChartViewport.starting(at: newLowerBound, length: effectiveVisibleLength) + prefersViewportVisibleDomain = true + viewport = SCChartNavigationCoordinator.scrollViewport( + effectiveViewport, + to: newLowerBound, + zoomBehavior: zoomBehavior, + bounds: bounds + ) } ) } + /// The visible-domain window currently configured on the wrapper. + /// + /// This reflects the wrapper's public state rather than the internally clamped render window used while applying gesture updates. public var visibleDomain: SCChartVisibleDomain { - scrollBehavior.visibleDomain + if prefersViewportVisibleDomain { + return SCChartVisibleDomain(length: viewport.length) + } + return scrollBehavior.visibleDomain + } + + var renderedVisibleDomain: SCChartVisibleDomain { + if prefersViewportVisibleDomain { + return SCChartVisibleDomain(length: effectiveViewport.length) + } + return scrollBehavior.visibleDomain + } + + private var zoomGesture: some Gesture { + MagnificationGesture() + .onChanged { value in + guard gestureConfiguration.allowsZooming, zoomBehavior.isEnabled else { return } + let delta = Double(value / lastMagnification) + lastMagnification = value + prefersViewportVisibleDomain = true + viewport = SCChartNavigationCoordinator.zoomViewport( + effectiveViewport, + magnification: delta, + zoomBehavior: zoomBehavior, + bounds: bounds + ) + } + .onEnded { _ in + lastMagnification = 1 + } } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A time-series line chart wrapper with scrollable date-domain support. public struct SCScrollableTimeSeriesChart: View { public let points: [SCChartTimePoint] public let scrollBehavior: SCChartScrollBehavior + /// The zoom policy applied to the visible date-domain window. + public let zoomBehavior: SCChartZoomBehavior public let seriesStyle: SCChartSeriesStyle public let axesStyle: SCChartAxesStyle public let domain: SCChartDomain? @@ -139,8 +210,12 @@ public struct SCScrollableTimeSeriesChart: View { public let gestureConfiguration: SCChartGestureConfiguration public let yAxisFormat: SCChartNumericValueFormat - @Binding private var scrollPosition: Date + private let navigationSource: SCTimeNavigationSource + @State private var lastMagnification: CGFloat = 1 + /// Creates a scrollable time-series chart bound to an explicit date scroll position. + /// + /// This compatibility initializer keeps the original scroll-position model and disables zoom-specific state so existing call sites keep their behavior. public init( points: [SCChartTimePoint], scrollPosition: Binding, @@ -154,8 +229,9 @@ public struct SCScrollableTimeSeriesChart: View { yAxisFormat: SCChartNumericValueFormat = .automatic ) { self.points = points.sorted { $0.date < $1.date } - self._scrollPosition = scrollPosition + self.navigationSource = .scrollPosition(scrollPosition) self.scrollBehavior = scrollBehavior + self.zoomBehavior = .disabled self.seriesStyle = seriesStyle self.axesStyle = axesStyle self.domain = domain ?? .auto(values: points.map(\.value), baseZero: false) @@ -165,6 +241,9 @@ public struct SCScrollableTimeSeriesChart: View { self.yAxisFormat = yAxisFormat } + /// Creates a scrollable time-series chart from a visible-domain helper instead of a full scroll behavior. + /// + /// Use this when a date-based chart still uses the legacy scroll-position binding but the initial window is easier to describe through ``SCChartVisibleDomain``. public init( points: [SCChartTimePoint], scrollPosition: Binding, @@ -191,6 +270,35 @@ public struct SCScrollableTimeSeriesChart: View { ) } + /// Creates a scrollable and zoomable time-series chart bound to an explicit viewport value. + /// + /// Bind a full ``SCChartTimeViewport`` when the chart needs programmatic zoom or when multiple controls should coordinate the same visible date range. + public init( + points: [SCChartTimePoint], + viewport: Binding, + scrollBehavior: SCChartScrollBehavior = .timeWindow(seconds: 60 * 60 * 24 * 7), + zoomBehavior: SCChartZoomBehavior = .standard, + seriesStyle: SCChartSeriesStyle = .line(), + axesStyle: SCChartAxesStyle = .standard(), + domain: SCChartDomain? = nil, + referenceLines: [SCChartReferenceLine] = [], + xAxisFormat: SCChartDateValueFormat = .monthDay, + gestureConfiguration: SCChartGestureConfiguration = .scrollOnly, + yAxisFormat: SCChartNumericValueFormat = .automatic + ) { + self.points = points.sorted { $0.date < $1.date } + self.navigationSource = .viewport(viewport) + self.scrollBehavior = scrollBehavior + self.zoomBehavior = zoomBehavior + self.seriesStyle = seriesStyle + self.axesStyle = axesStyle + self.domain = domain ?? .auto(values: points.map(\.value), baseZero: false) + self.referenceLines = referenceLines + self.xAxisFormat = xAxisFormat + self.gestureConfiguration = gestureConfiguration + self.yAxisFormat = yAxisFormat + } + public var body: some View { SCNativeChartContainer(axesStyle: axesStyle) { Chart(points) { point in @@ -219,17 +327,127 @@ public struct SCScrollableTimeSeriesChart: View { } .scChartScrollableX( enabled: gestureConfiguration.allowsScrolling, - visibleDomain: scrollBehavior.visibleDomain, - position: $scrollPosition + visibleDomain: renderedVisibleDomain, + position: scrollPositionBinding ) .scChartDomain(domain) .scChartDateXAxis(axesStyle, format: xAxisFormat) .scChartNumericYAxis(axesStyle, format: yAxisFormat) } + .simultaneousGesture(zoomGesture) } + /// The visible-domain window currently configured on the wrapper. + /// + /// This reflects the wrapper's public viewport state rather than the internally clamped render window used while processing scroll and zoom updates. public var visibleDomain: SCChartVisibleDomain { - scrollBehavior.visibleDomain + SCChartVisibleDomain(length: currentViewport.length) + } + + var renderedVisibleDomain: SCChartVisibleDomain { + switch navigationSource { + case .scrollPosition: + return scrollBehavior.visibleDomain + case .viewport: + return SCChartVisibleDomain(length: effectiveViewport.length) + } + } + + private var bounds: ClosedRange { + let fallback = currentViewport.startDate + let lowerBound = points.first?.date ?? fallback + let upperBound = points.last?.date ?? fallback.addingTimeInterval(max(scrollBehavior.visibleDomain.length, 0.0001)) + return lowerBound...max(lowerBound, upperBound) + } + + private var currentViewport: SCChartTimeViewport { + switch navigationSource { + case let .scrollPosition(scrollPosition): + return SCChartTimeViewport.starting( + at: scrollPosition.wrappedValue, + duration: scrollBehavior.visibleDomain.length + ) + case let .viewport(viewport): + return viewport.wrappedValue + } + } + + private var effectiveZoomBehavior: SCChartZoomBehavior { + switch navigationSource { + case .scrollPosition: + return .disabled + case .viewport: + return zoomBehavior + } + } + + private var effectiveViewport: SCChartTimeViewport { + switch navigationSource { + case .scrollPosition: + return currentViewport + case .viewport: + return SCChartNavigationCoordinator.clampedViewport( + currentViewport, + zoomBehavior: effectiveZoomBehavior, + bounds: bounds + ) + } + } + + private var scrollPositionBinding: Binding { + Binding( + get: { effectiveViewport.startDate }, + set: { newStartDate in + guard gestureConfiguration.allowsScrolling else { return } + switch navigationSource { + case .scrollPosition: + assign( + SCChartTimeViewport.starting( + at: newStartDate, + duration: currentViewport.length + ) + ) + case .viewport: + assign( + SCChartNavigationCoordinator.scrollViewport( + effectiveViewport, + to: newStartDate, + zoomBehavior: effectiveZoomBehavior, + bounds: bounds + ) + ) + } + } + ) + } + + private var zoomGesture: some Gesture { + MagnificationGesture() + .onChanged { value in + guard gestureConfiguration.allowsZooming, effectiveZoomBehavior.isEnabled else { return } + let delta = Double(value / lastMagnification) + lastMagnification = value + assign( + SCChartNavigationCoordinator.zoomViewport( + effectiveViewport, + magnification: delta, + zoomBehavior: effectiveZoomBehavior, + bounds: bounds + ) + ) + } + .onEnded { _ in + lastMagnification = 1 + } + } + + private func assign(_ viewport: SCChartTimeViewport) { + switch navigationSource { + case let .scrollPosition(scrollPosition): + scrollPosition.wrappedValue = viewport.startDate + case let .viewport(binding): + binding.wrappedValue = viewport + } } } @@ -239,3 +457,8 @@ private struct SCIndexedScrollablePoint: Identifiable { var id: String { point.id } } + +private enum SCTimeNavigationSource { + case scrollPosition(Binding) + case viewport(Binding) +} diff --git a/Sources/SimpleChart/Native/Charts/SCSelectableCharts.swift b/Sources/SimpleChart/Native/Charts/SCSelectableCharts.swift index 74f9904..4bf1df3 100644 --- a/Sources/SimpleChart/Native/Charts/SCSelectableCharts.swift +++ b/Sources/SimpleChart/Native/Charts/SCSelectableCharts.swift @@ -9,6 +9,7 @@ import Charts import SwiftUI @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A line chart wrapper with native point selection and inspection support. public struct SCSelectableLineChart: View { public let points: [SCChartPoint] public let seriesStyle: SCChartSeriesStyle @@ -21,6 +22,7 @@ public struct SCSelectableLineChart: View { @Binding private var selection: SCChartSelection? + /// Creates a selectable line chart bound directly to an optional selection. public init( points: [SCChartPoint], selection: Binding, @@ -43,6 +45,7 @@ public struct SCSelectableLineChart: View { self.yAxisFormat = yAxisFormat } + /// Creates a selectable line chart bound to the shared selection-state helper. public init( points: [SCChartPoint], selectionState: Binding, @@ -173,6 +176,7 @@ public struct SCSelectableLineChart: View { } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A bar chart wrapper with native bar selection and inspection support. public struct SCSelectableBarChart: View { public let points: [SCChartPoint] public let seriesStyle: SCChartSeriesStyle @@ -184,6 +188,7 @@ public struct SCSelectableBarChart: View { @Binding private var selection: SCChartSelection? + /// Creates a selectable bar chart bound directly to an optional selection. public init( points: [SCChartPoint], selection: Binding, @@ -204,6 +209,7 @@ public struct SCSelectableBarChart: View { self.yAxisFormat = yAxisFormat } + /// Creates a selectable bar chart bound to the shared selection-state helper. public init( points: [SCChartPoint], selectionState: Binding, @@ -308,6 +314,7 @@ public struct SCSelectableBarChart: View { } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A scatter chart wrapper with native point selection and inspection support. public struct SCSelectableScatterChart: View { public let points: [SCChartScatterPoint] public let seriesStyle: SCChartSeriesStyle @@ -319,6 +326,7 @@ public struct SCSelectableScatterChart: View { @Binding private var selection: SCChartSelection? + /// Creates a selectable scatter chart bound directly to an optional selection. public init( points: [SCChartScatterPoint], selection: Binding, @@ -339,6 +347,7 @@ public struct SCSelectableScatterChart: View { self.yAxisFormat = yAxisFormat } + /// Creates a selectable scatter chart bound to the shared selection-state helper. public init( points: [SCChartScatterPoint], selectionState: Binding, diff --git a/Sources/SimpleChart/Native/Charts/SCSelectableSectorCharts.swift b/Sources/SimpleChart/Native/Charts/SCSelectableSectorCharts.swift index ee6953b..595ab70 100644 --- a/Sources/SimpleChart/Native/Charts/SCSelectableSectorCharts.swift +++ b/Sources/SimpleChart/Native/Charts/SCSelectableSectorCharts.swift @@ -9,6 +9,7 @@ import Charts import SwiftUI @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A sector chart wrapper with angle selection and inspection support. public struct SCSelectableSectorChart: View { public let segments: [SCChartSectorSegment] public let palette: [Color] @@ -17,6 +18,7 @@ public struct SCSelectableSectorChart: View { @Binding private var selection: SCChartSelection? + /// Creates a selectable sector chart bound directly to an optional selection. public init( segments: [SCChartSectorSegment], selection: Binding, @@ -31,6 +33,7 @@ public struct SCSelectableSectorChart: View { self.gestureConfiguration = gestureConfiguration } + /// Creates a selectable sector chart bound to the shared selection-state helper. public init( segments: [SCChartSectorSegment], selectionState: Binding, @@ -50,6 +53,7 @@ public struct SCSelectableSectorChart: View { ) } + /// Creates a selectable sector chart from floating-point `(title, value)` tuples. public init( segments: [(String, T)], selection: Binding, @@ -67,6 +71,7 @@ public struct SCSelectableSectorChart: View { ) } + /// Creates a selectable sector chart from integer `(title, value)` tuples. public init( segments: [(String, T)], selection: Binding, @@ -163,6 +168,7 @@ public struct SCSelectableSectorChart: View { } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) +/// A donut chart wrapper with angle selection and inspection support. public struct SCSelectableDonutChart: View { public let segments: [SCChartSectorSegment] public let innerRadiusRatio: Double @@ -172,6 +178,7 @@ public struct SCSelectableDonutChart: View { @Binding private var selection: SCChartSelection? + /// Creates a selectable donut chart bound directly to an optional selection. public init( segments: [SCChartSectorSegment], selection: Binding, @@ -188,6 +195,7 @@ public struct SCSelectableDonutChart: View { self.gestureConfiguration = gestureConfiguration } + /// Creates a selectable donut chart bound to the shared selection-state helper. public init( segments: [SCChartSectorSegment], selectionState: Binding, @@ -209,6 +217,7 @@ public struct SCSelectableDonutChart: View { ) } + /// Creates a selectable donut chart from floating-point `(title, value)` tuples. public init( segments: [(String, T)], selection: Binding, @@ -228,6 +237,7 @@ public struct SCSelectableDonutChart: View { ) } + /// Creates a selectable donut chart from integer `(title, value)` tuples. public init( segments: [(String, T)], selection: Binding, diff --git a/Sources/SimpleChart/Native/Composition/SCChart3DComposition.swift b/Sources/SimpleChart/Native/Composition/SCChart3DComposition.swift index 33bc413..747af71 100644 --- a/Sources/SimpleChart/Native/Composition/SCChart3DComposition.swift +++ b/Sources/SimpleChart/Native/Composition/SCChart3DComposition.swift @@ -10,6 +10,7 @@ import SwiftUI @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) +/// Describes the 3D marks that can be combined inside `SCComposedChart3D`. public enum SCChart3DMark { case point([SCChart3DPoint], style: SCChart3DSeriesStyle = .init()) case rectangle([SCChart3DPoint], style: SCChart3DSeriesStyle = .init()) @@ -26,10 +27,12 @@ public enum SCChart3DMark { @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) +/// Bundles 3D marks and camera pose into a reusable composition value. public struct SCChart3DComposition { public let marks: [SCChart3DMark] public let pose: SCChart3DPoseStyle + /// Creates a 3D chart composition from marks and an optional viewing pose. public init( marks: [SCChart3DMark], pose: SCChart3DPoseStyle = .default diff --git a/Sources/SimpleChart/Native/Composition/SCComposedChart3D.swift b/Sources/SimpleChart/Native/Composition/SCComposedChart3D.swift index bbbfe04..0838181 100644 --- a/Sources/SimpleChart/Native/Composition/SCComposedChart3D.swift +++ b/Sources/SimpleChart/Native/Composition/SCComposedChart3D.swift @@ -8,16 +8,20 @@ import Charts import SwiftUI +#if compiler(>=6.3) @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) +/// Renders a reusable 3D composition built from `SCChart3DMark` values. public struct SCComposedChart3D: View { public let composition: SCChart3DComposition + /// Creates a 3D composed chart from a prepared composition value. public init(composition: SCChart3DComposition) { self.composition = composition } + /// Creates a 3D composed chart directly from marks and an optional pose. public init( marks: [SCChart3DMark], pose: SCChart3DPoseStyle = .default @@ -71,3 +75,4 @@ public struct SCComposedChart3D: View { } } } +#endif diff --git a/Sources/SimpleChart/Native/Core/SCChart3DStyle.swift b/Sources/SimpleChart/Native/Core/SCChart3DStyle.swift index 3b80650..18183f0 100644 --- a/Sources/SimpleChart/Native/Core/SCChart3DStyle.swift +++ b/Sources/SimpleChart/Native/Core/SCChart3DStyle.swift @@ -11,6 +11,7 @@ import SwiftUI @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) +/// Presets the viewing angle used when rendering 3D chart compositions. public enum SCChart3DPoseStyle: Equatable { case `default` case front @@ -25,10 +26,12 @@ public enum SCChart3DPoseStyle: Equatable { @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) +/// Styles point, rectangle, rule, and surface marks in 3D chart wrappers. public struct SCChart3DSeriesStyle: Equatable { public let color: Color public let symbolSize: CGFloat + /// Creates a 3D series style from a color and default symbol size. public init( color: Color = .accentColor, symbolSize: CGFloat = 40 diff --git a/Sources/SimpleChart/Native/Core/SCChartAnnotation.swift b/Sources/SimpleChart/Native/Core/SCChartAnnotation.swift index 03df0ad..3b4ad4c 100644 --- a/Sources/SimpleChart/Native/Core/SCChartAnnotation.swift +++ b/Sources/SimpleChart/Native/Core/SCChartAnnotation.swift @@ -7,6 +7,7 @@ import SwiftUI +/// Positions annotation content relative to the mark or overlay it decorates. public enum SCChartAnnotationAnchor: String, Codable, Equatable { case top case topLeading @@ -17,12 +18,14 @@ public enum SCChartAnnotationAnchor: String, Codable, Equatable { case overlay } +/// Controls the visual treatment used when rendering an annotation label. public enum SCChartAnnotationStyle: String, Codable, Equatable { case plain case caption case badge } +/// Describes reusable annotation content for composed charts, overlays, and inspection UI. public struct SCChartAnnotation: Equatable { public let text: String public let color: Color @@ -32,6 +35,7 @@ public struct SCChartAnnotation: Equatable { public let alignment: Alignment public let style: SCChartAnnotationStyle + /// Creates a reusable annotation description from explicit text, colors, and placement. public init( text: String, color: Color = .secondary, @@ -52,6 +56,7 @@ public struct SCChartAnnotation: Equatable { } public extension SCChartAnnotation { + /// Creates a caption-style annotation tuned for reference lines. static func lineLabel( _ text: String, color: Color = .secondary, @@ -67,6 +72,7 @@ public extension SCChartAnnotation { ) } + /// Creates a badge-style callout suitable for hover and selection summaries. static func callout( _ text: String, color: Color = .secondary, @@ -86,6 +92,7 @@ public extension SCChartAnnotation { ) } + /// Creates a pill-shaped badge annotation. static func badge( _ text: String, color: Color = .primary, @@ -105,6 +112,7 @@ public extension SCChartAnnotation { ) } + /// Creates a lightweight caption annotation. static func caption( _ text: String, color: Color = .secondary, @@ -120,6 +128,7 @@ public extension SCChartAnnotation { ) } + /// Formats a numeric value and returns it as a badge-style annotation. static func valueLabel( _ value: Double, format: SCChartNumericValueFormat = .automatic, diff --git a/Sources/SimpleChart/Native/Core/SCChartAxis.swift b/Sources/SimpleChart/Native/Core/SCChartAxis.swift index 9f969e2..461a7a9 100644 --- a/Sources/SimpleChart/Native/Core/SCChartAxis.swift +++ b/Sources/SimpleChart/Native/Core/SCChartAxis.swift @@ -7,6 +7,7 @@ import SwiftUI +/// Places an explicit axis title and marks on a specific edge of the chart. public enum SCChartAxisPosition: String, Codable, Equatable { case automatic case leading @@ -15,6 +16,7 @@ public enum SCChartAxisPosition: String, Codable, Equatable { case bottom } +/// Describes where axis-mark values should come from when building Swift Charts axes. public enum SCChartAxisValueSource: Equatable { case automatic(desiredCount: Int? = nil) case integers([Int]) @@ -23,6 +25,7 @@ public enum SCChartAxisValueSource: Equatable { case dates([Date]) } +/// Configures the marks rendered for a single chart axis. public struct SCChartAxisMarks: Equatable { public let desiredCount: Int public let valueSource: SCChartAxisValueSource @@ -33,6 +36,7 @@ public struct SCChartAxisMarks: Equatable { public let lineColor: Color public let labelColor: Color + /// Creates an axis-mark configuration with optional fixed values and styling. public init( desiredCount: Int = 3, valueSource: SCChartAxisValueSource = .automatic(), @@ -54,6 +58,7 @@ public struct SCChartAxisMarks: Equatable { } } +/// Describes a fully configured x- or y-axis, including title, placement, and mark styling. public struct SCChartAxis: Equatable { public let isVisible: Bool public let title: String @@ -62,6 +67,7 @@ public struct SCChartAxis: Equatable { public let position: SCChartAxisPosition public let marks: SCChartAxisMarks + /// Creates a chart axis from explicit visibility, title, and mark configuration. public init( isVisible: Bool = false, title: String = "", @@ -82,6 +88,7 @@ public struct SCChartAxis: Equatable { public extension SCChartAxis { static let hidden = SCChartAxis() + /// Creates a visible bottom x-axis with the supplied title and mark styling. static func x( title: String = "", titleColor: Color = .primary, @@ -114,6 +121,7 @@ public extension SCChartAxis { ) } + /// Creates a visible y-axis with the supplied title, placement, and mark styling. static func y( title: String = "", titleColor: Color = .primary, diff --git a/Sources/SimpleChart/Native/Core/SCChartDomain.swift b/Sources/SimpleChart/Native/Core/SCChartDomain.swift index 1f74f25..1a5b3ad 100644 --- a/Sources/SimpleChart/Native/Core/SCChartDomain.swift +++ b/Sources/SimpleChart/Native/Core/SCChartDomain.swift @@ -103,6 +103,7 @@ public struct SCChartDomain: Equatable, Codable { ) } + /// Derives a domain from matching floating-point lower and upper bound collections. public static func make( lowerValues: [T], upperValues: [U], @@ -121,6 +122,7 @@ public struct SCChartDomain: Equatable, Codable { ) } + /// Derives a domain from matching integer lower and upper bound collections. public static func make( lowerValues: [T], upperValues: [U], @@ -156,6 +158,7 @@ public struct SCChartDomain: Equatable, Codable { ) } + /// Convenience alias for `make(values:)` with floating-point input. public static func auto( values: [T], baseZero: Bool = false, @@ -172,6 +175,7 @@ public struct SCChartDomain: Equatable, Codable { ) } + /// Convenience alias for `make(values:)` with integer input. public static func auto( values: [T], baseZero: Bool = false, @@ -205,6 +209,7 @@ public struct SCChartDomain: Equatable, Codable { ) } + /// Convenience alias for auto-deriving a domain from explicit lower and upper floating-point bounds. public static func auto( lowerValues: [Double], upperValues: [Double], @@ -223,6 +228,7 @@ public struct SCChartDomain: Equatable, Codable { ) } + /// Convenience alias for auto-deriving a domain from matching floating-point lower and upper bound collections. public static func auto( lowerValues: [T], upperValues: [U], @@ -241,6 +247,7 @@ public struct SCChartDomain: Equatable, Codable { ) } + /// Convenience alias for auto-deriving a domain from matching integer lower and upper bound collections. public static func auto( lowerValues: [T], upperValues: [U], @@ -259,6 +266,7 @@ public struct SCChartDomain: Equatable, Codable { ) } + /// Convenience alias for auto-deriving a domain from existing range points. public static func auto( points: [SCChartRangePoint], baseZero: Bool = false, @@ -276,6 +284,7 @@ public struct SCChartDomain: Equatable, Codable { ) } + /// Creates a fixed domain without applying automatic padding. public static func fixed(_ range: ClosedRange) -> SCChartDomain { SCChartDomain( lowerBound: range.lowerBound, diff --git a/Sources/SimpleChart/Native/Core/SCChartForegroundStyleScale.swift b/Sources/SimpleChart/Native/Core/SCChartForegroundStyleScale.swift index 3c9a49a..de950ee 100644 --- a/Sources/SimpleChart/Native/Core/SCChartForegroundStyleScale.swift +++ b/Sources/SimpleChart/Native/Core/SCChartForegroundStyleScale.swift @@ -7,10 +7,12 @@ import SwiftUI +/// Maps category names to colors for grouped, stacked, and composed chart legends. public struct SCChartForegroundStyleScale: Equatable { public let domain: [String] public let range: [Color] + /// Creates an explicit foreground-style scale from a domain/range mapping. public init(domain: [String], range: [Color]) { self.domain = domain self.range = range @@ -18,6 +20,7 @@ public struct SCChartForegroundStyleScale: Equatable { } public extension SCChartForegroundStyleScale { + /// Builds a categorical foreground-style scale from an ordered domain and palette. static func categorical( _ domain: [String], palette: [Color] diff --git a/Sources/SimpleChart/Native/Core/SCChartInteraction.swift b/Sources/SimpleChart/Native/Core/SCChartInteraction.swift index 0dfb13b..0dd6aa4 100644 --- a/Sources/SimpleChart/Native/Core/SCChartInteraction.swift +++ b/Sources/SimpleChart/Native/Core/SCChartInteraction.swift @@ -9,6 +9,7 @@ import Foundation /// Mutable selection state that can be stored outside a chart wrapper and rebound later. public struct SCChartSelectionState: Equatable { + /// The currently selected chart element, if any. public var selection: SCChartSelection? /// Creates selection state with an optional active selection. @@ -76,6 +77,7 @@ public enum SCChartInspectionOverlay: Equatable, Codable { /// Scroll-window behavior shared by scrollable wrappers. public struct SCChartScrollBehavior: Equatable, Codable { + /// The default visible x-domain window used when the chart first appears. public let visibleDomain: SCChartVisibleDomain /// Creates a scroll behavior from a visible-domain window description. @@ -119,24 +121,119 @@ public struct SCChartScrollBehavior: Equatable, Codable { } } +/// Configures whether zoom is enabled and how far a chart window may zoom in or out. +public struct SCChartZoomBehavior: Equatable, Codable { + /// Whether pinch-style zoom interactions should change the visible window. + public let isEnabled: Bool + /// The smallest allowed visible-domain length after zooming in. + public let minimumVisibleLength: Double? + /// The largest allowed visible-domain length after zooming out. + public let maximumVisibleLength: Double? + /// A multiplier applied to gesture magnification deltas before they update the visible window. + public let sensitivity: Double + + /// Creates a zoom policy with optional minimum/maximum visible lengths. + /// + /// - Parameters: + /// - isEnabled: Whether zoom gestures should be honored. + /// - minimumVisibleLength: The minimum visible-domain length allowed by the policy. + /// - maximumVisibleLength: The maximum visible-domain length allowed by the policy. + /// - sensitivity: A multiplier that makes the zoom feel slower or faster than the raw gesture delta. + public init( + isEnabled: Bool = true, + minimumVisibleLength: Double? = nil, + maximumVisibleLength: Double? = nil, + sensitivity: Double = 1 + ) { + self.isEnabled = isEnabled + self.minimumVisibleLength = minimumVisibleLength.map { max($0, 0.0001) } + self.maximumVisibleLength = maximumVisibleLength.map { max($0, 0.0001) } + self.sensitivity = max(sensitivity, 0.0001) + } + + /// A permissive default zoom policy suitable for most scrollable wrappers. + public static let standard = SCChartZoomBehavior() + + /// Disables zoom-specific behavior while keeping the API surface stable. + public static let disabled = SCChartZoomBehavior( + isEnabled: false, + minimumVisibleLength: nil, + maximumVisibleLength: nil, + sensitivity: 1 + ) + + func clamped(length: Double, within boundsLength: Double) -> Double { + let lowerBound = minimumVisibleLength ?? 0.0001 + let upperBound = min(maximumVisibleLength ?? boundsLength, max(boundsLength, 0.0001)) + return min(max(length, lowerBound), max(upperBound, lowerBound)) + } + + func adjustedMagnification(from magnification: Double) -> Double { + guard magnification.isFinite else { return 1 } + let delta = (magnification - 1) * sensitivity + return max(0.0001, 1 + delta) + } +} + /// Enables or disables selection and scrolling gestures for interactive wrappers. public struct SCChartGestureConfiguration: Equatable, Codable { + /// Whether the wrapper should expose selection gestures. public let allowsSelection: Bool + /// Whether the wrapper should expose horizontal scrolling gestures. public let allowsScrolling: Bool + /// Whether the wrapper should expose pinch-style zoom gestures. + public let allowsZooming: Bool - /// Creates a gesture policy for wrappers that support selection and scrolling. + /// Creates a gesture policy for wrappers that support selection, scrolling, and zooming. public init( allowsSelection: Bool = true, - allowsScrolling: Bool = true + allowsScrolling: Bool = true, + allowsZooming: Bool = true ) { self.allowsSelection = allowsSelection self.allowsScrolling = allowsScrolling + self.allowsZooming = allowsZooming + } + + private enum CodingKeys: String, CodingKey { + case allowsSelection + case allowsScrolling + case allowsZooming + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let allowsSelection = try container.decode(Bool.self, forKey: .allowsSelection) + let allowsScrolling = try container.decode(Bool.self, forKey: .allowsScrolling) + let allowsZooming = try container.decodeIfPresent(Bool.self, forKey: .allowsZooming) + ?? allowsScrolling + self.init( + allowsSelection: allowsSelection, + allowsScrolling: allowsScrolling, + allowsZooming: allowsZooming + ) } + /// Enables selection, scrolling, and zooming together. public static let interactive = SCChartGestureConfiguration() - public static let selectionOnly = SCChartGestureConfiguration(allowsSelection: true, allowsScrolling: false) - public static let scrollOnly = SCChartGestureConfiguration(allowsSelection: false, allowsScrolling: true) - public static let staticOnly = SCChartGestureConfiguration(allowsSelection: false, allowsScrolling: false) + /// Enables selection only. + public static let selectionOnly = SCChartGestureConfiguration( + allowsSelection: true, + allowsScrolling: false, + allowsZooming: false + ) + /// Enables scrolling and zooming without selection. + public static let scrollOnly = SCChartGestureConfiguration( + allowsSelection: false, + allowsScrolling: true, + allowsZooming: true + ) + /// Disables all interactive gesture handling. + public static let staticOnly = SCChartGestureConfiguration( + allowsSelection: false, + allowsScrolling: false, + allowsZooming: false + ) } /// Lightweight hover state that can be bridged to and from a concrete chart selection. diff --git a/Sources/SimpleChart/Native/Core/SCChartLegend.swift b/Sources/SimpleChart/Native/Core/SCChartLegend.swift index 304cc2a..b3354e9 100644 --- a/Sources/SimpleChart/Native/Core/SCChartLegend.swift +++ b/Sources/SimpleChart/Native/Core/SCChartLegend.swift @@ -7,12 +7,14 @@ import SwiftUI +/// Controls whether a chart legend is shown automatically, forced visible, or hidden. public enum SCChartLegendVisibility: String, Codable, Equatable { case automatic case visible case hidden } +/// Places a legend around or over the chart plot area. public enum SCChartLegendPosition: String, Codable, Equatable { case automatic case top @@ -22,12 +24,14 @@ public enum SCChartLegendPosition: String, Codable, Equatable { case overlay } +/// Configures legend visibility, placement, and layout spacing for native wrappers. public struct SCChartLegend: Equatable { public let visibility: SCChartLegendVisibility public let position: SCChartLegendPosition public let alignment: Alignment public let spacing: CGFloat + /// Creates a legend configuration with explicit visibility, placement, and layout settings. public init( visibility: SCChartLegendVisibility = .automatic, position: SCChartLegendPosition = .automatic, @@ -46,6 +50,7 @@ public extension SCChartLegend { static let visible = SCChartLegend(visibility: .visible) static let hidden = SCChartLegend(visibility: .hidden) + /// Returns a visible legend configuration with a custom placement and spacing. static func visible( position: SCChartLegendPosition = .automatic, alignment: Alignment = .center, diff --git a/Sources/SimpleChart/Native/Core/SCChartNavigationCoordinator.swift b/Sources/SimpleChart/Native/Core/SCChartNavigationCoordinator.swift new file mode 100644 index 0000000..d459c46 --- /dev/null +++ b/Sources/SimpleChart/Native/Core/SCChartNavigationCoordinator.swift @@ -0,0 +1,96 @@ +// +// SCChartNavigationCoordinator.swift +// +// +// Created by Codex on 2026-04-13. +// + +import Foundation + +enum SCChartNavigationCoordinator { + static func clampedViewport( + _ viewport: SCChartViewport, + zoomBehavior: SCChartZoomBehavior, + bounds: ClosedRange + ) -> SCChartViewport { + let boundsLength = max(bounds.upperBound - bounds.lowerBound, 0.0001) + let clampedLength = zoomBehavior.clamped(length: viewport.length, within: boundsLength) + return SCChartViewport + .starting(at: viewport.lowerBound, length: clampedLength) + .clamped(to: bounds) + } + + static func clampedViewport( + _ viewport: SCChartTimeViewport, + zoomBehavior: SCChartZoomBehavior, + bounds: ClosedRange + ) -> SCChartTimeViewport { + let boundsLength = max(bounds.upperBound.timeIntervalSince(bounds.lowerBound), 0.0001) + let clampedLength = zoomBehavior.clamped(length: viewport.length, within: boundsLength) + return SCChartTimeViewport + .starting(at: viewport.startDate, duration: clampedLength) + .clamped(to: bounds) + } + + static func scrollViewport( + _ viewport: SCChartViewport, + to lowerBound: Double, + zoomBehavior: SCChartZoomBehavior, + bounds: ClosedRange + ) -> SCChartViewport { + clampedViewport( + SCChartViewport.starting(at: lowerBound, length: viewport.length), + zoomBehavior: zoomBehavior, + bounds: bounds + ) + } + + static func scrollViewport( + _ viewport: SCChartTimeViewport, + to startDate: Date, + zoomBehavior: SCChartZoomBehavior, + bounds: ClosedRange + ) -> SCChartTimeViewport { + clampedViewport( + SCChartTimeViewport.starting(at: startDate, duration: viewport.length), + zoomBehavior: zoomBehavior, + bounds: bounds + ) + } + + static func zoomViewport( + _ viewport: SCChartViewport, + magnification: Double, + center: Double? = nil, + zoomBehavior: SCChartZoomBehavior, + bounds: ClosedRange + ) -> SCChartViewport { + clampedViewport( + viewport.zoomed( + by: zoomBehavior.adjustedMagnification(from: magnification), + centeredAt: center, + within: bounds + ), + zoomBehavior: zoomBehavior, + bounds: bounds + ) + } + + static func zoomViewport( + _ viewport: SCChartTimeViewport, + magnification: Double, + center: Date? = nil, + zoomBehavior: SCChartZoomBehavior, + bounds: ClosedRange + ) -> SCChartTimeViewport { + clampedViewport( + viewport.zoomed( + by: zoomBehavior.adjustedMagnification(from: magnification), + centeredAt: center, + within: bounds + ), + zoomBehavior: zoomBehavior, + bounds: bounds + ) + } +} diff --git a/Sources/SimpleChart/Native/Core/SCChartOverlay.swift b/Sources/SimpleChart/Native/Core/SCChartOverlay.swift index 868b970..022f804 100644 --- a/Sources/SimpleChart/Native/Core/SCChartOverlay.swift +++ b/Sources/SimpleChart/Native/Core/SCChartOverlay.swift @@ -7,6 +7,7 @@ import SwiftUI +/// Describes reusable overlay content that can be layered onto a composed chart. public enum SCChartOverlay: Equatable { case referenceLine(SCChartReferenceLine) case referenceLines([SCChartReferenceLine]) diff --git a/Sources/SimpleChart/Native/Core/SCChartPlotStyle.swift b/Sources/SimpleChart/Native/Core/SCChartPlotStyle.swift index af346d3..d74524d 100644 --- a/Sources/SimpleChart/Native/Core/SCChartPlotStyle.swift +++ b/Sources/SimpleChart/Native/Core/SCChartPlotStyle.swift @@ -7,6 +7,7 @@ import SwiftUI +/// Styles the background, padding, and border of a chart plot area. public struct SCChartPlotStyle: Equatable { public let backgroundColor: Color public let backgroundOpacity: Double @@ -16,6 +17,7 @@ public struct SCChartPlotStyle: Equatable { public let borderColor: Color? public let borderWidth: CGFloat + /// Creates a plot-area style from explicit background, border, and padding values. public init( backgroundColor: Color = .clear, backgroundOpacity: Double = 0, @@ -38,6 +40,7 @@ public struct SCChartPlotStyle: Equatable { public extension SCChartPlotStyle { static let standard = SCChartPlotStyle() + /// Creates a card-style plot background suitable for dashboards and inspector views. static func card( backgroundColor: Color = .secondary, backgroundOpacity: Double = 0.12, diff --git a/Sources/SimpleChart/Native/Core/SCChartPlotSupport.swift b/Sources/SimpleChart/Native/Core/SCChartPlotSupport.swift index 08d9455..3a3e575 100644 --- a/Sources/SimpleChart/Native/Core/SCChartPlotSupport.swift +++ b/Sources/SimpleChart/Native/Core/SCChartPlotSupport.swift @@ -8,6 +8,7 @@ import CoreGraphics import Foundation +/// Controls how overlapping plot-based series should stack vertically. public enum SCChartPlotStacking: String, Codable, Equatable { case standard case normalized @@ -15,6 +16,7 @@ public enum SCChartPlotStacking: String, Codable, Equatable { case unstacked } +/// Describes plot widths, heights, or insets for vectorized plot marks. public enum SCChartPlotDimension: Equatable { case automatic case fixed(CGFloat) diff --git a/Sources/SimpleChart/Native/Core/SCChartScale.swift b/Sources/SimpleChart/Native/Core/SCChartScale.swift index a332e72..ff7c643 100644 --- a/Sources/SimpleChart/Native/Core/SCChartScale.swift +++ b/Sources/SimpleChart/Native/Core/SCChartScale.swift @@ -7,11 +7,13 @@ import Foundation +/// Bundles optional x-domain, y-domain, and foreground-style scaling for composed charts. public struct SCChartScale: Equatable { public let xVisibleDomain: SCChartVisibleDomain? public let yDomain: SCChartDomain? public let foregroundStyleScale: SCChartForegroundStyleScale? + /// Creates a scale bundle from visible-domain, y-domain, and style-scale components. public init( xVisibleDomain: SCChartVisibleDomain? = nil, yDomain: SCChartDomain? = nil, @@ -26,18 +28,22 @@ public struct SCChartScale: Equatable { public extension SCChartScale { static let automatic = SCChartScale() + /// Returns a scale bundle with only an x visible-domain override. static func visible(x domain: SCChartVisibleDomain) -> SCChartScale { SCChartScale(xVisibleDomain: domain) } + /// Returns a scale bundle with only a y-domain override. static func y(_ domain: SCChartDomain) -> SCChartScale { SCChartScale(yDomain: domain) } + /// Returns a scale bundle with only a foreground-style scale override. static func foregroundStyleScale(_ scale: SCChartForegroundStyleScale) -> SCChartScale { SCChartScale(foregroundStyleScale: scale) } + /// Returns a scale bundle with a fixed y-domain range. static func fixed(y range: ClosedRange) -> SCChartScale { SCChartScale( yDomain: SCChartDomain( diff --git a/Sources/SimpleChart/Native/Core/SCChartValueFormat.swift b/Sources/SimpleChart/Native/Core/SCChartValueFormat.swift index 4476ad2..4fdc53f 100644 --- a/Sources/SimpleChart/Native/Core/SCChartValueFormat.swift +++ b/Sources/SimpleChart/Native/Core/SCChartValueFormat.swift @@ -7,6 +7,7 @@ import Foundation +/// Formats numeric values for axis labels, annotations, and inspection callouts. public enum SCChartNumericValueFormat: Equatable, Codable { case automatic case compact @@ -14,6 +15,7 @@ public enum SCChartNumericValueFormat: Equatable, Codable { case percent(precision: Int = 0) case number(precision: Int = 0) + /// Converts a numeric value into display text using the selected formatting strategy. public func string(from value: Double) -> String { switch self { case .automatic: @@ -36,6 +38,7 @@ public enum SCChartNumericValueFormat: Equatable, Codable { } } +/// Formats dates for time-series axes, annotations, and inspection callouts. public enum SCChartDateValueFormat: Equatable, Codable { case automatic case date @@ -44,6 +47,7 @@ public enum SCChartDateValueFormat: Equatable, Codable { case monthYear case hourMinute + /// Converts a date into display text using the selected formatting strategy. public func string(from date: Date) -> String { switch self { case .automatic: diff --git a/Sources/SimpleChart/Native/Core/SCChartVisibleDomain.swift b/Sources/SimpleChart/Native/Core/SCChartVisibleDomain.swift index 2136a43..df53706 100644 --- a/Sources/SimpleChart/Native/Core/SCChartVisibleDomain.swift +++ b/Sources/SimpleChart/Native/Core/SCChartVisibleDomain.swift @@ -7,41 +7,51 @@ import Foundation +/// Describes how much of a scrollable x-domain should stay visible at once. public struct SCChartVisibleDomain: Equatable, Codable { public let length: Double + /// Creates a visible-domain window from a raw numeric length. public init(length: Double) { self.length = max(length, 0.0001) } + /// Creates a visible-domain window measured in categorical points. public static func points(_ count: Int) -> SCChartVisibleDomain { SCChartVisibleDomain(length: Double(max(count, 1))) } + /// Creates a visible-domain window measured in seconds. public static func seconds(_ duration: TimeInterval) -> SCChartVisibleDomain { SCChartVisibleDomain(length: duration) } + /// Creates a visible-domain window measured in minutes. public static func minutes(_ duration: Double) -> SCChartVisibleDomain { SCChartVisibleDomain(length: duration * 60) } + /// Creates a visible-domain window measured in hours. public static func hours(_ duration: Double) -> SCChartVisibleDomain { SCChartVisibleDomain(length: duration * 60 * 60) } + /// Creates a visible-domain window measured in days. public static func days(_ duration: Double) -> SCChartVisibleDomain { SCChartVisibleDomain(length: duration * 60 * 60 * 24) } + /// Creates a visible-domain window measured in weeks. public static func weeks(_ duration: Double) -> SCChartVisibleDomain { .days(duration * 7) } + /// Returns the default analytics-style window size in visible points. public static func analytics(points count: Int = 14) -> SCChartVisibleDomain { .points(count) } + /// Returns the default finance-style window size in visible trading days. public static func finance(tradingDays count: Int = 5) -> SCChartVisibleDomain { .points(count) } diff --git a/Sources/SimpleChart/Native/Core/SCHistogramBinning.swift b/Sources/SimpleChart/Native/Core/SCHistogramBinning.swift index 6d4b2a4..b2567e9 100644 --- a/Sources/SimpleChart/Native/Core/SCHistogramBinning.swift +++ b/Sources/SimpleChart/Native/Core/SCHistogramBinning.swift @@ -7,7 +7,9 @@ import Foundation +/// Builds histogram bins from raw numeric values when callers do not pre-bin data. public enum SCHistogramBinning { + /// Converts raw values into evenly sized histogram bins. public static func makeBins(values: [Double], binCount: Int) -> [SCHistogramBin] { guard !values.isEmpty else { return [] } diff --git a/Sources/SimpleChart/Native/Core/SCNativeChartContainer.swift b/Sources/SimpleChart/Native/Core/SCNativeChartContainer.swift index 63a67e2..98665be 100644 --- a/Sources/SimpleChart/Native/Core/SCNativeChartContainer.swift +++ b/Sources/SimpleChart/Native/Core/SCNativeChartContainer.swift @@ -7,6 +7,7 @@ import SwiftUI +/// Wraps a `Chart` with shared axis-title, legend, and plot-area presentation used by native wrappers. public struct SCNativeChartContainer: View { private let xAxis: SCChartAxis private let yAxis: SCChartAxis @@ -14,6 +15,7 @@ public struct SCNativeChartContainer: View { private let plotStyle: SCChartPlotStyle private let content: Content + /// Creates a chart container that derives axis titles from a shared axes style unless overridden. public init( axesStyle: SCChartAxesStyle, xAxis: SCChartAxis? = nil, diff --git a/Sources/SimpleChart/Native/Core/SCNativeChartSupport.swift b/Sources/SimpleChart/Native/Core/SCNativeChartSupport.swift index a288a1b..1b21ad3 100644 --- a/Sources/SimpleChart/Native/Core/SCNativeChartSupport.swift +++ b/Sources/SimpleChart/Native/Core/SCNativeChartSupport.swift @@ -196,6 +196,7 @@ extension SCChartPlotDimension { } } +#if compiler(>=6.3) @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) @@ -224,6 +225,7 @@ extension SCChart3DPoseStyle { } } } +#endif extension Array { subscript(safe index: Int) -> Element? { diff --git a/Sources/SimpleChart/Native/Marks/SCChartMark.swift b/Sources/SimpleChart/Native/Marks/SCChartMark.swift index 1c620c6..8df36f7 100644 --- a/Sources/SimpleChart/Native/Marks/SCChartMark.swift +++ b/Sources/SimpleChart/Native/Marks/SCChartMark.swift @@ -7,6 +7,7 @@ import Foundation +/// Describes the mark families that can be combined inside `SCComposedChart`. public enum SCChartMark: Equatable { case line([SCChartPoint], style: SCChartSeriesStyle = .line()) case area([SCChartPoint], style: SCChartSeriesStyle = .area()) diff --git a/Sources/SimpleChart/Native/Models/SCChart3DPoint.swift b/Sources/SimpleChart/Native/Models/SCChart3DPoint.swift index 3f5bcad..6b8233b 100644 --- a/Sources/SimpleChart/Native/Models/SCChart3DPoint.swift +++ b/Sources/SimpleChart/Native/Models/SCChart3DPoint.swift @@ -7,6 +7,7 @@ import Foundation +/// Represents a single point in 3D chart space. public struct SCChart3DPoint: Identifiable, Equatable { public let id: String public let x: Double @@ -14,6 +15,7 @@ public struct SCChart3DPoint: Identifiable, Equatable { public let z: Double public let label: String? + /// Creates a 3D point with optional label metadata. public init( id: String? = nil, x: Double, @@ -30,6 +32,7 @@ public struct SCChart3DPoint: Identifiable, Equatable { } public extension SCChart3DPoint { + /// Builds 3D points from floating-point tuples and optional parallel labels. static func make( points: [(T, U, V)], labels: [String]? = nil @@ -45,6 +48,7 @@ public extension SCChart3DPoint { } } + /// Builds 3D points from integer tuples and optional parallel labels. static func make( points: [(T, U, V)], labels: [String]? = nil diff --git a/Sources/SimpleChart/Native/Models/SCChartBand.swift b/Sources/SimpleChart/Native/Models/SCChartBand.swift index b868b22..62b0289 100644 --- a/Sources/SimpleChart/Native/Models/SCChartBand.swift +++ b/Sources/SimpleChart/Native/Models/SCChartBand.swift @@ -7,6 +7,7 @@ import SwiftUI +/// Represents a highlighted y-range band that can be overlaid on another chart. public struct SCChartBand: Identifiable, Equatable { public let id: String public let title: String @@ -16,6 +17,7 @@ public struct SCChartBand: Identifiable, Equatable { public let opacity: Double public let annotation: SCChartAnnotation? + /// Creates a band from explicit bounds, fill styling, and optional annotation. public init( id: String? = nil, title: String, @@ -38,6 +40,7 @@ public struct SCChartBand: Identifiable, Equatable { } public extension SCChartBand { + /// Builds bands from floating-point tuples of title, lower bound, and upper bound. static func make( bands: [(String, T, T)], color: Color = .accentColor, @@ -54,6 +57,7 @@ public extension SCChartBand { } } + /// Builds bands from integer tuples of title, lower bound, and upper bound. static func make( bands: [(String, T, T)], color: Color = .accentColor, diff --git a/Sources/SimpleChart/Native/Models/SCChartBarGroup.swift b/Sources/SimpleChart/Native/Models/SCChartBarGroup.swift index 9173d0d..1e75974 100644 --- a/Sources/SimpleChart/Native/Models/SCChartBarGroup.swift +++ b/Sources/SimpleChart/Native/Models/SCChartBarGroup.swift @@ -7,11 +7,13 @@ import Foundation +/// Represents a single series entry inside a grouped bar category. public struct SCChartBarGroupEntry: Identifiable, Equatable { public let id: String public let series: String public let value: Double + /// Creates a grouped-bar series entry from a series label and value. public init(id: String? = nil, series: String, value: Double) { self.id = id ?? series self.series = series @@ -19,11 +21,13 @@ public struct SCChartBarGroupEntry: Identifiable, Equatable { } } +/// Represents a single grouped-bar category and its per-series entries. public struct SCChartBarGroup: Identifiable, Equatable { public let id: String public let category: String public let entries: [SCChartBarGroupEntry] + /// Creates a grouped-bar category with prebuilt entries. public init(id: String? = nil, category: String, entries: [SCChartBarGroupEntry]) { self.id = id ?? category self.category = category @@ -32,6 +36,7 @@ public struct SCChartBarGroup: Identifiable, Equatable { } public extension SCChartBarGroup { + /// Builds a grouped-bar category from floating-point `(series, value)` tuples. static func make( label: String, values: [(String, T)] @@ -42,6 +47,7 @@ public extension SCChartBarGroup { ) } + /// Builds a grouped-bar category from integer `(series, value)` tuples. static func make( label: String, values: [(String, T)] diff --git a/Sources/SimpleChart/Native/Models/SCChartLineSeries.swift b/Sources/SimpleChart/Native/Models/SCChartLineSeries.swift index 99ced19..5ee83b2 100644 --- a/Sources/SimpleChart/Native/Models/SCChartLineSeries.swift +++ b/Sources/SimpleChart/Native/Models/SCChartLineSeries.swift @@ -7,12 +7,14 @@ import Foundation +/// Represents one named series inside a multi-line chart. public struct SCChartLineSeries: Identifiable, Equatable { public let id: String public let name: String public let points: [SCChartPoint] public let style: SCChartSeriesStyle + /// Creates a named line series from prebuilt points and optional styling. public init( id: String? = nil, name: String, @@ -27,6 +29,7 @@ public struct SCChartLineSeries: Identifiable, Equatable { } public extension SCChartLineSeries { + /// Builds a line series from floating-point values and optional labels. static func make( name: String, values: [T], @@ -40,6 +43,7 @@ public extension SCChartLineSeries { ) } + /// Builds a line series from integer values and optional labels. static func make( name: String, values: [T], diff --git a/Sources/SimpleChart/Native/Models/SCChartPlotPoint.swift b/Sources/SimpleChart/Native/Models/SCChartPlotPoint.swift index d6f7e15..5f8cc53 100644 --- a/Sources/SimpleChart/Native/Models/SCChartPlotPoint.swift +++ b/Sources/SimpleChart/Native/Models/SCChartPlotPoint.swift @@ -7,12 +7,14 @@ import Foundation +/// Represents a single x/y point for vectorized plot-based charts. public struct SCChartPlotPoint: Identifiable, Equatable { public let id: String public let x: Double public let y: Double public let seriesName: String? + /// Creates a plot point from explicit x/y coordinates and optional series metadata. public init( id: String? = nil, x: Double, @@ -26,6 +28,7 @@ public struct SCChartPlotPoint: Identifiable, Equatable { } } +/// Represents a horizontal plot span with shared y-value and x start/end bounds. public struct SCChartPlotSpan: Identifiable, Equatable { public let id: String public let xStart: Double @@ -33,6 +36,7 @@ public struct SCChartPlotSpan: Identifiable, Equatable { public let y: Double public let seriesName: String? + /// Creates a plot span from explicit x-range, y-value, and optional series metadata. public init( id: String? = nil, xStart: Double, @@ -50,6 +54,7 @@ public struct SCChartPlotSpan: Identifiable, Equatable { } } +/// Represents a vertical plot range with shared x-value and y start/end bounds. public struct SCChartPlotRange: Identifiable, Equatable { public let id: String public let x: Double @@ -57,6 +62,7 @@ public struct SCChartPlotRange: Identifiable, Equatable { public let yEnd: Double public let seriesName: String? + /// Creates a plot range from explicit x-value, y-bounds, and optional series metadata. public init( id: String? = nil, x: Double, @@ -74,6 +80,7 @@ public struct SCChartPlotRange: Identifiable, Equatable { } } +/// Represents a plot rectangle bounded by x and y start/end coordinates. public struct SCChartPlotRectangle: Identifiable, Equatable { public let id: String public let xStart: Double @@ -82,6 +89,7 @@ public struct SCChartPlotRectangle: Identifiable, Equatable { public let yEnd: Double public let seriesName: String? + /// Creates a plot rectangle from explicit corner bounds and optional series metadata. public init( id: String? = nil, xStart: Double, @@ -105,6 +113,7 @@ public struct SCChartPlotRectangle: Identifiable, Equatable { } public extension SCChartPlotPoint { + /// Builds plot points from floating-point `(x, y)` tuples. static func make( points: [(T, U)], seriesName: String? = nil @@ -119,6 +128,7 @@ public extension SCChartPlotPoint { } } + /// Builds plot points from integer `(x, y)` tuples. static func make( points: [(T, U)], seriesName: String? = nil @@ -133,6 +143,7 @@ public extension SCChartPlotPoint { } } + /// Builds plot points from parallel floating-point x and y arrays. static func make( xValues: [T], yValues: [U], @@ -150,6 +161,7 @@ public extension SCChartPlotPoint { } public extension SCChartPlotSpan { + /// Builds plot spans from floating-point `(xStart, xEnd, y)` tuples. static func make( spans: [(T, T, U)], seriesName: String? = nil @@ -165,6 +177,7 @@ public extension SCChartPlotSpan { } } + /// Builds plot spans from integer `(xStart, xEnd, y)` tuples. static func make( spans: [(T, T, U)], seriesName: String? = nil @@ -182,6 +195,7 @@ public extension SCChartPlotSpan { } public extension SCChartPlotRange { + /// Builds plot ranges from floating-point `(x, yStart, yEnd)` tuples. static func make( ranges: [(T, U, U)], seriesName: String? = nil @@ -197,6 +211,7 @@ public extension SCChartPlotRange { } } + /// Builds plot ranges from integer `(x, yStart, yEnd)` tuples. static func make( ranges: [(T, U, U)], seriesName: String? = nil @@ -214,6 +229,7 @@ public extension SCChartPlotRange { } public extension SCChartPlotRectangle { + /// Builds plot rectangles from floating-point `(xStart, xEnd, yStart, yEnd)` tuples. static func make( rectangles: [(T, T, U, U)], seriesName: String? = nil @@ -230,6 +246,7 @@ public extension SCChartPlotRectangle { } } + /// Builds plot rectangles from integer `(xStart, xEnd, yStart, yEnd)` tuples. static func make( rectangles: [(T, T, U, U)], seriesName: String? = nil diff --git a/Sources/SimpleChart/Native/Models/SCChartRectangle.swift b/Sources/SimpleChart/Native/Models/SCChartRectangle.swift index b25732c..a1ba266 100644 --- a/Sources/SimpleChart/Native/Models/SCChartRectangle.swift +++ b/Sources/SimpleChart/Native/Models/SCChartRectangle.swift @@ -7,6 +7,7 @@ import SwiftUI +/// Represents a rectangle mark bounded by categorical x positions and numeric y bounds. public struct SCChartRectangle: Identifiable, Equatable { public let id: String public let xStart: Double @@ -16,6 +17,7 @@ public struct SCChartRectangle: Identifiable, Equatable { public let color: Color? public let annotation: SCChartAnnotation? + /// Creates a rectangle mark from explicit x/y bounds and optional styling. public init( id: String? = nil, xStart: Double, diff --git a/Sources/SimpleChart/Native/Models/SCChartScatterPoint.swift b/Sources/SimpleChart/Native/Models/SCChartScatterPoint.swift index dc66770..9fbab83 100644 --- a/Sources/SimpleChart/Native/Models/SCChartScatterPoint.swift +++ b/Sources/SimpleChart/Native/Models/SCChartScatterPoint.swift @@ -7,12 +7,14 @@ import Foundation +/// Represents a single scatter-plot point with optional label metadata. public struct SCChartScatterPoint: Identifiable, Equatable { public let id: String public let x: Double public let y: Double public let label: String? + /// Creates a scatter point from explicit x/y coordinates and optional label text. public init(id: String, x: Double, y: Double, label: String? = nil) { self.id = id self.x = x diff --git a/Sources/SimpleChart/Native/Models/SCChartSectorSegment.swift b/Sources/SimpleChart/Native/Models/SCChartSectorSegment.swift index 95d955a..4044a03 100644 --- a/Sources/SimpleChart/Native/Models/SCChartSectorSegment.swift +++ b/Sources/SimpleChart/Native/Models/SCChartSectorSegment.swift @@ -7,12 +7,14 @@ import SwiftUI +/// Represents one segment in a sector or donut chart. public struct SCChartSectorSegment: Identifiable, Equatable { public let id: String public let title: String public let value: Double public let color: Color? + /// Creates a sector segment from title, value, and optional explicit color. public init(id: String? = nil, title: String, value: Double, color: Color? = nil) { self.id = id ?? title self.title = title diff --git a/Sources/SimpleChart/Native/Models/SCChartSelection.swift b/Sources/SimpleChart/Native/Models/SCChartSelection.swift index 5a1ef9f..8b007c6 100644 --- a/Sources/SimpleChart/Native/Models/SCChartSelection.swift +++ b/Sources/SimpleChart/Native/Models/SCChartSelection.swift @@ -7,11 +7,13 @@ import Foundation +/// Captures the currently selected series, x-label, and value for interactive charts. public struct SCChartSelection: Equatable { public let seriesName: String? public let xLabel: String? public let value: Double + /// Creates a selection snapshot from optional series and x-label metadata plus a numeric value. public init(seriesName: String? = nil, xLabel: String? = nil, value: Double) { self.seriesName = seriesName self.xLabel = xLabel diff --git a/Sources/SimpleChart/Native/Models/SCChartStackSegment.swift b/Sources/SimpleChart/Native/Models/SCChartStackSegment.swift index 5744480..7ab0f90 100644 --- a/Sources/SimpleChart/Native/Models/SCChartStackSegment.swift +++ b/Sources/SimpleChart/Native/Models/SCChartStackSegment.swift @@ -7,12 +7,14 @@ import Foundation +/// Represents a single stacked-bar segment inside a category. public struct SCChartStackSegment: Identifiable, Equatable { public let id: String public let category: String public let segment: String public let value: Double + /// Creates a stacked-bar segment from category, segment label, and value. public init(id: String? = nil, category: String, segment: String, value: Double) { self.id = id ?? "\(category)-\(segment)" self.category = category diff --git a/Sources/SimpleChart/Native/Models/SCChartTimeViewport.swift b/Sources/SimpleChart/Native/Models/SCChartTimeViewport.swift new file mode 100644 index 0000000..74531c5 --- /dev/null +++ b/Sources/SimpleChart/Native/Models/SCChartTimeViewport.swift @@ -0,0 +1,102 @@ +// +// SCChartTimeViewport.swift +// +// +// Created by Codex on 2026-04-13. +// + +import Foundation + +/// Represents a date-based x-domain window used for scrolling and zoom coordination. +public struct SCChartTimeViewport: Equatable { + /// The first date included in the visible window. + public let startDate: Date + /// The last date included in the visible window. + public let endDate: Date + + /// Creates a viewport from explicit start and end dates. + /// + /// The initializer normalizes the inputs so `startDate` is always less than or equal to `endDate`. + public init(startDate: Date, endDate: Date) { + self.startDate = min(startDate, endDate) + self.endDate = max(startDate, endDate) + } + + /// The viewport expressed as a closed date range. + public var range: ClosedRange { + startDate...endDate + } + + /// The time span covered by the viewport in seconds. + public var length: TimeInterval { + endDate.timeIntervalSince(startDate) + } + + /// Creates a viewport that starts at a date and extends for a duration in seconds. + /// + /// - Parameters: + /// - startDate: The leading edge of the visible window. + /// - duration: The requested time span in seconds. + public static func starting(at startDate: Date, duration: TimeInterval) -> SCChartTimeViewport { + SCChartTimeViewport( + startDate: startDate, + endDate: startDate.addingTimeInterval(max(duration, 0)) + ) + } + + /// Creates a viewport centered on a specific date for a duration in seconds. + /// + /// - Parameters: + /// - center: The date that should sit in the middle of the window. + /// - duration: The requested time span in seconds. + public static func centered(at center: Date, duration: TimeInterval) -> SCChartTimeViewport { + let safeDuration = max(duration, 0) + let halfDuration = safeDuration / 2 + return SCChartTimeViewport( + startDate: center.addingTimeInterval(-halfDuration), + endDate: center.addingTimeInterval(halfDuration) + ) + } + + /// Returns a viewport moved to a new start date while keeping the same duration. + /// + /// - Parameter startDate: The new leading edge of the window. + public func shifted(to startDate: Date) -> SCChartTimeViewport { + .starting(at: startDate, duration: length) + } + + /// Clamps the viewport so it fits entirely inside a larger date range. + /// + /// Use this after applying external scroll or zoom input when the visible window must stay within the data bounds. + public func clamped(to bounds: ClosedRange) -> SCChartTimeViewport { + guard bounds.lowerBound <= bounds.upperBound else { return self } + let boundsLength = bounds.upperBound.timeIntervalSince(bounds.lowerBound) + let effectiveLength = min(length, boundsLength) + let latestStartDate = bounds.upperBound.addingTimeInterval(-effectiveLength) + let proposedStartDate = min( + max(startDate, bounds.lowerBound), + latestStartDate + ) + return .starting(at: proposedStartDate, duration: effectiveLength) + } + + /// Returns a zoomed viewport around an optional center and optional outer bounds. + /// + /// - Parameters: + /// - factor: A magnification factor where values greater than `1` zoom in and values between `0` and `1` zoom out. + /// - center: The date that should remain visually anchored during the zoom operation. When omitted, the current midpoint is used. + /// - bounds: Optional outer limits that the zoomed viewport must remain inside. + public func zoomed( + by factor: Double, + centeredAt center: Date? = nil, + within bounds: ClosedRange? = nil + ) -> SCChartTimeViewport { + let safeFactor = max(factor, 0.0001) + let targetCenter = center ?? startDate.addingTimeInterval(length / 2) + let zoomed = SCChartTimeViewport.centered(at: targetCenter, duration: length / safeFactor) + if let bounds { + return zoomed.clamped(to: bounds) + } + return zoomed + } +} diff --git a/Sources/SimpleChart/Native/Models/SCChartViewport.swift b/Sources/SimpleChart/Native/Models/SCChartViewport.swift index 2efae85..ead16d3 100644 --- a/Sources/SimpleChart/Native/Models/SCChartViewport.swift +++ b/Sources/SimpleChart/Native/Models/SCChartViewport.swift @@ -7,27 +7,33 @@ import Foundation +/// Represents a numeric x-domain window used for scrolling and zoom coordination. public struct SCChartViewport: Equatable { public let lowerBound: Double public let upperBound: Double + /// Creates a viewport from explicit lower and upper bounds. public init(lowerBound: Double, upperBound: Double) { self.lowerBound = min(lowerBound, upperBound) self.upperBound = max(lowerBound, upperBound) } + /// The viewport expressed as a closed numeric range. public var range: ClosedRange { lowerBound...upperBound } + /// The numeric span covered by the viewport. public var length: Double { upperBound - lowerBound } + /// Creates a viewport that starts at a lower bound and extends for a given length. public static func starting(at lowerBound: Double, length: Double) -> SCChartViewport { SCChartViewport(lowerBound: lowerBound, upperBound: lowerBound + max(length, 0)) } + /// Creates a viewport centered on a specific value. public static func centered(at center: Double, length: Double) -> SCChartViewport { let effectiveLength = max(length, 0) let halfLength = effectiveLength / 2 @@ -37,10 +43,12 @@ public struct SCChartViewport: Equatable { ) } + /// Returns a viewport moved to a new lower bound while keeping the same length. public func shifted(to lowerBound: Double) -> SCChartViewport { SCChartViewport(lowerBound: lowerBound, upperBound: lowerBound + length) } + /// Clamps the viewport so it fits entirely inside a larger bounds range. public func clamped(to bounds: ClosedRange) -> SCChartViewport { guard bounds.lowerBound <= bounds.upperBound else { return self } let effectiveLength = min(length, bounds.upperBound - bounds.lowerBound) @@ -51,6 +59,7 @@ public struct SCChartViewport: Equatable { return SCChartViewport.starting(at: proposedLowerBound, length: effectiveLength) } + /// Returns a zoomed viewport around an optional center and optional outer bounds. public func zoomed( by factor: Double, centeredAt center: Double? = nil, diff --git a/Sources/SimpleChart/Native/Models/SCHistogramBin.swift b/Sources/SimpleChart/Native/Models/SCHistogramBin.swift index 41523a2..ef1887f 100644 --- a/Sources/SimpleChart/Native/Models/SCHistogramBin.swift +++ b/Sources/SimpleChart/Native/Models/SCHistogramBin.swift @@ -7,12 +7,14 @@ import Foundation +/// Represents a precomputed histogram bucket with lower/upper bounds and count. public struct SCHistogramBin: Identifiable, Equatable, Codable { public let id: String public let lowerBound: Double public let upperBound: Double public let count: Int + /// Creates a histogram bin from explicit bounds and a bucket count. public init(id: String, lowerBound: Double, upperBound: Double, count: Int) { self.id = id self.lowerBound = lowerBound diff --git a/Sources/SimpleChart/SCBarChart/SCBarChart.swift b/Sources/SimpleChart/SCBarChart/SCBarChart.swift index 01da6d7..9242c3e 100644 --- a/Sources/SimpleChart/SCBarChart/SCBarChart.swift +++ b/Sources/SimpleChart/SCBarChart/SCBarChart.swift @@ -8,11 +8,13 @@ import SwiftUI @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) +/// Deprecated compatibility wrapper that forwards the original bar-chart config API to the native bar-chart implementation. public struct SCBarChart: View { let chartData: [SCBarChartData] let chartConfig: SCBarChartConfig @available(*, deprecated, message: "Use SCNativeBarChart with SCChartPoint, SCChartSeriesStyle, and SCChartAxesStyle instead.") + /// Creates the deprecated bar-chart wrapper from a legacy configuration value. public init(config: SCBarChartConfig) { self.chartData = config.chartData self.chartConfig = config diff --git a/Sources/SimpleChart/SCBarChart/SCBarChartConfig.swift b/Sources/SimpleChart/SCBarChart/SCBarChartConfig.swift index bed797b..e2ba676 100644 --- a/Sources/SimpleChart/SCBarChart/SCBarChartConfig.swift +++ b/Sources/SimpleChart/SCBarChart/SCBarChartConfig.swift @@ -10,6 +10,7 @@ import SwiftUI //@available(iOS 15, macOS 12.0, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +/// Deprecated legacy configuration container for the original bar-chart API. public struct SCBarChartConfig { let chartData: [SCBarChartData] @@ -42,6 +43,7 @@ public struct SCBarChartConfig { let yAxisFigureFontFactor: Double @available(*, deprecated, message: "Use SCNativeBarChart with SCChartPoint, SCChartSeriesStyle, SCChartAxesStyle, and SCChartDomain instead.") + /// Creates a legacy bar-chart configuration value for compatibility with the original API surface. public init(chartData: [SCBarChartData], baseZero: Bool = false, showInterval: Bool = false, showXAxis: Bool = false, showYAxis: Bool = false, showYAxisFigure: Bool = false, showLegend: Bool = false, showLabel: Bool = false, intervalColor: Color = .secondary, intervalLineWidth: CGFloat = 0.5, stroke: Bool = false, strokeWidth: CGFloat = 1, color: [Color] = [.primary], numOfInterval: Int = 3, xLegend: String = "", yLegend: String = "", xLegendColor: Color = .primary, yLegendColor: Color = .primary, gradientStart: UnitPoint = .top, gradientEnd: UnitPoint = .bottom, yAxisFigureColor: Color = .secondary, yAxisFigureFontFactor: Double = 0.06667, minLower: Double = Double.infinity, maxUpper: Double = -Double.infinity) { var chartData = chartData if chartData.isEmpty { diff --git a/Sources/SimpleChart/SCBarChart/SCBarChartData.swift b/Sources/SimpleChart/SCBarChart/SCBarChartData.swift index ec95927..f17b65d 100644 --- a/Sources/SimpleChart/SCBarChart/SCBarChartData.swift +++ b/Sources/SimpleChart/SCBarChart/SCBarChartData.swift @@ -9,12 +9,14 @@ import Foundation //@available(iOS 15, macOS 12.0, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +/// Deprecated legacy data point for the original bar-chart API. public struct SCBarChartData: Codable, Equatable { init(rawValue: Double) { self.value = rawValue } @available(*, deprecated, message: "Use SCChartPoint instead.") + /// Creates a legacy bar-chart data point from a numeric value. public init(_ value: Double){ self.value = value } diff --git a/Sources/SimpleChart/SCHistogram/SCHistogram.swift b/Sources/SimpleChart/SCHistogram/SCHistogram.swift index 897cc71..b9db6e1 100644 --- a/Sources/SimpleChart/SCHistogram/SCHistogram.swift +++ b/Sources/SimpleChart/SCHistogram/SCHistogram.swift @@ -8,11 +8,13 @@ import SwiftUI @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) +/// Deprecated compatibility wrapper that forwards the original histogram config API to the native histogram implementation. public struct SCHistogram: View { let chartData: [SCHistogramData] let chartConfig: SCHistogramConfig @available(*, deprecated, message: "Use SCNativeHistogramChart with SCHistogramBin or raw values plus SCChartAxesStyle instead.") + /// Creates the deprecated histogram wrapper from a legacy configuration value. public init(config: SCHistogramConfig) { self.chartData = config.chartData self.chartConfig = config diff --git a/Sources/SimpleChart/SCHistogram/SCHistogramConfig.swift b/Sources/SimpleChart/SCHistogram/SCHistogramConfig.swift index a2e2fad..adc063e 100644 --- a/Sources/SimpleChart/SCHistogram/SCHistogramConfig.swift +++ b/Sources/SimpleChart/SCHistogram/SCHistogramConfig.swift @@ -10,6 +10,7 @@ import SwiftUI //@available(iOS 15, macOS 12.0, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +/// Deprecated legacy configuration container for the original histogram API. public struct SCHistogramConfig { let chartData: [SCHistogramData] @@ -42,6 +43,7 @@ public struct SCHistogramConfig { let yAxisFigureFontFactor: Double @available(*, deprecated, message: "Use SCNativeHistogramChart with SCHistogramBin or raw values plus SCChartAxesStyle and SCChartDomain instead.") + /// Creates a legacy histogram configuration value for compatibility with the original API surface. public init(chartData: [SCHistogramData], baseZero: Bool = false, showInterval: Bool = false, showXAxis: Bool = false, showYAxis: Bool = false, showYAxisFigure: Bool = false, showLegend: Bool = false, showLabel: Bool = false, intervalColor: Color = .secondary, intervalLineWidth: CGFloat = 0.5, stroke: Bool = false, strokeWidth: CGFloat = 1, color: [Color] = [.primary], numOfInterval: Int = 3, xLegend: String = "", yLegend: String = "", xLegendColor: Color = .primary, yLegendColor: Color = .primary, gradientStart: UnitPoint = .top, gradientEnd: UnitPoint = .bottom, yAxisFigureColor: Color = .secondary, yAxisFigureFontFactor: Double = 0.06667, minLower: Double = Double.infinity, maxUpper: Double = -Double.infinity){ var chartData = chartData if chartData.isEmpty { diff --git a/Sources/SimpleChart/SCHistogram/SCHistogramData.swift b/Sources/SimpleChart/SCHistogram/SCHistogramData.swift index c3657ac..e9466e4 100644 --- a/Sources/SimpleChart/SCHistogram/SCHistogramData.swift +++ b/Sources/SimpleChart/SCHistogram/SCHistogramData.swift @@ -9,12 +9,14 @@ import Foundation //@available(iOS 15, macOS 12.0, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +/// Deprecated legacy data point for the original histogram API. public struct SCHistogramData: Codable, Equatable { init(rawValue: Double) { self.value = rawValue } @available(*, deprecated, message: "Use SCHistogramBin or SCChartPoint-based histogram input instead.") + /// Creates a legacy histogram data point from a numeric value. public init(_ value: Double){ self.value = value } diff --git a/Sources/SimpleChart/SCLineChart/SCLineChart.swift b/Sources/SimpleChart/SCLineChart/SCLineChart.swift index 1eb00c0..dc64567 100644 --- a/Sources/SimpleChart/SCLineChart/SCLineChart.swift +++ b/Sources/SimpleChart/SCLineChart/SCLineChart.swift @@ -8,11 +8,13 @@ import SwiftUI @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) +/// Deprecated compatibility wrapper that forwards the original line-chart config API to the native line-chart implementation. public struct SCLineChart: View { let chartData: [SCLineChartData] let chartConfig: SCLineChartConfig @available(*, deprecated, message: "Use SCNativeLineChart with SCChartPoint, SCChartSeriesStyle, and SCChartAxesStyle instead.") + /// Creates the deprecated line-chart wrapper from a legacy configuration value. public init(config: SCLineChartConfig) { self.chartData = config.chartData self.chartConfig = config diff --git a/Sources/SimpleChart/SCLineChart/SCLineChartConfig.swift b/Sources/SimpleChart/SCLineChart/SCLineChartConfig.swift index 3cd33c7..5dee778 100644 --- a/Sources/SimpleChart/SCLineChart/SCLineChartConfig.swift +++ b/Sources/SimpleChart/SCLineChart/SCLineChartConfig.swift @@ -10,6 +10,7 @@ import SwiftUI //@available(iOS 15, macOS 12.0, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +/// Deprecated legacy configuration container for the original line-chart API. public struct SCLineChartConfig { let chartData: [SCLineChartData] let baseZero: Bool @@ -41,6 +42,7 @@ public struct SCLineChartConfig { let yAxisFigureFontFactor: Double @available(*, deprecated, message: "Use SCNativeLineChart with SCChartPoint, SCChartSeriesStyle, SCChartAxesStyle, and SCChartDomain instead.") + /// Creates a legacy line-chart configuration value for compatibility with the original API surface. public init(chartData: [SCLineChartData], baseZero: Bool = false, showInterval: Bool = false, showXAxis: Bool = false, showYAxis: Bool = false, showYAxisFigure: Bool = false, showLegend: Bool = false, showLabel: Bool = false, intervalColor: Color = .secondary, intervalLineWidth: CGFloat = 0.5, stroke: Bool = false, strokeWidth: CGFloat = 1, color: [Color] = [.primary], numOfInterval: Int = 3, xLegend: String = "", yLegend: String = "", xLegendColor: Color = .primary, yLegendColor: Color = .primary, gradientStart: UnitPoint = .top, gradientEnd: UnitPoint = .bottom, yAxisFigureColor: Color = .secondary, yAxisFigureFontFactor: Double = 0.06667, minLower: Double = Double.infinity, maxUpper: Double = -Double.infinity){ var chartData = chartData if chartData.isEmpty { diff --git a/Sources/SimpleChart/SCLineChart/SCLineChartData.swift b/Sources/SimpleChart/SCLineChart/SCLineChartData.swift index 2041e76..6dd9610 100644 --- a/Sources/SimpleChart/SCLineChart/SCLineChartData.swift +++ b/Sources/SimpleChart/SCLineChart/SCLineChartData.swift @@ -9,12 +9,14 @@ import Foundation //@available(iOS 15, macOS 12.0, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) +/// Deprecated legacy data point for the original line-chart API. public struct SCLineChartData: Codable, Equatable { init(rawValue: Double) { self.value = rawValue } @available(*, deprecated, message: "Use SCChartPoint instead.") + /// Creates a legacy line-chart data point from a numeric value. public init(_ value: Double){ self.value = value } diff --git a/Sources/SimpleChart/SCManager.swift b/Sources/SimpleChart/SCManager.swift index 7f533e2..51682f8 100644 --- a/Sources/SimpleChart/SCManager.swift +++ b/Sources/SimpleChart/SCManager.swift @@ -10,32 +10,39 @@ import Foundation //@available(iOS 15, macOS 12.0, *) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) @available(*, deprecated, message: "Use direct SCChartPoint and SCChartRangePoint construction for the native chart wrappers instead.") +/// Deprecated helper namespace that converts primitive arrays into the legacy chart data models. public class SCManager { private init(){} + /// Returns the default sample data used by the deprecated legacy bar-chart API. public static func defaultBarChartData() -> [SCBarChartData] { return [3.1, 2.1, 3.1, 5.1, 9.9].map { SCBarChartData(rawValue: $0) } } + /// Returns the default sample data used by the deprecated legacy histogram API. public static func defaultHistogramData() -> [SCHistogramData] { return [3.1, 2.1, 3.1, 5.1, 9.9].map { SCHistogramData(rawValue: $0) } } + /// Returns the default sample data used by the deprecated legacy line-chart API. public static func defaultLineChartData() -> [SCLineChartData] { return [3.1, 2.1, 3.1, 5.1, 9.9].map { SCLineChartData(rawValue: $0) } } + /// Returns the default sample data used by the deprecated legacy quad-curve API. public static func defaultQuadCurveData() -> [SCQuadCurveData] { return [3.1, 2.1, 3.1, 5.1, 9.9].map { SCQuadCurveData(rawValue: $0) } } + /// Returns the default sample data used by the deprecated legacy range-chart API. public static func defaultRangeChartData() -> [SCRangeChartData] { return [(1.0, 3.1), (1.0, 2.1), (1.0, 3.1), (1.0, 5.1), (1.0, 9.9)].map { SCRangeChartData(rawLower: $0.0, rawUpper: $0.1) } } + /// Converts floating-point values into deprecated legacy line-chart data models. public static func getLineChartData(data: [Double]) -> [SCLineChartData]{ var chartData: [SCLineChartData] = [SCLineChartData]() for (_, value) in data.enumerated(){ @@ -44,6 +51,7 @@ public class SCManager { return chartData } + /// Converts integer values into deprecated legacy line-chart data models. public static func getLineChartData(data: [Int]) -> [SCLineChartData]{ var chartData: [SCLineChartData] = [SCLineChartData]() for (_, value) in data.enumerated(){ @@ -52,6 +60,7 @@ public class SCManager { return chartData } + /// Converts floating-point values into deprecated legacy bar-chart data models. public static func getBarChartData(data: [Double]) -> [SCBarChartData]{ var chartData: [SCBarChartData] = [SCBarChartData]() for (_, value) in data.enumerated(){ @@ -60,6 +69,7 @@ public class SCManager { return chartData } + /// Converts integer values into deprecated legacy bar-chart data models. public static func getBarChartData(data: [Int]) -> [SCBarChartData]{ var chartData: [SCBarChartData] = [SCBarChartData]() for (_, value) in data.enumerated(){ @@ -68,6 +78,7 @@ public class SCManager { return chartData } + /// Converts floating-point values into deprecated legacy histogram data models. public static func getHistogramData(data: [Double]) -> [SCHistogramData]{ var chartData: [SCHistogramData] = [SCHistogramData]() for (_, value) in data.enumerated(){ @@ -76,6 +87,7 @@ public class SCManager { return chartData } + /// Converts integer values into deprecated legacy histogram data models. public static func getHistogramData(data: [Int]) -> [SCHistogramData]{ var chartData: [SCHistogramData] = [SCHistogramData]() for (_, value) in data.enumerated(){ @@ -84,6 +96,7 @@ public class SCManager { return chartData } + /// Converts floating-point values into deprecated legacy quad-curve data models. public static func getQuadCurveData(data: [Double]) -> [SCQuadCurveData]{ var chartData: [SCQuadCurveData] = [SCQuadCurveData]() for (_, value) in data.enumerated(){ @@ -92,6 +105,7 @@ public class SCManager { return chartData } + /// Converts integer values into deprecated legacy quad-curve data models. public static func getQuadCurveData(data: [Int]) -> [SCQuadCurveData]{ var chartData: [SCQuadCurveData] = [SCQuadCurveData]() for (_, value) in data.enumerated(){ @@ -100,6 +114,7 @@ public class SCManager { return chartData } + /// Converts lower and upper floating-point bounds into deprecated legacy range-chart data models. public static func getRangeChartData(lower: [Double], upper: [Double]) -> [SCRangeChartData]? { if lower.count == upper.count { var returnedData = [SCRangeChartData]() @@ -119,6 +134,7 @@ public class SCManager { } } + /// Converts lower and upper integer bounds into deprecated legacy range-chart data models. public static func getRangeChartData(lower: [Int], upper: [Int]) -> [SCRangeChartData]? { if lower.count == upper.count { var returnedData = [SCRangeChartData]() @@ -138,6 +154,7 @@ public class SCManager { } } + /// Converts tuple-based floating-point ranges into deprecated legacy range-chart data models. public static func getRangeChartData(data: [(lower: Double, upper: Double)]) -> [SCRangeChartData] { var returnedData = [SCRangeChartData]() for i in 0.. [SCRangeChartData] { var returnedData = [SCRangeChartData]() for i in 0.. to pick the right wrapper family. +- Read when you need selection, hover, scroll, or zoom. +- Use the repository guides in `docs/` for longer walkthroughs and copy-paste examples. diff --git a/Sources/SimpleChart/SimpleChart.docc/InteractiveCharts.md b/Sources/SimpleChart/SimpleChart.docc/InteractiveCharts.md new file mode 100644 index 0000000..5c717c6 --- /dev/null +++ b/Sources/SimpleChart/SimpleChart.docc/InteractiveCharts.md @@ -0,0 +1,55 @@ +# Interactive Charts + +SimpleChart keeps interactive chart state in public helper types so you can drive selection, scrolling, and zoom from SwiftUI state instead of dropping down to raw Swift Charts APIs. + +## Selection and Inspection + +Use the selectable and hoverable wrappers when you want chart gestures without rebuilding the interaction layer yourself. + +The main pieces are: + +- ``SCChartSelectionState`` for externalized selection state +- ``SCChartInspectionOverlay`` for point-label, callout, crosshair, and inspector presentation +- ``SCChartGestureConfiguration`` for enabling or disabling selection, scrolling, and zooming + +Examples: + +- ``SCSelectableLineChart`` +- ``SCSelectableBarChart`` +- ``SCHoverableLineChart`` +- ``SCInspectorTimeSeriesChart`` + +## Scroll and Zoom + +Use visible-window helpers when the chart should be navigable over time or across a large indexed series. + +- ``SCChartScrollBehavior`` describes the default visible window. +- ``SCChartViewport`` stores an indexed x-domain window. +- ``SCChartTimeViewport`` stores a date-based x-domain window. +- ``SCChartZoomBehavior`` constrains how far the chart may zoom in or out. + +For simple scrolling only, a `Date` binding is enough: + +```swift +SCScrollableTimeSeriesChart( + points: history, + scrollPosition: $scrollPosition, + scrollBehavior: .timeWindow(hours: 24) +) +``` + +For viewport-driven zoom and programmatic navigation, bind the whole window: + +```swift +SCScrollableTimeSeriesChart( + points: history, + viewport: $viewport, + scrollBehavior: .timeWindow(hours: 24), + zoomBehavior: .init( + minimumVisibleLength: 60 * 60, + maximumVisibleLength: 60 * 60 * 24 * 7 + ) +) +``` + +The same mental model applies to ``SCScrollableLineChart`` for indexed data. diff --git a/Sources/SimpleChart/SimpleChart.docc/LegacyMigration.md b/Sources/SimpleChart/SimpleChart.docc/LegacyMigration.md new file mode 100644 index 0000000..d89a8f1 --- /dev/null +++ b/Sources/SimpleChart/SimpleChart.docc/LegacyMigration.md @@ -0,0 +1,25 @@ +# Migrating from the Legacy API + +The original `SCManager`, `SC*Config`, and `SC*Data` types are still present for compatibility, but they are deprecated and bridged onto the native Swift Charts-first wrapper layer. + +## Migration Direction + +The preferred path is: + +- `SCLineChart` -> ``SCNativeLineChart`` +- `SCBarChart` -> ``SCNativeBarChart`` +- `SCHistogram` -> ``SCNativeHistogramChart`` +- `SCQuadCurve` -> ``SCNativeQuadCurveChart`` +- `SCRangeChart` -> ``SCNativeRangeChart`` + +## Why Migrate + +The native wrapper layer gives you: + +- clearer helper models such as ``SCChartPoint`` and ``SCChartDomain`` +- stronger interaction support through ``SCChartSelectionState`` and scroll/zoom helpers +- better Xcode symbol docs and DocC organization + +## Keep the Migration Incremental + +Because the legacy wrappers bridge onto the newer types, you can migrate chart-by-chart instead of rewriting the whole package integration at once. diff --git a/Sources/SimpleChart/SimpleChart.docc/SimpleChart.md b/Sources/SimpleChart/SimpleChart.docc/SimpleChart.md new file mode 100644 index 0000000..c5ab053 --- /dev/null +++ b/Sources/SimpleChart/SimpleChart.docc/SimpleChart.md @@ -0,0 +1,69 @@ +# ``SimpleChart`` + +Native Swift Charts-first wrappers and helper types for building bar, line, range, time-series, composed, and interactive charts with a small, reusable API surface. + +## Overview + +SimpleChart provides ready-made wrappers on top of Swift Charts while also exposing the helper models and interaction state used by those wrappers internally. + +Use the package when you want: + +- fast wrapper-style chart construction for common chart families +- a helper-first API for points, domains, axes, overlays, and interaction state +- a migration path from the original `SCManager` / `SC*Config` legacy layer + +The package is organized around a few core concepts: + +- ``SCChartPoint`` and related point models for data input +- ``SCChartSeriesStyle`` and ``SCChartAxesStyle`` for visual configuration +- ``SCChartDomain`` and visible-window helpers such as ``SCChartViewport`` and ``SCChartTimeViewport`` +- wrapper families such as ``SCNativeLineChart``, ``SCNativeBarChart``, and ``SCScrollableTimeSeriesChart`` + +## Start Here + +- +- +- +- + +## Topics + +### Essentials + +- +- ``SCChartPoint`` +- ``SCChartSeriesStyle`` +- ``SCChartAxesStyle`` +- ``SCChartDomain`` + +### Wrapper Families + +- +- ``SCNativeLineChart`` +- ``SCNativeBarChart`` +- ``SCNativeRangeChart`` +- ``SCNativeTimeSeriesChart`` +- ``SCComposedChart`` + +### Interactive Charts + +- +- ``SCChartSelectionState`` +- ``SCChartInspectionOverlay`` +- ``SCChartScrollBehavior`` +- ``SCChartZoomBehavior`` +- ``SCChartGestureConfiguration`` +- ``SCChartViewport`` +- ``SCChartTimeViewport`` +- ``SCScrollableLineChart`` +- ``SCScrollableTimeSeriesChart`` + +### Migration + +- +- ``SCManager`` +- ``SCLineChart`` +- ``SCBarChart`` +- ``SCHistogram`` +- ``SCQuadCurve`` +- ``SCRangeChart`` diff --git a/Sources/SimpleChart/SimpleChart.docc/WrapperChooser.md b/Sources/SimpleChart/SimpleChart.docc/WrapperChooser.md new file mode 100644 index 0000000..9363060 --- /dev/null +++ b/Sources/SimpleChart/SimpleChart.docc/WrapperChooser.md @@ -0,0 +1,25 @@ +# Choosing a Wrapper + +Choose a ready-made wrapper first. Reach for the helper layer directly only when you are composing multiple mark types or sharing configuration across many charts. + +## Common Cases + +- Use ``SCNativeLineChart`` for one categorical line or area series. +- Use ``SCNativeBarChart`` for single-series category comparisons. +- Use ``SCNativeRangeChart`` when each x-position has lower and upper bounds. +- Use ``SCNativeTimeSeriesChart`` for date-based lines without interaction. +- Use ``SCComposedChart`` when you need mixed marks, overlays, or reusable compositions. + +## Interactive Variants + +- Use ``SCSelectableLineChart`` / ``SCSelectableBarChart`` / ``SCSelectableScatterChart`` for point selection. +- Use ``SCHoverableLineChart`` / ``SCHoverableBarChart`` / ``SCHoverableScatterChart`` for pointer-driven inspection. +- Use ``SCScrollableLineChart`` and ``SCScrollableTimeSeriesChart`` for navigable x-domain windows. + +## When to Use the Helper Layer + +Use the helper types directly when you want to: + +- reuse marks and overlays through ``SCChartComposition`` +- manage visible windows through ``SCChartViewport`` or ``SCChartTimeViewport`` +- share styling and scale logic via ``SCChartSeriesStyle``, ``SCChartAxesStyle``, and ``SCChartScale`` diff --git a/Tests/SimpleChartTests/SCChartZoomNavigationTests.swift b/Tests/SimpleChartTests/SCChartZoomNavigationTests.swift new file mode 100644 index 0000000..6b8c528 --- /dev/null +++ b/Tests/SimpleChartTests/SCChartZoomNavigationTests.swift @@ -0,0 +1,161 @@ +import XCTest +import SwiftUI +@testable import SimpleChart + +final class SCChartZoomNavigationTests: XCTestCase { + func testTimeViewportLengthShiftClampAndZoomHelpersPreserveExpectedWindow() { + let start = Date(timeIntervalSince1970: 100) + let viewport = SCChartTimeViewport.starting(at: start, duration: 60) + let shifted = viewport.shifted(to: Date(timeIntervalSince1970: 150)) + let clamped = SCChartTimeViewport( + startDate: Date(timeIntervalSince1970: 180), + endDate: Date(timeIntervalSince1970: 300) + ) + .clamped(to: Date(timeIntervalSince1970: 120)...Date(timeIntervalSince1970: 240)) + let zoomed = viewport.zoomed( + by: 2, + centeredAt: Date(timeIntervalSince1970: 130), + within: Date(timeIntervalSince1970: 90)...Date(timeIntervalSince1970: 240) + ) + + XCTAssertEqual(viewport.length, 60, accuracy: 0.0001) + XCTAssertEqual(shifted.startDate.timeIntervalSince1970, 150, accuracy: 0.0001) + XCTAssertEqual(shifted.endDate.timeIntervalSince1970, 210, accuracy: 0.0001) + XCTAssertEqual(clamped.startDate.timeIntervalSince1970, 120, accuracy: 0.0001) + XCTAssertEqual(clamped.endDate.timeIntervalSince1970, 240, accuracy: 0.0001) + XCTAssertEqual(zoomed.startDate.timeIntervalSince1970, 115, accuracy: 0.0001) + XCTAssertEqual(zoomed.endDate.timeIntervalSince1970, 145, accuracy: 0.0001) + } + + func testZoomBehaviorAndGestureConfigurationStoreZoomSupport() { + let zoomBehavior = SCChartZoomBehavior( + minimumVisibleLength: 2, + maximumVisibleLength: 12, + sensitivity: 0.5 + ) + let gestures = SCChartGestureConfiguration( + allowsSelection: true, + allowsScrolling: true, + allowsZooming: true + ) + + XCTAssertTrue(zoomBehavior.isEnabled) + XCTAssertEqual(zoomBehavior.minimumVisibleLength, 2) + XCTAssertEqual(zoomBehavior.maximumVisibleLength, 12) + XCTAssertEqual(zoomBehavior.sensitivity, 0.5) + XCTAssertTrue(gestures.allowsZooming) + XCTAssertTrue(SCChartGestureConfiguration.interactive.allowsZooming) + XCTAssertTrue(SCChartGestureConfiguration.scrollOnly.allowsZooming) + XCTAssertFalse(SCChartGestureConfiguration.selectionOnly.allowsZooming) + } + + func testNavigationCoordinatorClampsIndexedViewportWithinBoundsAndZoomLimits() { + let viewport = SCChartViewport.starting(at: 8, length: 10) + let clamped = SCChartNavigationCoordinator.clampedViewport( + viewport, + zoomBehavior: SCChartZoomBehavior(minimumVisibleLength: 3, maximumVisibleLength: 6), + bounds: 0...12 + ) + + XCTAssertEqual(clamped.lowerBound, 6, accuracy: 0.0001) + XCTAssertEqual(clamped.upperBound, 12, accuracy: 0.0001) + XCTAssertEqual(clamped.length, 6, accuracy: 0.0001) + } + + func testNavigationCoordinatorClampsTimeViewportWithinBoundsAndZoomLimits() { + let viewport = SCChartTimeViewport( + startDate: Date(timeIntervalSince1970: 180), + endDate: Date(timeIntervalSince1970: 320) + ) + let clamped = SCChartNavigationCoordinator.clampedViewport( + viewport, + zoomBehavior: SCChartZoomBehavior(minimumVisibleLength: 60, maximumVisibleLength: 120), + bounds: Date(timeIntervalSince1970: 100)...Date(timeIntervalSince1970: 240) + ) + + XCTAssertEqual(clamped.startDate.timeIntervalSince1970, 120, accuracy: 0.0001) + XCTAssertEqual(clamped.endDate.timeIntervalSince1970, 240, accuracy: 0.0001) + XCTAssertEqual(clamped.length, 120, accuracy: 0.0001) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) + func testScrollableWrappersStoreZoomBehaviorAndViewportBindings() { + var indexedViewport = SCChartViewport.starting(at: 0, length: 6) + let indexedBinding = Binding(get: { indexedViewport }, set: { indexedViewport = $0 }) + let indexedChart = SCScrollableLineChart( + points: SCChartPoint.make(labeledValues: [("A", 2), ("B", 4), ("C", 6), ("D", 8)]), + viewport: indexedBinding, + scrollBehavior: .continuous(.points(6)), + zoomBehavior: SCChartZoomBehavior(minimumVisibleLength: 2, maximumVisibleLength: 6), + gestureConfiguration: .interactive + ) + + var timeViewport = SCChartTimeViewport.starting( + at: Date(timeIntervalSince1970: 100), + duration: 3600 + ) + let timeBinding = Binding(get: { timeViewport }, set: { timeViewport = $0 }) + let timeChart = SCScrollableTimeSeriesChart( + points: SCChartTimePoint.make(values: [ + (Date(timeIntervalSince1970: 100), 2), + (Date(timeIntervalSince1970: 200), 4), + (Date(timeIntervalSince1970: 300), 6) + ]), + viewport: timeBinding, + scrollBehavior: .timeWindow(seconds: 3600), + zoomBehavior: SCChartZoomBehavior(minimumVisibleLength: 900, maximumVisibleLength: 7200), + gestureConfiguration: .scrollOnly + ) + + XCTAssertEqual(indexedChart.zoomBehavior.minimumVisibleLength, 2) + XCTAssertEqual(indexedChart.zoomBehavior.maximumVisibleLength, 6) + XCTAssertTrue(indexedChart.gestureConfiguration.allowsZooming) + XCTAssertEqual(timeChart.zoomBehavior.minimumVisibleLength, 900) + XCTAssertEqual(timeChart.zoomBehavior.maximumVisibleLength, 7200) + XCTAssertTrue(timeChart.gestureConfiguration.allowsZooming) + XCTAssertEqual(indexedViewport.length, 6, accuracy: 0.0001) + XCTAssertEqual(timeViewport.length, 3600, accuracy: 0.0001) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) + func testScrollableLineChartInitiallyRendersConfiguredVisibleDomainBeforeViewportTakesOver() { + var viewport = SCChartViewport.starting(at: 1, length: 3) + let binding = Binding(get: { viewport }, set: { viewport = $0 }) + let chart = SCScrollableLineChart( + points: SCChartPoint.make(labeledValues: [ + ("A", 2), + ("B", 4), + ("C", 6), + ("D", 8), + ("E", 10), + ("F", 12) + ]), + viewport: binding, + visibleDomain: .points(6), + zoomBehavior: .standard, + gestureConfiguration: .interactive + ) + + XCTAssertEqual(chart.visibleDomain.length, 6, accuracy: 0.0001) + XCTAssertEqual(chart.renderedVisibleDomain.length, 6, accuracy: 0.0001) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, macCatalyst 17, *) + func testScrollableTimeSeriesChartLegacyScrollPositionPreservesConfiguredWindowLength() { + var scrollPosition = Date(timeIntervalSince1970: 100) + let binding = Binding(get: { scrollPosition }, set: { scrollPosition = $0 }) + let chart = SCScrollableTimeSeriesChart( + points: SCChartTimePoint.make(values: [ + (Date(timeIntervalSince1970: 100), 2), + (Date(timeIntervalSince1970: 130), 4), + (Date(timeIntervalSince1970: 160), 6) + ]), + scrollPosition: binding, + visibleDomain: .seconds(300), + gestureConfiguration: .scrollOnly + ) + + XCTAssertEqual(chart.visibleDomain.length, 300, accuracy: 0.0001) + XCTAssertEqual(chart.renderedVisibleDomain.length, 300, accuracy: 0.0001) + } +} diff --git a/Tests/SimpleChartTests/SCNativeInteractionAndTimeSeriesTests.swift b/Tests/SimpleChartTests/SCNativeInteractionAndTimeSeriesTests.swift index c14bada..1537574 100644 --- a/Tests/SimpleChartTests/SCNativeInteractionAndTimeSeriesTests.swift +++ b/Tests/SimpleChartTests/SCNativeInteractionAndTimeSeriesTests.swift @@ -48,6 +48,27 @@ final class SCNativeInteractionAndTimeSeriesTests: XCTestCase { XCTAssertFalse(gestures.allowsScrolling) } + func testGestureConfigurationDecodesLegacyPayloadByInferringZoomFromScrolling() throws { + let scrollingPayload = """ + {"allowsSelection":true,"allowsScrolling":true} + """.data(using: .utf8)! + let selectionOnlyPayload = """ + {"allowsSelection":true,"allowsScrolling":false} + """.data(using: .utf8)! + + let scrollingConfig = try JSONDecoder().decode( + SCChartGestureConfiguration.self, + from: scrollingPayload + ) + let selectionOnlyConfig = try JSONDecoder().decode( + SCChartGestureConfiguration.self, + from: selectionOnlyPayload + ) + + XCTAssertTrue(scrollingConfig.allowsZooming) + XCTAssertFalse(selectionOnlyConfig.allowsZooming) + } + func testNumericAndDateFormatsProduceStableText() { XCTAssertFalse(SCChartNumericValueFormat.compact.string(from: 12_400).isEmpty) XCTAssertTrue(SCChartNumericValueFormat.currency(code: "USD").string(from: 42).contains("$")) diff --git a/Tests/SimpleChartTests/SCNativePlotAnd3DChartTests.swift b/Tests/SimpleChartTests/SCNativePlotAnd3DChartTests.swift index bbcf597..d6efb16 100644 --- a/Tests/SimpleChartTests/SCNativePlotAnd3DChartTests.swift +++ b/Tests/SimpleChartTests/SCNativePlotAnd3DChartTests.swift @@ -159,6 +159,7 @@ final class SCNativePlotAnd3DChartTests: XCTestCase { XCTAssertEqual(bandAreaChart.bandFunction?(2).yEnd, 4) } + #if compiler(>=6.3) @available(iOS 26.0, macOS 26.0, visionOS 26.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) @@ -201,4 +202,5 @@ final class SCNativePlotAnd3DChartTests: XCTestCase { XCTAssertEqual(surfaceChart.pose, pose) XCTAssertEqual(surfaceChart.function(2, 3), 5) } + #endif } diff --git a/docs/chart-selection-guide.md b/docs/chart-selection-guide.md index f3b1517..0efb925 100644 --- a/docs/chart-selection-guide.md +++ b/docs/chart-selection-guide.md @@ -89,8 +89,8 @@ These wrappers are availability-gated to the newer OS levels supported by Swift | Hover to inspect line values | `SCHoverableLineChart` | | Hover to inspect bar values | `SCHoverableBarChart` | | Hover to inspect scatter values | `SCHoverableScatterChart` | -| Scroll an indexed line chart | `SCScrollableLineChart` | -| Scroll a time-series chart | `SCScrollableTimeSeriesChart` | +| Scroll or zoom an indexed line chart | `SCScrollableLineChart` | +| Scroll or zoom a time-series chart | `SCScrollableTimeSeriesChart` | Shared helper types: @@ -98,8 +98,10 @@ Shared helper types: - `SCChartHoverState` - `SCChartInspectionOverlay` - `SCChartScrollBehavior` +- `SCChartZoomBehavior` - `SCChartGestureConfiguration` - `SCChartViewport` +- `SCChartTimeViewport` ## When to Use `SCComposedChart` diff --git a/docs/editor-support.md b/docs/editor-support.md new file mode 100644 index 0000000..c79ac4b --- /dev/null +++ b/docs/editor-support.md @@ -0,0 +1,27 @@ +# Editor Support + +SimpleChart is a standard Swift Package Manager package, so Xcode and `sourcekit-lsp` can work directly from the package manifest without extra project generation. + +## Xcode + +- Open the package directory directly in Xcode, or add the package to an app target and jump into the package sources from there. +- Use Option-click or Quick Help on `SimpleChart` symbols to read the package documentation comments. +- Use `Product > Build Documentation` to render the DocC catalog locally once the package is open in Xcode. + +## sourcekit-lsp + +`sourcekit-lsp` should work out of the box because the repo is SwiftPM-native. + +Recommended setup: + +1. Run `swift build` once from the repository root so the package graph and build artifacts are warmed up. +2. Open the repo root in your editor. +3. Let the editor start `sourcekit-lsp` against the package manifest. + +You generally do not need a custom `.sourcekit-lsp` configuration file for this package. + +## Troubleshooting + +- If symbol navigation looks stale, rerun `swift build`. +- If Quick Help or completion is incomplete, make sure the editor opened the repo root that contains `Package.swift`. +- If you are switching Xcode toolchains, restart the editor so `sourcekit-lsp` picks up the active Swift toolchain cleanly. diff --git a/docs/getting-started.md b/docs/getting-started.md index d7c0eb3..0a2f3eb 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -6,6 +6,8 @@ If you already know which chart you need, use the [Chart Selection Guide](chart- If you prefer a guided sequence instead of a reference-style guide, use the [Tutorials](tutorials/README.md). +If you want IDE-native docs, open the package in Xcode and use the DocC catalog at [Sources/SimpleChart/SimpleChart.docc/SimpleChart.md](../Sources/SimpleChart/SimpleChart.docc/SimpleChart.md). For editor setup details, use the [Editor Support guide](editor-support.md). + ## Requirements SimpleChart uses the first-party Swift Charts baselines: @@ -32,6 +34,7 @@ Some interaction wrappers require newer OS versions: 2. Go to `Package Dependencies`. 3. Add `https://github.com/ImpostersLimited/SimpleChart.git`. 4. Link the `SimpleChart` product to your target. +5. Option-click `SimpleChart` symbols for Quick Help, or use `Product > Build Documentation` after opening the package in Xcode to render the DocC catalog locally. ### Package.swift @@ -206,7 +209,8 @@ struct ScrollableExample: View { SCScrollableLineChart( points: points, viewport: $viewport, - scrollBehavior: .continuous(.points(4)) + scrollBehavior: .continuous(.points(4)), + zoomBehavior: .init(minimumVisibleLength: 2, maximumVisibleLength: 7) ) } } @@ -233,4 +237,5 @@ For a fuller chooser, use the [Chart Selection Guide](chart-selection-guide.md). - [Tutorials](tutorials/README.md) - [Chart Selection Guide](chart-selection-guide.md) +- [Editor Support](editor-support.md) - [README](../README.md) for the full API surface and migration notes diff --git a/docs/tutorials/04-time-series-and-scrolling.md b/docs/tutorials/04-time-series-and-scrolling.md index 5528c98..458a892 100644 --- a/docs/tutorials/04-time-series-and-scrolling.md +++ b/docs/tutorials/04-time-series-and-scrolling.md @@ -43,12 +43,15 @@ struct SelectableTimeSeriesExample: View { } ``` -## Step 4: Add Scrolling +## Step 4: Add Scrolling and Zoom ```swift @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) struct ScrollableTimeSeriesExample: View { - @State private var viewport = SCChartViewport.starting(at: 0, length: 14) + @State private var viewport = SCChartTimeViewport.starting( + at: Date(timeIntervalSince1970: 1_700_000_000), + duration: 60 * 60 * 24 * 14 + ) let points: [SCChartTimePoint] @@ -56,8 +59,11 @@ struct ScrollableTimeSeriesExample: View { SCScrollableTimeSeriesChart( points: points, viewport: $viewport, - visibleDomain: .analytics(points: 14), scrollBehavior: .analytics(points: 21), + zoomBehavior: .init( + minimumVisibleLength: 60 * 60 * 6, + maximumVisibleLength: 60 * 60 * 24 * 21 + ), xAxisFormat: .monthDay ) } @@ -72,6 +78,14 @@ let financeWindow = SCChartVisibleDomain.finance(tradingDays: 5) let oneDay = SCChartScrollBehavior.timeWindow(days: 1) let tradingWeek = SCChartScrollBehavior.finance(tradingDays: 5) +let timeViewport = SCChartTimeViewport.starting( + at: Date(timeIntervalSince1970: 1_700_000_000), + duration: 60 * 60 * 24 * 14 +) +let zoomBehavior = SCChartZoomBehavior( + minimumVisibleLength: 60 * 60 * 6, + maximumVisibleLength: 60 * 60 * 24 * 21 +) ``` ## Next Step diff --git a/tasks/lessons.md b/tasks/lessons.md index e9bd0b9..d3b23ed 100644 --- a/tasks/lessons.md +++ b/tasks/lessons.md @@ -1,3 +1,6 @@ # Active Lessons - Use the dedicated `apply_patch` tool for manual file edits. Do not route patch application through `exec_command`, even if the shell patch succeeds. +- When refactoring wrapper render state, preserve initializer-driven compatibility contracts separately from the internally clamped viewport state; legacy wrappers often promise more than the render helper should enforce. +- When adding stored properties to public `Codable` models, do not rely on synthesis if older payloads may omit the new key; add an explicit compatibility decoder and test it. +- When availability-gating source behind a compiler-version check, mirror that guard in test files too; docbuild and older CI toolchains will still compile tests. diff --git a/tasks/todo.md b/tasks/todo.md index 6da618e..07b223b 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -1,5 +1,35 @@ # Active Todo +- [x] Fix the docbuild CI regression by matching the 3D test coverage to the source-side compiler guards +- [x] Re-run local docbuild verification, preferring Xcode 16.4 to match GitHub Actions when available +- [x] Re-run full package tests after the docbuild fix + +- [x] Fix the review follow-ups for 3D wrapper compiler guards and gesture-configuration Codable compatibility +- [x] Add regression coverage for decoding older `SCChartGestureConfiguration` payloads +- [x] Re-run targeted interaction tests plus full package verification + +- [x] Fix the scrollable wrapper review regressions so initializer-driven visible windows remain compatible for indexed and legacy time-series charts +- [x] Add focused regression tests covering the line-chart initial visible-domain contract and the legacy time-series scrollPosition window-length contract +- [x] Re-run targeted zoom/navigation tests plus full `swift test` + +## Review Notes + +- Fixed the docbuild CI regression by moving the 3D test coverage behind the same `#if compiler(>=6.3)` gate as the source wrappers. +- Verified on 2026-04-15 with local `xcodebuild docbuild` and full package tests after the CI-specific fix. +- Fixed the 3D wrapper compiler-guard regression so every `SCComposedChart3D`-dependent wrapper stays behind the Swift 6.3+ gate. +- Added backward-compatible decoding for `SCChartGestureConfiguration` so older payloads infer `allowsZooming` from `allowsScrolling` when that key is absent. +- Added focused regression coverage for legacy gesture-configuration decoding. +- Verified on 2026-04-15 with `swift test --filter SCNativeInteractionAndTimeSeriesTests` and full `swift test`: 76 tests passed after the compatibility fixes. +- Restored the indexed wrapper's initial visible-window precedence so `scrollBehavior` / `visibleDomain` still control first render until viewport-driven navigation takes over. +- Restored the legacy time-series `scrollPosition` path so it preserves the configured window length instead of shrinking wide domains to the data bounds. +- Added focused regression coverage in `SCChartZoomNavigationTests` for both review comments. +- Verified on 2026-04-15 with `swift test --filter SCChartZoomNavigationTests` and full `swift test`: 75 tests passed. + +- [x] Audit the remaining exposed public API surface for missing Xcode Quick Help coverage +- [x] Add `///` documentation for all remaining public types and public entry points in the package +- [x] Update changelog and review notes for the full API Quick Help pass +- [x] Re-run `swift build` to verify the documentation pass did not break the package + - [x] Audit the first-discovery public API for missing Xcode Quick Help summaries - [x] Add focused `///` documentation to the core models, styles, composition entrypoints, and primary wrappers - [x] Update changelog/review notes for the Quick Help documentation pass @@ -107,6 +137,21 @@ - [x] Add a compact chart-selection guide for choosing the right wrapper - [x] Verify the updated documentation flow and update review notes +- [x] Audit the current scrollable wrapper and interaction helper surface for the zoom/navigation extension points +- [x] Add failing tests for indexed and time-series zoom/navigation state, coordinator behavior, and wrapper configuration +- [x] Add a public navigation-state layer for zoomable indexed and time-series windows +- [x] Add a shared interaction coordinator that translates binding and gesture intent into clamped navigation updates +- [x] Extend the existing scrollable wrappers to adopt the new zoom/navigation API without regressing current scroll behavior +- [x] Update README, changelog, and review notes for the zoomable interactive wrapper slice +- [x] Re-run focused and full package verification for the zoom/navigation slice + +- [x] Audit the current IDE-native documentation/tooling gap across DocC, Quick Help, and editor support +- [x] Add a `SimpleChart.docc` catalog with a landing page and focused articles for wrapper discovery and interactive charts +- [x] Polish high-value Quick Help on the interaction/navigation surface so symbol docs are more teachable in Xcode +- [x] Add a contributor-facing editor support note for Xcode and sourcekit-lsp usage +- [x] Add a GitHub Actions workflow that verifies the documentation build path +- [x] Re-run package verification, update review notes, and sync the tracking issue for the docs/tooling slice + ## Notes - Native Swift Charts is only available on iOS 16, macOS 13, tvOS 16, and watchOS 9, so the new wrapper must be additive and availability-gated. @@ -185,3 +230,15 @@ - Verified on 2026-04-10 after the helper-kernel completeness slice with `swift test`: package builds cleanly and 65 tests pass. - Added Xcode Quick Help summaries to the first-discovery public API surface, covering the core point/range/time/reference models, domain/style helpers, composed-chart entrypoints, and the main line/bar/inspection wrappers. - Verified on 2026-04-11 after the Quick Help documentation pass with `swift build`: package builds cleanly. +- Expanded the Xcode Quick Help pass to cover the full exposed package API surface, including the remaining native wrappers/helpers, shared interaction and viewport properties, `SCChartDomain` overloads, and the deprecated legacy compatibility layer. +- The symbol-level audit for public types, initializers, helper functions, and exposed computed properties now returns zero undocumented symbols across `Sources/SimpleChart` when preview-only declarations are excluded. +- Verified on 2026-04-11 after the full-package Quick Help pass with `swift build` and `swift test`: package builds cleanly and 68 tests pass. +- Added `SCChartTimeViewport`, `SCChartZoomBehavior`, and `SCChartNavigationCoordinator` so indexed and date-based windows share a stable zoom/navigation model. +- Extended `SCScrollableLineChart` and `SCScrollableTimeSeriesChart` with zoom-aware coordination while preserving the pre-existing public `visibleDomain` wrapper state contract. +- Added focused zoom/navigation coverage in `SCChartZoomNavigationTests.swift` and updated the docs/tutorial surface to show viewport-driven zoom usage instead of raw Swift Charts fallback patterns. +- Verified on 2026-04-15 for the zoomable interactive wrapper slice with `swift test --filter SCChartZoomNavigationTests`, `swift test --filter SCNativeInteractionAndTimeSeriesTests`, and full `swift test`: package builds cleanly and 73 tests pass. +- Added a first-party `SimpleChart.docc` catalog with landing and tutorial-style articles for getting started, wrapper selection, interactive charts, and legacy migration. +- Added `docs/editor-support.md` so contributors have one place to look for Xcode Quick Help, local DocC rendering, and `sourcekit-lsp` expectations. +- Added richer Quick Help coverage for the zoom/navigation surface, including `SCChartTimeViewport`, `SCChartScrollBehavior`, `SCChartZoomBehavior`, `SCChartGestureConfiguration`, and the scrollable interactive wrappers. +- Added a GitHub Actions `Documentation` workflow that runs `xcodebuild docbuild` against the SwiftPM scheme on macOS. +- Verified on 2026-04-15 for the docs/tooling slice with `swift build`, `swift test`, and `xcodebuild docbuild -scheme SimpleChart -destination 'platform=macOS' -derivedDataPath .build/DerivedData CODE_SIGNING_ALLOWED=NO`: package builds cleanly, all 73 tests pass, and the DocC build succeeds.