I'm currently building an app in SwiftUI that needs to show some charts. Swift Charts has been quite helpful but I can't seem to set the domain of the chart just how I want it. The following chart display events that happend in a week and I want to display every hour even if nothing happened there, and I want the hours to go up, instead of the default where the new day start at the top of the chart.
struct ChartView: View {
let dataSets: [someData]
// simplified date init
let datemin = date(year:2025, month: 2, day: 24)
let datemax = date(year:2025, month: 2, day: 25)
var body: some View {
Chart(dataset) { data in
PointMark(x: .value("Day", data.day, unit: .weekday),
y: .value("Time", data.time, unit: .minute))
}
}
// The focus is on this line
.chartYScale(domain: .automatic(reversed: true))
}
}
The .chartYScale(domain:) modifier allows me to set the start of the day at the bottom, but still ignores some hours.
If I instead use this
Chart(...) { ... }
.chartYScale(domain: datemin...datemax)
The chart display every hour of the day, but now the start of the day is at the bottom. I can't seem to find a way to get both things at the same time. if I add both modifiers only the first one get applied while the other ignored.
Any solutions or workarounds?
Swift Charts
RSS for tagVisualize data with highly customizable charts across all Apple platforms using the compositional syntax of SwifUI.
Posts under Swift Charts tag
22 Posts
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
In creating a sequenced gesture combining a LongPressGesture and a DragGesture, I found that the combined gesture exhibits two problems:
The @GestureState does not properly update as the gesture progresses through its phases. Specifically, the updating(_:body:) closure (documented here) is only ever executed during the drag interaction. Long presses and drag-releases do not call the updating(_:body:) closure.
Upon completing the long press gesture and activating the drag gesture, the drag gesture remains empty until the finger or cursor has moved. The expected behavior is for the drag gesture to begin even when its translation is of size .zero.
This second problem – the nonexistence of a drag gesture once the long press has completed – prevents access to the location of the long-press-then-drag. Access to this location is critical for displaying to the user that the drag interaction has commenced.
The below code is based on Apple's example presented here. I've highlighted the failure points in the code with // *.
My questions are as follows:
What is required to properly update the gesture state?
Is it possible to have a viable drag gesture immediately upon fulfilling the long press gesture, even with a translation of .zero?
Alternatively to the above question, is there a way to gain access to the location of the long press gesture?
import SwiftUI
import Charts
enum DragState {
case inactive
case pressing
case dragging(translation: CGSize)
var isDragging: Bool {
switch self {
case .inactive, .pressing:
return false
case .dragging:
return true
}
}
}
struct ChartGestureOverlay<Value: Comparable & Hashable>: View {
@Binding var highlightedValue: Value?
let chartProxy: ChartProxy
let valueFromChartProxy: (CGFloat, ChartProxy) -> Value?
let onDragChange: (DragState) -> Void
@GestureState private var dragState = DragState.inactive
var body: some View {
Rectangle()
.fill(Color.clear)
.contentShape(Rectangle())
.onTapGesture { location in
if let newValue = valueFromChartProxy(location.x, chartProxy) {
highlightedValue = newValue
}
}
.gesture(longPressAndDrag)
}
private var longPressAndDrag: some Gesture {
let longPress = LongPressGesture(minimumDuration: 0.2)
let drag = DragGesture(minimumDistance: .zero)
.onChanged { value in
if let newValue = valueFromChartProxy(value.location.x, chartProxy) {
highlightedValue = newValue
}
}
return longPress.sequenced(before: drag)
.updating($dragState) { value, gestureState, _ in
switch value {
case .first(true):
// * This is never called
gestureState = .pressing
case .second(true, let drag):
// * Drag is often nil
// * When drag is nil, we lack access to the location
gestureState = .dragging(translation: drag?.translation ?? .zero)
default:
// * This is never called
gestureState = .inactive
}
onDragChange(gestureState)
}
}
}
struct DataPoint: Identifiable {
let id = UUID()
let category: String
let value: Double
}
struct ContentView: View {
let dataPoints = [
DataPoint(category: "A", value: 5),
DataPoint(category: "B", value: 3),
DataPoint(category: "C", value: 8),
DataPoint(category: "D", value: 2),
DataPoint(category: "E", value: 7)
]
@State private var highlightedCategory: String? = nil
@State private var dragState = DragState.inactive
var body: some View {
VStack {
Text("Bar Chart with Gesture Interaction")
.font(.headline)
.padding()
Chart {
ForEach(dataPoints) { dataPoint in
BarMark(
x: .value("Category", dataPoint.category),
y: .value("Value", dataPoint.value)
)
.foregroundStyle(highlightedCategory == dataPoint.category ? Color.red : Color.gray)
.annotation(position: .top) {
if highlightedCategory == dataPoint.category {
Text("\(dataPoint.value, specifier: "%.1f")")
.font(.caption)
.foregroundColor(.primary)
}
}
}
}
.frame(height: 300)
.chartOverlay { chartProxy in
ChartGestureOverlay<String>(
highlightedValue: $highlightedCategory,
chartProxy: chartProxy,
valueFromChartProxy: { xPosition, chartProxy in
if let category: String = chartProxy.value(atX: xPosition) {
return category
}
return nil
},
onDragChange: { newDragState in
dragState = newDragState
}
)
}
.onChange(of: highlightedCategory, { oldCategory, newCategory in
})
}
.padding()
}
}
#Preview {
ContentView()
}
Thank you!