Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I can only display LineChart once. #7

Closed
UncleRic opened this issue Feb 4, 2021 · 10 comments
Closed

I can only display LineChart once. #7

UncleRic opened this issue Feb 4, 2021 · 10 comments

Comments

@UncleRic
Copy link

UncleRic commented Feb 4, 2021

Environment: SwiftUI via Xcode 12.4

I tried to redisplay the LineChart graph with new data; but additional renderings didn't happen.

One band-aid approach is to launch another LineChart graph in place of the original for the new data.
This works; but is totally inefficient for multiple sets of data; and I still can't re-render already-rendered LineCharts().
Which means I have to have a LineChart() for EACH set of chartData.

The data is loaded from a class object and imported via @ObservedObject var dataSource = DataStuff()'s Published var.

Here's the code snippet with two different data sources:

...
 VStack {
                if dataSource.chartData != nil {
                    if graphToggle {
                    LineChart()
                        .touchOverlay(specifier: "%.2f")
                        .yAxisGrid()
                        .xAxisLabels()
                        .yAxisLabels()
                        .headerBox()
                        .legends()
                        .environmentObject(dataSource.chartData!)
                        .frame(minWidth: 300, maxWidth: 900, minHeight: 300, idealHeight: 400, maxHeight: 500, alignment: .center)
                        .padding(.all, 24)
                        .padding(.top, 12)
                    } else {
                        LineChart()
                            .touchOverlay(specifier: "%.2f")
                            .yAxisGrid()
                            .xAxisLabels()
                            .yAxisLabels()
                            .headerBox()
                            .legends()
                            .environmentObject(dataSource.chartData!)
                            .frame(minWidth: 300, maxWidth: 900, minHeight: 300, idealHeight: 400, maxHeight: 500, alignment: .center)
                            .padding(.all, 24)
                            .padding(.top, 12)
                    }
                    Spacer()
                }
                Spacer()
                ...

Do you know how I can reuse this code with fresh data within SwiftUI?

P.S. I tried to create a replacement instance of LineChart() for the new graph; doesn't work:

Button {
                   lineChart = nil
                   lineChart = LineChart()
                   dataSource.loadChartData1()
}
@willdale
Copy link
Owner

willdale commented Feb 4, 2021

Hi,

You can just change the [dataPoint].

Button(action: {
    dataSource.data.dataPoints = dataSource.makeDataTwo()
}, label: {
    Text("Change Data")
})

Does this code work for you?

View

struct UncleRicTestView: View {
    
    @ObservedObject var dataSource : TestObject = TestObject(data: ChartData(dataPoints: []))
    
    var body: some View {
        LineChart()
            .touchOverlay(specifier: "%.2f")
            .yAxisGrid()
            .xAxisLabels()
            .yAxisLabels()
            .headerBox()
            .legends()
            .environmentObject(dataSource.data)
            .frame(minWidth: 300, maxWidth: 900, minHeight: 300, idealHeight: 400, maxHeight: 500, alignment: .center)
            .padding(.all, 24)
            .padding(.top, 12)
        
        Button(action: {
            dataSource.data.dataPoints = dataSource.makeDataTwo()
        }, label: {
            Text("Change Data")
        })
    }
}

Object

class TestObject: ObservableObject {
 
    @Published var data : ChartData
    
    init(data: ChartData) {
        self.data = data
        self.data.dataPoints = makeDataOne()
    }
    
    func makeDataOne() -> [ChartDataPoint] {
        let data : [ChartDataPoint] = [
            ChartDataPoint(value: 20),
            ChartDataPoint(value: 90),
            ChartDataPoint(value: 100),
            ChartDataPoint(value: 75),
            ChartDataPoint(value: 160),
            ChartDataPoint(value: 110),
            ChartDataPoint(value: 90)
        ]
        return data
    }
    
    func makeDataTwo() -> [ChartDataPoint] {
        let data : [ChartDataPoint] = [
            ChartDataPoint(value: 70),
            ChartDataPoint(value: 20),
            ChartDataPoint(value: 0),
            ChartDataPoint(value: 50),
            ChartDataPoint(value: 40),
            ChartDataPoint(value: 150),
            ChartDataPoint(value: 60)
        ]
        return data  
    }
}

Does this help?

@UncleRic
Copy link
Author

UncleRic commented Feb 5, 2021

It looks good.
I was able to switch data.
But I have over 200 graphs that I select via a wheel picker.
I want them to share the same chart; but with different data, obviously.
I'll stress test it and get back to you.
Thanks for the speedy reply!
I was prepping for a huge file otherwise.

@USBA
Copy link

USBA commented Feb 5, 2021

Hey @UncleRic, I had a similar issue.
Try @willdale's solution on the issue #5

@State var selectedGraph = ""  // <---- the binding of that wheel picker

@State var data = ChartData(dataPoints: []) 


func chartDataPoints() -> [dataPoint] {
   ....
}



....


LineChart()
            .touchOverlay(specifier: "%.2f")
            .yAxisGrid()
            .xAxisLabels()
            .yAxisLabels()
            .headerBox()
            .legends()
            .environmentObject(data)
            .id(UUID())     // <---- add an id to the chart

           // Re-Initialise data at `onAppear`
          .onAppear {
              data.dataPoints = chartDataPoints()
          }

          // Update the data of the chart whenever the user selects something else from the wheel picker.
          .onChange(of: selectedGraph, perform: { value in
            data.dataPoints = chartDataPoints()
          })

In addition to @willdale's solution, add an id to the chart to force it to refresh when something changes.

@willdale
Copy link
Owner

willdale commented Feb 5, 2021

As an extension to what @USBA has said. Here's an example of it with a Picker.

The TempData lets you add some additional info for use in your view.

The .tag(data.datapoints) tells the ForEach which part of data you want to pass too selection.

View

struct UncleRicTestView: View {
    
    @ObservedObject var dataSource : TestObject = TestObject(chartData: ChartData(dataPoints: []))
    
    @State private var index : Int = 0
    
    @State private var selection: [ChartDataPoint] = []
    
    var body: some View {
        
        VStack {
            Picker(selection: $selection, label: Text("Test")) {
                ForEach(dataSource.data, id: \.self) { data in
                    Text("\(data.name)").tag(data.datapoints)
                }
                .onChange(of: selection) { value in
                    dataSource.chartData.dataPoints = value
                }
            }
                
            LineChart()
                .touchOverlay(specifier: "%.2f")
                .yAxisGrid()
                .xAxisLabels()
                .yAxisLabels()
                .headerBox()
                .legends()
                .environmentObject(dataSource.chartData)
                .frame(minWidth: 300, maxWidth: 900, minHeight: 300, idealHeight: 400, maxHeight: 500, alignment: .center)
                .padding(.all, 24)
                .padding(.top, 12)
        }
        .onAppear {
            dataSource.chartData.dataPoints = dataSource.data.first?.datapoints ?? [ChartDataPoint(value: 0)]
        }
    }
}

Object

class TestObject: ObservableObject {
 
    @Published var chartData : ChartData    
    @Published var data      : [TempData] = makeSomeDataPoints()
    
    init(chartData: ChartData) {
        self.chartData = chartData
    }
    
   static func makeSomeDataPoints() -> [TempData] {
        var main : [TempData] = []
        for index in 0...200 {
            var data : [ChartDataPoint] = []
            for _ in 1...7 {
                let value: Double = Double.random(in: 1...50)
                data.append(ChartDataPoint(value: value))
            }
            main.append(TempData(name: "\(index)", datapoints: data))
        }
        return main
    }
}

DataModel

struct TempData: Hashable {
    let name : String
    let datapoints : [ChartDataPoint]
}

@UncleRic
Copy link
Author

UncleRic commented Feb 5, 2021

I've studied your example.
You don't set the attributes, like the legend, chart style, etc.

I want to:

  1. Customize the attributes (style, legend etc.).
    Most of the attributes are stagnate, however the 'metadata' & 'title' are specific to the data used.

  2. After which, use dynamic ChartDataPoints (as you demoed).

What I tried to do is to do #1 with the code you sent.
That's tying me in a knot: Keep the customized environment whilst replenishing the data (+ metadata & title).

Your example uses the simplified ChartData, consisting of only ChartDataPoints.

I'm using ChartData consisting of both ChartDataPoints + Attributes.
That's the focus: trying to replicate what you did with the additional Attributes.

I'll continue to play with this thing.

Any help would be appreciated.

@willdale
Copy link
Owner

willdale commented Feb 5, 2021

Hi,

You can change chart parameters on at any time and the fly.

.onChange(of: selection) { value in
    withAnimation {
        dataSource.chartData.dataPoints = value
    }
    if Bool.random() {
        withAnimation {
            dataSource.chartData.lineStyle = LineStyle(colour: .blue, lineType: .line)
            dataSource.chartData.chartStyle.yAxisGridStyle = GridStyle(numberOfLines: 10)
        }
    } else {
        withAnimation {
            dataSource.chartData.lineStyle = LineStyle(colour: .red, lineType: .curvedLine)
            dataSource.chartData.chartStyle.yAxisGridStyle = GridStyle(numberOfLines: 3)
        }
    }
}

@willdale
Copy link
Owner

willdale commented Feb 5, 2021

Updated with metadata.

if Bool.random() {
    withAnimation {
        dataSource.chartData.metadata = ChartMetadata(title: "One",
                                                      subtitle: "is the loneliest number that you'll ever do",
                                                      lineLegend: "One")
        dataSource.chartData.lineStyle = LineStyle(colour: .blue, lineType: .line)
        dataSource.chartData.chartStyle.yAxisGridStyle = GridStyle(numberOfLines: 10)
    }
} else {
    withAnimation {
        dataSource.chartData.metadata = ChartMetadata(title: "Two",
                                                      subtitle: "can be as bad as one",
                                                      lineLegend: "One")
        dataSource.chartData.lineStyle = LineStyle(colour: .red, lineType: .curvedLine)
        dataSource.chartData.chartStyle.yAxisGridStyle = GridStyle(numberOfLines: 3)
    }
}

@UncleRic
Copy link
Author

UncleRic commented Feb 6, 2021

I play with it; thanks!

@willdale
Copy link
Owner

willdale commented Feb 6, 2021

If you can send me more code I can look more closely at it. I'm not super sure I understand the issue.

Have a great day.

@willdale
Copy link
Owner

I'm going to close this off but feel free to get in contact if you want help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants