ivnsch / SwiftCharts

Easy to use and highly customizable charts library for iOS
Apache License 2.0
2.53k stars 410 forks source link

SwiftCharts line chart jumps when starting to pan to the right #340

Closed JCMcLovin closed 6 years ago

JCMcLovin commented 6 years ago

I asked this question on Stack Overflow but I thought I'd ask here as well.

I'm creating a chart using SwiftCharts based on the Trendline example provided with the library. In my application, I'm charting weight lifting events showing the single highest lift weight on each distinct day. In my example, I'm charting 4 Bench Press events.

The chart renders when the view controller is first displayed, but when I pan to the right it stutters, spreads out, and the points move. This is what that looks like:

swiftcharts_trendline_480

Note that when the chart is first rendered, the lowest plotted value (208.0) appears to be between Mar 12 and Mar 13, yet after it does that stutter thing, the point moves to the right of Mar 13.

The chartPoints being plotted are:

▿ 4 elements ▿ 0 : Mar 12, 239.17 ▿ 1 : Mar 13, 208 ▿ 2 : Mar 15, 267.17 ▿ 3 : Mar 18, 240

Here's the view controller:

`Note that when the chart is first rendered, the lowest plotted value (208.0) appears to be between Mar 12 and Mar 13, yet after it does that stutter thing, the point moves to the right of Mar 13.

The chartPoints being plotted are:

▿ 4 elements ▿ 0 : Mar 12, 239.17 ▿ 1 : Mar 13, 208 ▿ 2 : Mar 15, 267.17 ▿ 3 : Mar 18, 240 Here's the view controller:

import UIKit
import SwiftCharts

class ChartViewController: UIViewController {

fileprivate var chart: Chart? // arc
fileprivate let dataManager = CoreDataHelper()

override func viewDidLoad() {
    super.viewDidLoad()

    let labelSettings = ChartLabelSettings(font: ChartDefaults.labelFont)

    let liftEventTypeUuid = "98608870-E3CE-476A-B1E4-018D2AE4BDBF"

    let liftEvents = dataManager.fetchLiftsEventsOfType(liftEventTypeUuid)

    let dailyLiftEvents = liftEvents.reduce(into: [Date:[LiftEvent]](), { dailyLiftEvents, currentLiftEvent in
        dailyLiftEvents[Calendar.current.startOfDay(for: currentLiftEvent.date), default: [LiftEvent]()].append(currentLiftEvent)
    })
    let dailyMaxLiftEvents = dailyLiftEvents.map({$0.value.max(by: {$0.oneRepMax < $1.oneRepMax})})
    var sortedLiftEvents = dailyMaxLiftEvents.sorted(by: { $0?.date.compare(($1?.date)!) == .orderedAscending })

    var readFormatter = DateFormatter()
    readFormatter.dateFormat = "dd.MM.yyyy"

    var displayFormatter = DateFormatter()
    displayFormatter.dateFormat = "MMM dd"

    let date = {(str: String) -> Date in
        return readFormatter.date(from: str)!
    }

    let calendar = Calendar.current

    let dateWithComponents = {(day: Int, month: Int, year: Int) -> Date in
        var components = DateComponents()
        components.day = day
        components.month = month
        components.year = year
        return calendar.date(from: components)!
    }

    func filler(_ date: Date) -> ChartAxisValueDate {
        let filler = ChartAxisValueDate(date: date, formatter: displayFormatter)
        filler.hidden = true
        return filler
    }

    let chartPoints = sortedLiftEvents.map { ChartPoint(x: ChartAxisValueDate(date: ($0?.date)!, formatter: displayFormatter), y: ChartAxisValueDouble(($0?.calculateOneRepMax().value)!)) }

    let highestWeight = sortedLiftEvents.last??.oneRepMax.value
    let lowestWeight = sortedLiftEvents.first??.oneRepMax.value

    let yValues = stride(from: roundToTen(x: lowestWeight! - 40), through: roundToTen(x: highestWeight! + 40), by: 20).map {ChartAxisValueDouble(Double($0), labelSettings: labelSettings)}

    let xGeneratorDate = ChartAxisValuesGeneratorDate(unit: .day, preferredDividers: 2, minSpace: 1, maxTextSize: 12)
    let xLabelGeneratorDate = ChartAxisLabelsGeneratorDate(labelSettings: labelSettings, formatter: displayFormatter)
    let firstDate = sortedLiftEvents.first??.date
    let lastDate = sortedLiftEvents.last??.date

    let xModel = ChartAxisModel(firstModelValue: (firstDate?.timeIntervalSince1970)!, lastModelValue: (lastDate?.timeIntervalSince1970)!, axisTitleLabels: [ChartAxisLabel(text: "Date", settings: labelSettings)], axisValuesGenerator: xGeneratorDate, labelsGenerator: xLabelGeneratorDate)

    let yModel = ChartAxisModel(axisValues: yValues, axisTitleLabel: ChartAxisLabel(text: "1-RM", settings: labelSettings.defaultVertical()))
    let chartFrame = ChartDefaults.chartFrame(view.bounds)

    var chartSettings = ChartDefaults.chartSettingsWithPanZoom // was ChartDefaults.chartSettings
    chartSettings.trailing = 80

    // Set a fixed (horizontal) scrollable area 2x than the original width, with zooming disabled.
    chartSettings.zoomPan.maxZoomX = 2
    chartSettings.zoomPan.minZoomX = 2
    chartSettings.zoomPan.minZoomY = 1
    chartSettings.zoomPan.maxZoomY = 1

    ChartAxisValuesStaticGenerator.generateYAxisValuesWithChartPoints(chartPoints, minSegmentCount: 10, maxSegmentCount: 20, multiple: 2, axisValueGenerator: {ChartAxisValueDouble($0, labelSettings: labelSettings)}, addPaddingSegmentIfEdge: false)

    let lineModel = ChartLineModel(chartPoints: chartPoints, lineColor: UIColor.red, lineCap: .round ,animDuration: 1, animDelay: 0)

    let trendLineModel = ChartLineModel(chartPoints: TrendlineGenerator.trendline(chartPoints), lineColor: UIColor.blue, animDuration: 0.5, animDelay: 1)

    let coordsSpace = ChartCoordsSpaceLeftBottomSingleAxis(chartSettings: chartSettings, chartFrame: chartFrame, xModel: xModel, yModel: yModel)
    let (xAxisLayer, yAxisLayer, innerFrame) = (coordsSpace.xAxisLayer, coordsSpace.yAxisLayer, coordsSpace.chartInnerFrame)

    let chartPointsLineLayer = ChartPointsLineLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, lineModels: [lineModel])

    let trendLineLayer = ChartPointsLineLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, lineModels: [trendLineModel])

    let settings = ChartGuideLinesDottedLayerSettings(linesColor: UIColor.black, linesWidth: ChartDefaults.guidelinesWidth)
    let guidelinesLayer = ChartGuideLinesDottedLayer(xAxisLayer: xAxisLayer, yAxisLayer: yAxisLayer, settings: settings)

    let chart = Chart(
        frame: chartFrame,
        innerFrame: innerFrame,
        settings: chartSettings,
        layers: [
            xAxisLayer,
            yAxisLayer,
            guidelinesLayer,
            chartPointsLineLayer,
            trendLineLayer
        ]
    )

    view.addSubview(chart.view)
    self.chart = chart
}

private struct liftWeightItem {
    let number: Int
    let text: String

    init(number: Int, text: String) {
        self.number = number
        self.text = text
    }
}

private struct liftDateItem {
    let name: String
    let quantity: liftWeightItem

    init(name: String, quantity: liftWeightItem) {
        self.name = name
        self.quantity = quantity
    }
}

func createChartPoint(dateStr: String, percent: Double, readFormatter: DateFormatter, displayFormatter: DateFormatter) -> ChartPoint {
    return ChartPoint(x: createDateAxisValue(dateStr, readFormatter: readFormatter, displayFormatter: displayFormatter), y: ChartAxisValueDouble(percent))
}

func createDateAxisValue(_ dateStr: String, readFormatter: DateFormatter, displayFormatter: DateFormatter) -> ChartAxisValue {
    let date = readFormatter.date(from: dateStr)!
    let labelSettings = ChartLabelSettings(font: ChartDefaults.labelFont, rotation: 45, rotationKeep: .top)
    return ChartAxisValueDate(date: date, formatter: displayFormatter, labelSettings: labelSettings)
}

func roundToTen(x : Double) -> Int {
    return 10 * Int(round(x / 10.0))
}

class ChartAxisValuePercent: ChartAxisValueDouble {
    override var description: String {
        return "\(formatter.string(from: NSNumber(value: scalar))!)%"
       }
   }
}

My suspicion is that it has something to do with the rendering of the x-axis so I've tried adjusting the preferredDividers and minSpace values:

let xGeneratorDate = ChartAxisValuesGeneratorDate(unit: .day, preferredDividers: 2, minSpace: 1, maxTextSize: 12)

but that doesn't fix it.

I've read through the documentation several times but I still haven't been able to figure it out. This is a beautifully written library so I'd really like to be able to use it. Any help is greatly appreciated.

JCMcLovin commented 6 years ago

Sorry, I realized this is for reporting issues with the library and isn't the place for help. I'm closing it. Anybody with the power to delete it, please feel free to do so.