willdale / SwiftUICharts

A charts / plotting library for SwiftUI. Works on macOS, iOS, watchOS, and tvOS and has accessibility features built in.
MIT License
843 stars 105 forks source link

Adding xAxisLabels causes LineChart issues #129

Closed iain-reid closed 2 years ago

iain-reid commented 2 years ago

I may be misunderstanding the implementation - but I don't seem to be able to get the x-axis labels to align at the bottom of the chart.

I've set the style to make use of the .chartData, and provided those labels in the LineChartData. When I add this to the Chart itself, the yAxis labels remain the same - but the yAxis grid compresses to the top 50% of the view, as does the line - and the x-axis labels are centered!

Before I add .xAxisLabels(chartData: chartData) :-

CleanShot 2021-11-01 at 16 15 13

After I add .xAxisLabels(chartData: chartData) :-

CleanShot 2021-11-01 at 16 15 58

And this is how I have implemented :-

private var baseline: Double {
        let baseline = Double(readings.minimumValue() - (readings.valueRange() / 2))
        if baseline <= 0 {
            return 0
        }
        return baseline
    }

    private var topLine: Double {
        return Double(readings.maximumValue() + (readings.valueRange() / 2))
    }

    private var chartStyle: LineChartStyle {
        let yAxisGridStyle = GridStyle(
            numberOfLines: 5,
            lineColour: Color.Brand.sky,
            lineWidth: 1,
            dash: [1000]
        )

        return LineChartStyle(
            xAxisLabelPosition: .bottom,
            xAxisLabelFont: Font.Primary.metadata,
            xAxisLabelColour: Color.UI.midGrey,
            xAxisLabelsFrom: .chartData(rotation: .degrees(0)),
            yAxisGridStyle: yAxisGridStyle,
            yAxisLabelPosition: .leading,
            yAxisLabelFont: Font.Primary.metadata,
            yAxisLabelColour: Color.UI.midGrey,
            yAxisNumberOfLabels: 5,
            yAxisLabelType: .numeric,
            baseline: .minimumWithMaximum(of: baseline),
            topLine: .maximum(of: topLine),
            globalAnimation: .default)
    }

    private var dataSet: LineDataSet {
        let lineStyle = LineStyle(
            lineColour:  ColourStyle(colour: lineColor),
            lineType: curvedLine ? .curvedLine : .line,
            strokeStyle: Stroke(lineWidth: 2, lineCap: .round, lineJoin: .round),
            ignoreZero: ignoreZero)

        return LineDataSet(dataPoints: readings.map { (value: UInt, date: Date?) in
            LineChartDataPoint(value: Double(value), date: date)
        }, style: lineStyle)
    }

    private var chartData: LineChartData {
        return LineChartData(
            dataSets: dataSet,
            metadata: ChartMetadata(title: "Peak Flow"),
            xAxisLabels: ["M", "T", "W", "T", "F"],
            chartStyle: chartStyle,
            noDataText: Text("No Data"))
    }

    var body: some View {
        LineChart(chartData: chartData)
            .id(chartData.id)
            .yAxisGrid(chartData: chartData)
            .xAxisLabels(chartData: chartData)
            .yAxisLabels(chartData: chartData)
            .yAxisPOI(chartData: chartData,
                                  markerName: "Target",
                                  markerValue: 450,
                                  labelPosition: .center(specifier: "Target %.0f"),
                                  labelColour: Color.Brand.white,
                                  labelBackground: Color.UI.midGrey,
                                  lineColour: Color.UI.midGrey,
                                  strokeStyle: StrokeStyle(lineWidth: 1, dash: [2, 2]))
    }
}

Any help at all would be appreciated - Not sure if it is a bug, but would be ace if you could feedback, love the library and keen to use. Many thanks :-)

willdale commented 2 years ago

Hey @iain-reid, The main issue is the order of the View Modifiers, id should be at the end it seams.

