@@ -3,9 +3,13 @@ import Charts
33
44struct BarChartView : View {
55 let data : ChartData
6+ var onDateSelected : ( ( Date ) -> Void ) ? = nil
67
7- @State private var selectedDate : Date ?
88 @State private var selectedDataPoints : SelectedDataPoints ?
9+ @State private var isDragging = false
10+ @State private var isLongPressing = false
11+ @State private var longPressLocation : CGPoint ?
12+ @State private var tappedDataPoint : DataPoint ?
913
1014 @Environment ( \. context) var context
1115
@@ -31,15 +35,17 @@ struct BarChartView: View {
3135 . chartYScale ( domain: yAxisDomain)
3236 . chartLegend ( . hidden)
3337 . environment ( \. timeZone, context. timeZone)
34- . modifier ( ChartSelectionModifier ( selection: $selectedDate) )
3538 . animation ( . spring, value: ObjectIdentifier ( data) )
36- . onChange ( of : selectedDate ) {
37- selectedDataPoints = SelectedDataPoints . compute ( for : $0 , data : data )
39+ . chartOverlay { proxy in
40+ makeGesturesOverlayView ( proxy : proxy )
3841 }
3942 . dynamicTypeSize ( ... DynamicTypeSize . xxxLarge)
4043 . accessibilityElement ( )
4144 . accessibilityLabel ( Strings . Accessibility. chartContainer)
4245 . accessibilityHint ( Strings . Accessibility. viewChartData)
46+ . onChange ( of: ObjectIdentifier ( data) ) { _ in
47+ tappedDataPoint = nil
48+ }
4349 }
4450
4551 // MARK: - Chart Marks
@@ -48,7 +54,7 @@ struct BarChartView: View {
4854 private var currentPeriodBars : some ChartContent {
4955 ForEach ( data. currentData) { point in
5056 BarMark (
51- x: . value( " Date " , point. date, unit: data. granularity. component) ,
57+ x: . value( " Date " , point. date, unit: data. granularity. component, calendar : context . calendar ) ,
5258 y: . value( " Value " , point. value) ,
5359 width: . automatic
5460 )
@@ -68,7 +74,15 @@ struct BarChartView: View {
6874 }
6975
7076 private func getOpacityForCurrentPeriodBar( for point: DataPoint ) -> CGFloat {
77+ if let tappedDataPoint, tappedDataPoint. id == point. id {
78+ return 1.0
79+ }
7180 guard let selectedDataPoints else {
81+ // If there's a tapped point, dim other bars
82+ if tappedDataPoint != nil {
83+ return 0.5
84+ }
85+ // If no selection and not tapped, check if data is incomplete
7286 let isIncomplete = context. calendar. isIncompleteDataPeriod ( for: point. date, granularity: data. granularity)
7387 return isIncomplete ? 0.5 : 1
7488 }
@@ -79,7 +93,7 @@ struct BarChartView: View {
7993 private var previousPeriodBars : some ChartContent {
8094 ForEach ( data. mappedPreviousData) { point in
8195 BarMark (
82- x: . value( " Date " , point. date, unit: data. granularity. component) ,
96+ x: . value( " Date " , point. date, unit: data. granularity. component, calendar : context . calendar ) ,
8397 y: . value( " Value " , point. value) ,
8498 width: . automatic,
8599 stacking: . unstacked
@@ -113,7 +127,7 @@ struct BarChartView: View {
113127 private var significantPointAnnotations : some ChartContent {
114128 if let maxPoint = data. significantPoints. currentMax, data. currentData. count > 0 {
115129 PointMark (
116- x: . value( " Date " , maxPoint. date, unit: data. granularity. component) ,
130+ x: . value( " Date " , maxPoint. date, unit: data. granularity. component, calendar : context . calendar ) ,
117131 y: . value( " Value " , maxPoint. value)
118132 )
119133 . opacity ( 0 )
@@ -123,7 +137,7 @@ struct BarChartView: View {
123137 metric: data. metric
124138 )
125139 // Important for drag selection to work correctly.
126- . opacity ( selectedDate == nil ? 1 : 0 )
140+ . opacity ( selectedDataPoints == nil ? 1 : 0 )
127141 }
128142 }
129143 }
@@ -174,6 +188,7 @@ struct BarChartView: View {
174188 if let date = value. as ( Date . self) {
175189 AxisValueLabel {
176190 ChartAxisDateLabel ( date: date, granularity: data. granularity)
191+ . offset ( x: - 2 ) // Align it better with bars
177192 }
178193 }
179194 }
@@ -220,6 +235,100 @@ struct BarChartView: View {
220235 )
221236 }
222237 }
238+
239+ // MARK: - Gestures
240+
241+ private func makeGesturesOverlayView( proxy: ChartProxy ) -> some View {
242+ GeometryReader { geometry in
243+ Rectangle ( )
244+ . fill ( . clear)
245+ . contentShape ( Rectangle ( ) )
246+ . onTapGesture { location in
247+ handleTapGesture ( at: location, proxy: proxy, geometry: geometry)
248+ }
249+ . onLongPressGesture ( minimumDuration: 0.3 ) {
250+ // Long press completed - keep showing annotation
251+ } onPressingChanged: { isPressing in
252+ if isPressing {
253+ // Long press started - show annotation at current location
254+ if let location = longPressLocation {
255+ isLongPressing = true
256+ selectedDataPoints = getSelectedDataPoints ( at: location, proxy: proxy, geometry: geometry)
257+ }
258+ } else {
259+ // Long press ended - clear annotation
260+ isLongPressing = false
261+ longPressLocation = nil
262+ if !isDragging {
263+ selectedDataPoints = nil
264+ }
265+ tappedDataPoint = nil
266+ }
267+ }
268+ . simultaneousGesture (
269+ DragGesture ( minimumDistance: 0 )
270+ . onChanged { value in
271+ // Store location for long press
272+ longPressLocation = value. location
273+
274+ // Handle drag if moved enough
275+ if value. translation. width. magnitude > 8 || value. translation. height. magnitude > 8 {
276+ isDragging = true
277+ selectedDataPoints = getSelectedDataPoints ( at: value. location, proxy: proxy, geometry: geometry)
278+ }
279+ }
280+ . onEnded { _ in
281+ isDragging = false
282+ longPressLocation = nil
283+ if !isLongPressing {
284+ selectedDataPoints = nil
285+ }
286+ tappedDataPoint = nil
287+ }
288+ )
289+ }
290+ }
291+
292+ private func handleTapGesture( at location: CGPoint , proxy: ChartProxy , geometry: GeometryProxy ) {
293+ // Only handle tap if not dragging or long pressing
294+ guard !isDragging && !isLongPressing else { return }
295+
296+ guard let onDateSelected,
297+ data. granularity != . hour,
298+ let selection = getSelectedDataPoints ( at: location, proxy: proxy, geometry: geometry) ,
299+ selection. current? . value != 0 || selection. previous? . value != 0 else {
300+ // Clear selection if tapping on empty area
301+ tappedDataPoint = nil
302+ return
303+ }
304+ tappedDataPoint = selection. current ?? selection. previous
305+ if let date = tappedDataPoint? . date {
306+ onDateSelected ( date)
307+ }
308+ }
309+
310+ private func getSelectedDataPoints( at location: CGPoint , proxy: ChartProxy , geometry: GeometryProxy ) -> SelectedDataPoints ? {
311+ let origin = geometry [ proxy. plotAreaFrame] . origin
312+ let location = CGPoint (
313+ x: location. x - origin. x,
314+ y: location. y - origin. y
315+ )
316+ guard let date: Date = proxy. value ( atX: location. x) else {
317+ return nil
318+ }
319+ // Calling `proxy.value(atX: location.x)` returns dates with a second
320+ // precision. But the data points are represented using the start of the
321+ // period. The chart needs to offset the selection so that
322+ // `SelectedDataPoints.compute` correctly finds the closest one.
323+ let interval : TimeInterval = {
324+ let now = Date ( )
325+ let interval = context. calendar. date ( byAdding: data. granularity. component, value: 1 , to: now)
326+ return ( interval ?? now) . timeIntervalSince ( now)
327+ } ( )
328+
329+ let offsetDate = date. addingTimeInterval ( - ( interval / 4 ) )
330+ return SelectedDataPoints . compute ( for: offsetDate, data: data)
331+ }
223332}
224333
225334// MARK: - Preview
0 commit comments