Swift 4 Charts Can't set the ChartYAxis Domain with Negative Numbers - ios

I'm struggling with the modifiers in the Swift 4 Chart API. I'm attempting to make
a RuleMark Chart where some of the data are negative numbers.
When I set the chart domain to 0...whatever, the chart is presented, but the "bottom"
of the chart is above the minimum values. If I attempt to put a negative number in
the domain range, the code breaks. The diagnostics say "ambiguous use of operator '-'"
If I do not include a .chartYScale, the chart is correctly presented, but the Swift
assigned y scale values leave a lot of wasted empty space making the chart unfriendly.
I've tried many range/domain options but have been unsuccessful. I want to be able to
programmatically control the Y scale including a negative number range.
Here's the Chart view:
struct RuleChartStandard: View {
let dataStore = DataStore.shared
var body: some View {
Chart(dataStore.dayInfoRecords) { dayInfo in
RuleMark(
x: .value("Date", dayInfo.dateString),
yStart: .value("Temperature", dayInfo.high),
yEnd: .value("Temperature", dayInfo.low)
)
}
.frame(height: 500)
.padding(.leading, 25)
.chartYScale(domain: 0...125)//this works, but the scale is incorrect
//.chartYScale(domain: -50...125)//this does not work
}
}
And here is the helper dataStore file:
class DataStore: ObservableObject {
static let shared = DataStore()
#Published var dayInfoRecords: [DayInfo] = []
init() {
generateDayInfoRecords(number: 50)
}//init
func generateDayInfoRecords(number: Int) {
for x in 0..<number {
let di = DayInfo(date: generateDateForDayInfo(dayIndex: x), precipitation: Double.random(in: 0..<10), snowfall: Double.random(in: 0..<10), high: Double.random(in: 45..<110), low: Double.random(in: -30..<45))
dayInfoRecords.append(di)
}
}//generate records
func generateDateForDayInfo(dayIndex: Int) -> Date {
var components = DateComponents()
components.day = dayIndex + 1
components.month = 1
components.year = 2022
let date = Calendar.current.date(from: components)
return date ?? Date()
}
}//class
Any guidance would be appreciated. Xcode 14.2, iOS 16.2

Credit Joakim Danielson with pointing out that it is the Preview that is misbehaving. The negative number domain bottom works in a simulator or device.
And for others to take this further, you can specify the stride amount like this:
var yAxisValues: [Int] {
stride(from: -30, to: 120, by: 10).map { $0 }
}
Then add the modifier to the Chart:
.chartYAxis {
AxisMarks(values: yAxisValues)
}

Related

Using Dates as plot points on a Line Chart, how do you plot the marks to start at the beginning of a day in SwiftUI?

I cannot change the location of the Mark within the day.
I have tried changing the Marks and the x axis values (which are Date objects) to the start of the day, but it doesn't change the plot point. I assume it is rounding my Dates to the middle of the day (noon). How do I make my marks appear at the beginning of the day?
Notice the marks in my line chart do not start at the bottom left, and each mark never appears on a vertical line.
struct PackagesReceivedChart: View {
var plottablePackageData: [PlottablePackageData]
var calendarComponent: Calendar.Component
var xAxisDates : [Date]
let underGradient = LinearGradient(
gradient: Gradient (
colors: [
.red.opacity(0.7),
.red.opacity(0.5),
.red.opacity(0.3),
]
),
startPoint: .top,
endPoint: .bottom
)
var body: some View {
Chart{
ForEach(plottablePackageData){
LineMark(
x: .value("time", $0.timeframe, unit: calendarComponent),
y: .value("packagesReceived", $0.count)
)
.foregroundStyle(.red)
.lineStyle(StrokeStyle(lineWidth: 3))
.symbol(){
Circle()
.fill(.red)
.frame(width: 10)
}
AreaMark(x: .value("time", $0.timeframe, unit: calendarComponent),
y: .value("packagesReceived", $0.count))
.interpolationMethod(.linear)
.foregroundStyle(underGradient)
}
}
.chartXAxis{
AxisMarks(values: xAxisDates)
}
.chartYAxis{
AxisMarks(position: .leading)
}
.chartForegroundStyleScale([
"Packages Received" : .red
])
}
}
Calendar.Component in this example is .weekday

In SwiftUI how to focus soft keypad on Simulator and Preview (it works on hardware)?