var body: some View {
    LineChart(chartData: chartData)
        .yAxisPOI(chartData: chartData,
                  markerName: "Target",
                  markerValue: 450,
                  labelPosition: .center(specifier: "Target %.0f"),
                  labelColour: Color.white,
                  labelBackground: Color.gray,
                  lineColour: Color.gray,
                  strokeStyle: StrokeStyle(lineWidth: 1, dash: [2, 2]))
        .xAxisGrid(chartData: chartData)
        .yAxisGrid(chartData: chartData)
        .xAxisLabels(chartData: chartData)
        .yAxisLabels(chartData: chartData)
        .id(chartData.id)
}

There were some other small layout issues with the implementation. They were solved by changing how the data is declared.

var chartData: LineChartData = {
    let readings = Readings()

    var chartStyle: LineChartStyle {
        let yAxisGridStyle = GridStyle(
            numberOfLines: 5,
            lineColour: Color.blue,
            lineWidth: 1,
            dash: [1000]
        )
        return LineChartStyle(
            xAxisLabelPosition: .bottom,
            xAxisLabelsFrom: .chartData(rotation: .degrees(0)),
            yAxisGridStyle: yAxisGridStyle,
            yAxisNumberOfLabels: 5,
            globalAnimation: .default)
    }

    var dataSet: LineDataSet {
        let lineStyle = LineStyle(
            lineColour:  ColourStyle(colour: .blue),
            lineType: .line,
            strokeStyle: Stroke(lineWidth: 2, lineCap: .round, lineJoin: .round)
        )

        return LineDataSet(dataPoints: readings.readings.map {
            LineChartDataPoint(value: Double($0))
        }, style: lineStyle)
    }

    return LineChartData(
        dataSets: dataSet,
        metadata: ChartMetadata(title: "Peak Flow"),
        xAxisLabels: ["M", "T", "W", "T", "F"],
        chartStyle: chartStyle,
        noDataText: Text("No Data"))
}()
iain-reid commented 2 years ago

Hi Will,

Thanks for the help, much appreciated! - I've managed to get the layout looking better:

CleanShot 2021-11-02 at 10 25 38

With the following code:

import SwiftUI
import SwiftUICharts

struct PeakFlowChart: View {

    var lineWidth: CGFloat = 2
    var lineColor: Color = Color.Brand.royalBlue
    var readings: PeakFlowReadings = []
    var curvedLine: Bool = false
    var ignoreZero: Bool = true

    //
    private var baseline: Double {
        let baseline = Double(readings.minimumValue() - (readings.valueRange() / 2))
        if baseline <= 0 {
            return 0
        }
        return baseline
    }

    private var topLine: Double {
        return Double(readings.maximumValue() + (readings.valueRange() / 2))
    }

    private var chartData: LineChartData {

        var chartStyle: LineChartStyle {
            let yAxisGridStyle = GridStyle(
                numberOfLines: 5,
                lineColour: Color.Brand.sky,
                lineWidth: 1,
                dash: [1000]
            )

            return LineChartStyle(
                xAxisLabelPosition: .bottom,
                xAxisLabelFont: Font.Primary.metadata,
                xAxisLabelColour: Color.UI.midGrey,
                xAxisLabelsFrom: .chartData(rotation: .degrees(0)),
                yAxisGridStyle: yAxisGridStyle,
                yAxisLabelPosition: .leading,
                yAxisLabelFont: Font.Primary.metadata,
                yAxisLabelColour: Color.UI.midGrey,
                yAxisNumberOfLabels: 5,
                yAxisLabelType: .numeric,
                baseline: .minimumWithMaximum(of: baseline),
                topLine: .maximum(of: topLine),
                globalAnimation: .default)
        }

        var dataSet: LineDataSet {
            let lineStyle = LineStyle(
                lineColour:  ColourStyle(colour: lineColor),
                lineType: curvedLine ? .curvedLine : .line,
                strokeStyle: Stroke(lineWidth: 2, lineCap: .round, lineJoin: .round),
                ignoreZero: ignoreZero)

            return LineDataSet(dataPoints: readings.map { (value: UInt, date: Date?) in
                LineChartDataPoint(value: Double(value), date: date)
            }, style: lineStyle)
        }

        return LineChartData(dataSets: dataSet,
                             metadata: ChartMetadata(title: "Peak Flow"),
                             xAxisLabels: ["M", "T", "W", "T", "F"],
                             chartStyle: chartStyle ,
                             noDataText: Text("No Data"))

    }

