70 year old newbie here. I am attempting to integrate a chart within my Mac app, and using Swift Charts (Apple's). It's relatively easy to make a chart. The problem is the default charts look different to Apple's charts and are missing key elements that Apple uses.
Example:
Apples charts generaly have
I wrote the code:
struct BarChart: View {
var body: some View {
GroupBox {
HStack {
Spacer()
Text("This is the label")
.foregroundColor(.secondary)
}
.padding(.horizontal)
Chart(data) { item in
BarMark(
x: .value("% ", item.count)
)
.foregroundStyle(by: .value("Data:", item.usage))
}
.chartXAxis {
AxisMarks(values: .automatic(desiredCount: 0))
}
.clipShape(RoundedRectangle(cornerRadius: 3))
}
}
}
Any idea how I can add:
The storage chart in the System Preferences app probably isn't implemented with SwiftUI Charts framework. The chart has been looking this way since ages ago. I'd assume it doesn't use SwiftUI at all.
Anyway, the corner radius can be created with clipShape
, but rather than applying it on the Chart
, you should apply it on the BarMark
s. Use the indices of your data source to determine which corners to round.
.clipShape(clipShape(forIndex: i))
...
func clipShape(forIndex i: Int) -> UnevenRoundedRectangle {
if i == 0 {
return UnevenRoundedRectangle(topLeadingRadius: 5, bottomLeadingRadius: 5)
} else if i == data.count - 1 {
return UnevenRoundedRectangle(bottomTrailingRadius: 5, topTrailingRadius: 5)
} else {
return UnevenRoundedRectangle()
}
}
The separators can be created with transparent BarMark
s that have a very small x
value.
BarMark(...) // the actual bar mark for your data
if i != data.count - 1 {
BarMark(x: .value("Separator", 0.1))
.foregroundStyle(.clear)
}
The shadow is the hardest part. There is no such API to add an inner shadow, only on the top edge. In fact, SwiftUI charts doesn't seem to support ShapeStyle
s that have shadows at all. For example, using chartForegroundStyleScale
like this does not create any shadow at all.
.chartForegroundStyleScale([
"Category 1": Color.red.shadow(.inner(radius: 5)),
"Category 2": Color.green.shadow(.inner(radius: 5)),
"Category 3": Color.yellow.shadow(.inner(radius: 5)),
])
The only way I can think of is to nudge a blurred RectangleMark
around. This creates a something quite similar, but not exactly.
RectangleMark(yEnd: 3)
.foregroundStyle(.black.opacity(0.34))
.blur(radius: 1)
// this inset is needed
// or else the "shadow" extends beyond the rounded rectangles
.clipShape(Rectangle().inset(by: 1))
.offset(y: -1)
Here is the full code:
struct ContentView: View {
let data = [
Foo(width: 3, type: .one),
Foo(width: 4, type: .two),
Foo(width: 5, type: .three),
]
var body: some View {
Chart{
ForEach(data.indices, id: \.self) { i in
BarMark(x: .value("X", data[i].width))
.clipShape(clipShape(forIndex: i))
.foregroundStyle(by: .value("X", data[i].type))
if i != data.count - 1 {
BarMark(x: .value("Separator", 0.1))
.foregroundStyle(.clear)
}
}
RectangleMark(yEnd: 3)
.foregroundStyle(.black.opacity(0.34))
.blur(radius: 1)
.clipShape(Rectangle().inset(by: 1))
.offset(y: -1)
}
.chartXAxis {
AxisMarks(values: .automatic(desiredCount: 0))
}
.frame(height: 50)
.padding()
}
func clipShape(forIndex i: Int) -> UnevenRoundedRectangle {
if i == 0 {
return UnevenRoundedRectangle(topLeadingRadius: 5, bottomLeadingRadius: 5)
} else if i == data.count - 1 {
return UnevenRoundedRectangle(bottomTrailingRadius: 5, topTrailingRadius: 5)
} else {
return UnevenRoundedRectangle()
}
}
}
enum FooType: String, Plottable {
case one, two, three
}
struct Foo: Hashable {
let width: Double
let type: FooType
}