I have nothing but praise for Paul Hudson's excellent Hacking With Swift tutorials but when I build and run Tutorial 10, the soft keypad only comes into focus on hardware (an iPhone 12 running iOS 15.6.1) but not on Simulator or Xcode Preview. The following message appears in the Console area.
Can't find keyplane that supports type 8 for keyboard
iPhone-PortraitChoco-DecimalPad; using
27100_PortraitChoco_iPhone-Simple-Pad_Default
On the Simulator the Done button appears at the bottom of the screen but does not appear on the Preview. I found similar issues pre-dating SwiftUI here and here. The most likely cause of the problem is that I have recreated the code incorrectly.
I am running Xcode 13.4.1 on MacOS 12.5.
Does someone else have the same problem running my code ? if not, what version of Xcode have you used ?
import SwiftUI
// Tutorial 10
struct ContentView: View {
#State private var checkAmount = 0.0
#State private var numberOfPeople = 2
#State private var tipPercentage = 20
#FocusState private var amountIsFocused: Bool
let tipPercentages = [10, 15, 20, 25, 0]
var totalPerPerson: Double {
let peopleCount = Double(numberOfPeople + 2)
let tipSelection = Double(tipPercentage)
let tipValue = checkAmount / 100 * tipSelection
let grandTotal = checkAmount + tipValue
let amountPerPerson = grandTotal / peopleCount
return amountPerPerson
}
var body: some View {
NavigationView {
Form {
Section {
TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currencyCode ?? "USD"))
.keyboardType(.decimalPad)
.focused($amountIsFocused)
Picker("Number of people", selection: $numberOfPeople) {
ForEach(2 ..< 100) {
Text("\($0) people")
}
}
}
Section {
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(tipPercentages, id: \.self) {
Text($0, format: .percent)
}
}
.pickerStyle(.segmented)
} header: {
Text("How much tip do you want to leave?")
}
Section {
Text(totalPerPerson, format: .currency(code: Locale.current.currencyCode ?? "USD"))
}
.navigationTitle("WeSplit")
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Done") {
amountIsFocused = false
}
}
}
}
}
}
}
For this to happen the following Simulator settings apply
Simulator, I/O, Keyboard, Connect Hardware Keyboard
When the app is built and run, Connect Hardware Keyboard is checked by default. In which case Command + k key will cause the software keyboard to be displayed. The app will operate on keystrokes either from the software keyboard or the qwerty keyboard. But if the user enters a value via the qwerty keyboard, this will immediately dismiss the software keyboard.
However, if Connect Hardware Keyboard is unchecked, the numeric keypad appears on the screen. The Qwerty keyboard is unresponsive and the on-screen numeric keypad offers the user only means of entering numeric values into the TextField via the Simulator.

How to select pie chart slice by default in Charts library

Am using Pie chart in, Charts ios library. I want to know, how can i select first slice by default when it Pie chart loads.
I found this chartView.highlightValue(x: 45, dataSetIndex: 0). But this code is not working.
My pie chart has two slices with PieChartDataEntry. I want first one to be selected by default.
let entries = (0..<count).map { (i) -> PieChartDataEntry in
if i == 0 {
return PieChartDataEntry(value: 45,
label: "")
} else {
return PieChartDataEntry(value: 55,
label: "")
}
}
If you want the first slice to be selected, set the x to be 0. Note that x here is the index of your data and dataSetIndex is always 0 for PieCharts
chartView.highlightValue(x: 0, dataSetIndex: 0)

Rotary Knob with SwiftUI