    var body: some View {

        LineChart(chartData: chartData)
            .yAxisPOI(chartData: chartData,
                      markerName: "Target",
                      markerValue: 440,
                      labelPosition: .center(specifier: "Target %.0f"),
                      labelColour: Color.Brand.white,
                      labelBackground: Color.UI.midGrey,
                      lineColour: Color.UI.midGrey,
                      strokeStyle: StrokeStyle(lineWidth: 1, dash: [2, 2]))
            .yAxisGrid(chartData: chartData)
            .xAxisLabels(chartData: chartData)
            .yAxisLabels(chartData: chartData)
            .id(chartData.id)

    }
}

struct PeakFlowChart_Previews: PreviewProvider {

    static var previews: some View {
        VStack {
            Spacer()
            PeakFlowChart(readings: ChartDummyData.peakFlowReadings)
                .frame(height: 300)
            Spacer()
        }
    }
}

However - the x-axis labels, and y-axis grid still seem a little misaligned. I was able to remedy by adding negative padding when adding the sequence of modifiers:

LineChart(chartData: chartData)
            .yAxisPOI(chartData: chartData,
                      markerName: "Target",
                      markerValue: 440,
                      labelPosition: .center(specifier: "Target %.0f"),
                      labelColour: Color.Brand.white,
                      labelBackground: Color.UI.midGrey,
                      lineColour: Color.UI.midGrey,
                      strokeStyle: StrokeStyle(lineWidth: 1, dash: [2, 2]))
            .yAxisGrid(chartData: chartData)
            .xAxisLabels(chartData: chartData)
            .padding(.bottom, -15)
            .yAxisLabels(chartData: chartData)
            .id(chartData.id)

CleanShot 2021-11-02 at 10 29 07

Is there a better solution to this?

willdale commented 2 years ago

Hi @iain-reid, It's the way that the LineChartData is initialised. As it's a computed property, a new LineChartData gets create each time.

There are calculations in the library that, it appears, can't run when declared this way. When I print out the padding required to layout to place the bottom label it returns 0.

When I change the in declaration to use a closure in the code snippet below, it lays out properly.

private var chartData: LineChartData = {

    var lineWidth: CGFloat = 2
    var lineColor: Color = Color.blue

    var curvedLine: Bool = false
    var ignoreZero: Bool = true

    var readings = Readings()

    var chartStyle: LineChartStyle {
        var baseline: Double {
            let baseline = Double(readings.minimumValue() - (readings.valueRange() / 2))
            if baseline <= 0 {
                return 0
            }
            return baseline
        }

        var topLine: Double {
            return Double(readings.maximumValue() + (readings.valueRange() / 2))
        }

        let yAxisGridStyle = GridStyle(
            numberOfLines: 5,
            lineColour: Color.blue,
            lineWidth: 1,
            dash: [1000]
        )

        return LineChartStyle(
            xAxisLabelPosition: .bottom,

            xAxisLabelsFrom: .chartData(rotation: .degrees(0)),
            yAxisGridStyle: yAxisGridStyle,
            yAxisLabelPosition: .leading,

            yAxisNumberOfLabels: 5,
            yAxisLabelType: .numeric,
            baseline: .minimumWithMaximum(of: baseline),
            topLine: .maximum(of: topLine),
            globalAnimation: .default)
    }

    var dataSet: LineDataSet {
        let lineStyle = LineStyle(
            lineColour:  ColourStyle(colour: lineColor),
            lineType: curvedLine ? .curvedLine : .line,
            strokeStyle: Stroke(lineWidth: 2, lineCap: .round, lineJoin: .round),
            ignoreZero: ignoreZero)

        return LineDataSet(dataPoints: readings.readings.map {
            LineChartDataPoint(value: Double($0))
        }, style: lineStyle)
    }

    return LineChartData(dataSets: dataSet,
                         metadata: ChartMetadata(title: "Peak Flow"),
                         xAxisLabels: ["M", "T", "W", "T", "F"],
                         chartStyle: chartStyle ,
                         noDataText: Text("No Data"))

}()

Edit

Screenshot

Edit 2

I just saw that it is still slightly misaligned.... For the moment to get the bottom label to line up set LineChartStyle -> xAxisTitle: ""