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

Charts 3.0.1 - Align x labels (dates) with plots #2094

Closed
frederic-adda opened this issue Jan 18, 2017 · 12 comments
Closed

Charts 3.0.1 - Align x labels (dates) with plots #2094

frederic-adda opened this issue Jan 18, 2017 · 12 comments

Comments

@frederic-adda
Copy link

I am having a hard time to migrate Charts from version 2 (Swift 2.3) to 3 (Swift 3).

Basically, I can't have the x labels (dates) aligned correctly with the corresponding plots.

This is what I had before in version 2:
https://i.stack.imgur.com/wOL3t.png

In version 2, I had values for days 7, 8, 10 and 11. So I was missing a day in the middle, but the labels were correctly alined with the plots.

And here is what I have in version 3:
https://i.stack.imgur.com/arIL2.png

In version 3, the "labels" in the x axis have now been replaced by double (for dates, it's a timeInterval since 1970), and formatted via a formatter. So, indeniably, the graph is more "correct" now, since the chart correctly extrapolates the value for the 9th, but I can't find how to put the labels under the corresponding plots.

This is my code for the x axis:

let chartView = LineChartView()
...
let xAxis = chartView.xAxis
xAxis.labelPosition = .bottom
xAxis.labelCount = entries.count
xAxis.drawLabelsEnabled = true
xAxis.drawLimitLinesBehindDataEnabled = true
xAxis.avoidFirstLastClippingEnabled = true

// Set the x values date formatter
let xValuesNumberFormatter = ChartXAxisFormatter()
xValuesNumberFormatter.dateFormatter = dayNumberAndShortNameFormatter // e.g. "wed 26"
xAxis.valueFormatter = xValuesNumberFormatter

Here is the ChartXAxisFormatter class I created:

import Foundation
import Charts

class ChartXAxisFormatter: NSObject {
    var dateFormatter: DateFormatter?
}

extension ChartXAxisFormatter: IAxisValueFormatter {

    func stringForValue(_ value: Double, axis: AxisBase?) -> String {
        if let dateFormatter = dateFormatter {

            let date = Date(timeIntervalSince1970: value)
            return dateFormatter.string(from: date)
        }

        return ""
    }

}

So, the values here are correct, the formatting is correct, the shape of the chart is correct, but the alignment of the labels with the corresponding plots is not good.

Thanks for your help

@4np
Copy link
Contributor

4np commented Jan 18, 2017

I ran in the same issue using timestamps before and it is caused by what is being rendered on the x-axis is an interpolated timestamp. This basically means that the timestamps resulting in the labels using a dateFormatter are not the timestamps for your scores, causing the labels to not match up.

For example, let's assume you have x values [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]. The x-axis renderer might actually print 2.5 as a label trying to evenly distribute the labels on the x-axis, even though that particular x-value is not in your dataset. If you then consider you are using timestamps this means the dateFormatter may format some intermediate timestamp as a date. As you format by day this results in your labels not matching up with your scores even though the label is correct (as you basically round it off by day). If you were to format by day hour:minute you would see the label is indeed an interpolated date.

I solved the issue on my end by resorting to using array indexes instead of using timestamps for the x-axis and using a custom X-axis formatter:

import Foundation
import Charts

class MyCustomBottomAxisFormatter: NSObject, IAxisValueFormatter {
    private var scores: [MyScoreObject]?
    
    lazy private var dateFormatter: DateFormatter = {
        // set up date formatter using locale
        let dateFormatter = DateFormatter()
        dateFormatter.locale = Locale.current
        dateFormatter.dateStyle = .short
        dateFormatter.timeStyle = .none
        return dateFormatter
    }()

    convenience init(usingScores scores: [MyScoreObject]) {
        self.init()
        self.scores = scores
    }
    
    func stringForValue(_ value: Double, axis: AxisBase?) -> String {
        let index = Int(value)

        guard let scores = scores, index < scores.count, let date = scores[index].date else {
            return "?"
        }
        
        return dateFormatter.string(from: date)
    }
}

@frederic-adda
Copy link
Author

frederic-adda commented Jan 18, 2017

Thanks a lot for your answer!
And the very clear explanation about the timestamps being interpolated, it really makes sense.
I'll try your solution, and let you know.

Thanks!

@frederic-adda
Copy link
Author

frederic-adda commented Jan 18, 2017

@4np I guess that you then have to define your array of ChartDataEntry with xValues as indices too, instead of time intervals, right ?
Like so :

// Define chart yValues
        var entries = [ChartDataEntry]()
        for (index, score) in scores.enumerated() {
            let yValue = score.value
            let entry = ChartDataEntry(x: Double(index), y: yValue)
            entries.append(entry)
        }

@frederic-adda
Copy link
Author

frederic-adda commented Jan 18, 2017

@4np OK, that worked! My chart is now the same as in version 2.

But, I had to add 2 more things to my xAxis:

  // Show the limit lines behind each plot
  xAxis.drawLimitLinesBehindDataEnabled = true

  // Make sure that only 1 x-label per index is shown
  xAxis.granularityEnabled = true
  xAxis.granularity = 1

Thanks for your help.
Also, I opened an issue at StackOverflow there:
http://stackoverflow.com/questions/41720445/ios-charts-3-0-align-x-labels-dates-with-plots

If you have a SO account and want to copy / paste your answer there, I'd be glad to accept your answer there too.

Thanks
Fred

@frederic-adda
Copy link
Author

frederic-adda commented Jan 18, 2017

Also, I left this issue open until the Charts devs have a chance to look at it, and maybe introduce a way to choose how the labels should be aligned.

Because I now have to choose between 2 poor solutions:

  • either use time intervals as xValues (which is the spirit in which the DataEntry should now be constructed), but with misaligned labels,
  • or use indices as xValues (which is a way to revert to Charts 2.0), but with the risk that consecutive indices may represent non-consecutive days (as in my example).

Please let us know what you think.

@frederic-adda frederic-adda changed the title Charts 3.0 - Align x labels (dates) with plots Charts 3.0.1 - Align x labels (dates) with plots Jan 18, 2017
@thierryH91200
Copy link
Contributor

@frederic-adda

I managed to solve the problem
XAxis.granularity = 1.0 must be one day. that is the most important
The smallest date must be 0

(TimeInSeconds - miniDate) / (3600.0 * 24.0)
capture d ecran 2017-01-30 a 12 35 22

And all the trick is in the formatter
We do the opposite operation to retrieve the date
Let date2 = Date (timeIntervalSince1970: (value * 3600 * 24) + miniTime)
         Return dateFormatter.string (from: date2)
do not forget
DateFormatter.timeZone = NSTimeZone (abbreviation: "GMT + 0: 00") as TimeZone!
The demo is macOS
The link is https://github.com/thierryH91200/Charts-log
Line Date tab

@frederic-adda
Copy link
Author

@thierryH91200 Thanks Thierry !
I'll try to implement your solution right away.

@frederic-adda
Copy link
Author

It worked! Fantastic!
Thanks for your help!

@4np
Copy link
Contributor

4np commented Feb 7, 2017

Cool @thierryH91200, that does indeed seem the way to go forward 👍

@ethanfox
Copy link

This does not work for me

@Rj707
Copy link

Rj707 commented Nov 20, 2017

@frederic-adda did you find the solution for your issue??
actually I have issue when showing dates on X-Axis. I am showing dates on X-Axis by setting ChartDataEntry's X-value as TimeInterval, but then chart doesn't show data properly. While setting ChartDataEntry's X-value as array index shows the data.

Here is my code for setting ChartDataEntry's X-value as TimeInterval
for (int i = 0; i < self.temperatureUpdates.count; i++)
{
SFTemperatureUpdate *temperatureUpdate = [self.temperatureUpdates objectAtIndex:i];
double yVal = [temperatureUpdate.value floatValue];
NSTimeInterval timeInterval = [temperatureUpdate.careatedAt timeIntervalSince1970];
double xVal = timeInterval;
[yVals1 addObject:[[ChartDataEntry alloc] initWithX:xVal y:yVal]];

}

dummydata
Uploading OriginalData.png…

@Steveybrown
Copy link

Trying to get the above solutions working when using multiple LineChartDataSets on the one graph, has anyone been able to get it successfully working?

I'm having problems finding a solution when one dataset might differ in size compared to the other.
i.e

let x = LineChartDataSet(values: values1, label: "test 1") // count 5
let y = LineChartDataSet(values: values2, label: "test 2") // count 8

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

6 participants