So I'm attempting to replicate the normal SwiftUI slider functionality with a rotary knob. I've got the UI coded up and functioning currently connected to a standard SwiftUI slider in order to rotate it.
Now I need to add the rest of the slider functionality(ie $value, range, stride) and the touch functionality(ie knob rotating when dragging up and down, left and right). And Honestly I'm at a lost on the best way to do this. Any help is greatly appreciated.
Below is the main file and the project can be found here on Github Slider Project
//
// FKKnob.swift
//
// Created by Brent Brinkley on 3/7/21.
//
import SwiftUI
struct FKKnob: View {
// Set the color for outer ring and inner dash
let color: Color
// Minimum appearance value:
let circleMin: CGFloat = 0.0
// Maximum appearance value
let circleMax: CGFloat = 0.9
// Because our circle is missing a chunk of degrees we have to account
// for this adjustment
let circOffsetAmnt: CGFloat = 1 / 0.09
// Offset needed to align knob properly
let knobOffset: Angle = .degrees(110)
// calculate the our circle's mid point
var cirMidPoint: CGFloat {
0.4 * circOffsetAmnt
}
// User modfiable control value
#State var value: CGFloat = 0.0
var body: some View {
VStack {
ZStack {
// MARK: - Knob with dashline
Knob(color: color)
.rotationEffect(
// Currently controlled by slider
.degrees(max(0, Double(360 * value )))
)
.gesture(DragGesture(minimumDistance: 0)
.onChanged({ value in
// Need help here setting amount based on x and y touch drag
}))
// MARK: - Greyed Out Ring
Circle()
.trim(from: circleMin, to: circleMax)
.stroke(Color.gray ,style: StrokeStyle(lineWidth: 6, lineCap: .round, dash: [0.5,8], dashPhase: 20))
.frame(width: 100, height: 100)
// MARK: - Colored ring inidicating change
Circle()
.trim(from: circleMin, to: value)
.stroke(color ,style: StrokeStyle(lineWidth: 6, lineCap: .round, dash: [0.5,8], dashPhase: 20))
.frame(width: 100, height: 100)
}
.rotationEffect(knobOffset)
Text("\(value * circOffsetAmnt, specifier: "%.0f")")
Slider(value: $value, in: circleMin...circleMax)
.frame(width: 300)
.accentColor(.orange)
}
}
}
struct DashedCircle_Previews: PreviewProvider {
static var previews: some View {
FKKnob(color: Color.orange)
}
}
Here's a version that I use in one of my projects.
When the drag starts, it sets an initial value (stored in startDragValue). This is because you always want the modification of the value to be based on what the knob value was when you started.
Then, I've made the decision to only change values based on the y axis. The rational is this: one could change based on absolute distance from x1, y1 to x2, y2, but you run into problems with trying to get negative values. For instance, it would probably make sense that the upper right quadrant would be an overall increase -- but what about the upper left quadrant -- would it lead to positive change (because of the y axis change) or negative (because of the x axis)?
If you decide to go the route of x,y change, this method will still get you set up.
struct ContentView: View {
#State var value : Double = 0.0
#State private var startDragValue : Double = -1.0
var body: some View {
Text("Knob \(value)")
.gesture(DragGesture(minimumDistance: 0)
.onEnded({ _ in
startDragValue = -1.0
})
.onChanged { dragValue in
let diff = dragValue.startLocation.y - dragValue.location.y
if startDragValue == -1 {
startDragValue = value
}
let newValue = startDragValue + Double(diff)
value = newValue < 0 ? 0 : newValue > 100 ? 100 : newValue
})
}
}
My slider bases the values on 100pt up or down from the control, but you can obviously change those to your preferences as well.
In terms of range, I'd suggest always having the knob go from 0.0 to 1.0 and then interpolating the values afterwards.
I found the SwiftUI RotationGesture() to work perfectly for this in that it allows the intuitive rotation of a knob without the downsides listed in the answer above.
The only unique downside is that it maybe users have gotten used to single-finger knob operation, (especially on iPhones) and single-finger drag gesture may be the way to go if you have small knobs in your app that it's tough to get two fingers on.
Having said that, you could always attach multiple gestures and get both. Anyway, thanks to #jnpdx for the bit about future gestures starting from where you left off with startRotation, and without further ado:
import SwiftUI
struct KnobView: View {
#State private var rotation: Double = 0.0
#State private var startRotation: Double = 0.0
var body: some View {
Knob()
.rotationEffect(.degrees(rotation))
.gesture(
RotationGesture()
.onChanged({ angle in
if startRotation == 0 {
startRotation = rotation
}
rotation = startRotation + angle.degrees
})
.onEnded({ _ in
startRotation = 0
})
)
}
}
// convience Knob to checkout this Stack Overflow answer
// that can be later replaced with your own
struct Knob: View {
var body: some View {
RoundedRectangle(cornerRadius: 12)
.frame(width: 200, height: 200)
}
}
struct KnobView_Previews: PreviewProvider {
static var previews: some View {
KnobView()
}
}

Swift iOS Charts - Hide certain Bar Chart value labels

I am using the 'Charts' framework for my iOS app using swift and I am using the Bar Chart on one of my VCs but it looks quite messy at the moment because it displays the value labels for every value, even if it's 0. I was wondering if it's possible to only show the labels if the value is above 0? I have been looking at other questions but I can't seem to find an answer for this. I know I can hide all of the labels using chartData.setDrawValues(false) however, I still want to display the values when they are above 0.
This is my current code to format the chart -
barChart.highlightPerTapEnabled = false
barChart.highlightPerDragEnabled = false
barChart.leftAxis.drawAxisLineEnabled = true
barChart.leftAxis.drawGridLinesEnabled = false
barChart.xAxis.labelPosition = .bottom
barChart.xAxis.centerAxisLabelsEnabled = true
barChart.xAxis.axisMinimum = 0
barChart.xAxis.axisMaximum = 7
barChart.xAxis.drawAxisLineEnabled = true
barChart.xAxis.drawGridLinesEnabled = true
barChart.rightAxis.enabled = false
barChart.leftAxis.axisMinimum = 0
I thought about doing something like this -
for value in chartDataSet.entries {
if value.y == 0 {
chartDataSet.drawValuesEnabled = false
}
else {
chartDataSet.drawValuesEnabled = true
}
}
but of course this doesn't work as it sets the boolean for the whole set.
Any help would be much appreciated!
Thanks
You just need to provide your custom IValueFormatter to BarChartData as below,
public class XValueFormatter: NSObject, IValueFormatter {
public func stringForValue(_ value: Double, entry: ChartDataEntry, dataSetIndex: Int, viewPortHandler: ViewPortHandler?) -> String {
return value <= 0.0 ? "" : String(describing: value)
}
}
let chartData = BarChartData(dataSet: yourSet)
chartData.setValueFormatter(XValueFormatter())
Note: String(describing: value) is just to provide a workable example. You can apply proper number formatter to convert Double into String.
barChartView.data?.setDrawValues(false)
// to disable the data in the bars

Resources