15

I'm using the ios-charts framework and want to create a marker that floats over the graph when I touch and move my finger from side to side. I'm using a line chart just for reference.

However, I can't figure out how to get the position of the marker on the chart when I touch it.

I've read about subclassing MarkerView but that isn't working. Can you please show me how to get the position of the point that shows up when you touch an ios-chart?

Rob Norback
  • 6,401
  • 2
  • 34
  • 38

2 Answers2

33

So I finally got this to work and wanted to add it to the community. Here is what my chart looks like:

enter image description here

When you run your finger over the chart the the reader above moves and reads the value from the chart.

To get this to work I had to do the following:

  1. In the file you are implementing the graph, be sure to include ChartViewDelegate

  2. Then implement the chartValueSelected method.

  3. If you then use the getMarkerPosition method, you'll be able to center your "marker" wherever you want.

Here's some sample code:

class MarkerView: UIView {
    @IBOutlet var valueLabel: UILabel!
    @IBOutlet var metricLabel: UILabel!
    @IBOutlet var dateLabel: UILabel!
}

let markerView = MarkerView()

func chartValueSelected(chartView: ChartViewBase, entry: ChartDataEntry, dataSetIndex: Int, highlight: ChartHighlight) { 

    let graphPoint = chartView.getMarkerPosition(entry: entry,  highlight: highlight)

    // Adding top marker
    markerView.valueLabel.text = "\(entry.value)"
    markerView.dateLabel.text = "\(months[entry.xIndex])"
    markerView.center = CGPointMake(graphPoint.x, markerView.center.y)
    markerView.hidden = false

}

markerView is a xib that I added manually to the main view that also contains the graph view. markerView is a UIView that contains three labels. It's the 3.0, impressions, and Apr you see in the picture.

graphPoint is the CGPoint located on the graph. You can use the x and y of the graphPoint to reposition your markerView. Here I simply kept the y value of the markerView the same and changed it's x-value to make it move back and forth on touch.

Rob Norback
  • 6,401
  • 2
  • 34
  • 38
  • What is floatingGraphReader ?? its main class is? – DeviPhone26 Mar 07 '16 at 06:33
  • What is floatingGraphReader? – Jess Murray Apr 10 '16 at 11:35
  • `floatingGraphReader` is simply the UIView that contains three labels. It's the 3.0, impressions, and Apr you see in the picture. Just added that to the answer. – Rob Norback Apr 11 '16 at 19:19
  • Thank you, this helped me. Though I would really like it if you could get the bar width as well :P Any clue? – netdigger May 16 '16 at 16:48
  • @everlof unfortunately I don't know how to get that bar width. – Rob Norback May 16 '16 at 16:58
  • Nice. Do I have to add a separate markerview file, or just add the code inside the class I allready have? When I did that, ran and pushed the bar, I get: "fatal error: unexpectedly found nil while unwrapping an Optional value", highlightning the : markerView.valueLabel.text = "\(entry.value)" line. – Tom Tallak Solbu Aug 23 '16 at 19:03
  • 1
    You have to create your own markerView with a valueLabel. The one I created is attached to a xib. So if yours isn't attached to anything it won't work. – Rob Norback Aug 23 '16 at 19:27
  • 2
    So forget about the top marker with 3 labels, how you draw the vertical line and the circle on the value ?? @RobNorback – Maryam Fekri Feb 08 '17 at 12:12
  • Hey @RobNorback, could you share your implementation of MarkerView ? I'm having a hard time implementing it. :T – Lucas Pereira Feb 09 '17 at 16:40
  • @Maryam, the MarkerView class is at the top of the code example. Nothing special, just a xib that I've linked IBOutlets to. – Rob Norback Apr 13 '17 at 19:07
  • The vertical line is referred to as the highlight property, and I simply used the markerPosition.x and markerPosition.y to place a dot on the screen where the actual point is on the graph. I should perhaps name that graphPoint for clarity. – Rob Norback Apr 13 '17 at 19:11
  • 2
    @RobNorback dude, how can I achieve this without using xib? – Lysdexia May 10 '17 at 08:38
  • I just want to draw a simple line (like the white one you have) - did you that yourself or was that included by the lib by default? @RobNorback – royherma Dec 10 '17 at 14:25
  • @royherma it's in the lib by default. Think it's called the highlight? Hope that helps. – Rob Norback Dec 11 '17 at 23:54
  • please what if I want to make the marker having the same y value as the entry? – Ne AS Jan 23 '18 at 11:03
  • @RobNorback I really like how you have only 1 circle at the point. How did you implement that? So far, when I enable circle, there's circles at all points on my line graph. – Kenny Ho Mar 06 '20 at 05:12
  • And what if we scroll and zoom the graph ? – Rajneesh071 Dec 30 '21 at 16:35
8

This is based on the example BallonMarker provided at the iOS-Charts repo.

Hopefully this comes in handy for someone, as I couldn't find other subclass examples of MarkerImage so this was a lot of trail and error for me as never used CGContext before

enter image description here

To use add this class

class PillMarker: MarkerImage {

    private (set) var color: UIColor
    private (set) var font: UIFont
    private (set) var textColor: UIColor
    private var labelText: String = ""
    private var attrs: [NSAttributedString.Key: AnyObject]!

    static let formatter: DateComponentsFormatter = {
        let f = DateComponentsFormatter()
        f.allowedUnits = [.minute, .second]
        f.unitsStyle = .short
        return f
    }()

    init(color: UIColor, font: UIFont, textColor: UIColor) {
        self.color = color
        self.font = font
        self.textColor = textColor

        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = .center
        attrs = [.font: font, .paragraphStyle: paragraphStyle, .foregroundColor: textColor, .baselineOffset: NSNumber(value: -4)]
        super.init()
    }

    override func draw(context: CGContext, point: CGPoint) {
        // custom padding around text
        let labelWidth = labelText.size(withAttributes: attrs).width + 10
        // if you modify labelHeigh you will have to tweak baselineOffset in attrs
        let labelHeight = labelText.size(withAttributes: attrs).height + 4

        // place pill above the marker, centered along x
        var rectangle = CGRect(x: point.x, y: point.y, width: labelWidth, height: labelHeight)
        rectangle.origin.x -= rectangle.width / 2.0
        let spacing: CGFloat = 20
        rectangle.origin.y -= rectangle.height + spacing

        // rounded rect
        let clipPath = UIBezierPath(roundedRect: rectangle, cornerRadius: 6.0).cgPath
        context.addPath(clipPath)
        context.setFillColor(UIColor.white.cgColor)
        context.setStrokeColor(UIColor.black.cgColor)
        context.closePath()
        context.drawPath(using: .fillStroke)

        // add the text
        labelText.draw(with: rectangle, options: .usesLineFragmentOrigin, attributes: attrs, context: nil)
    }

    override func refreshContent(entry: ChartDataEntry, highlight: Highlight) {
        labelText = customString(entry.y)
    }

    private func customString(_ value: Double) -> String {
        let formattedString = PillMarker.formatter.string(from: TimeInterval(value))!
        // using this to convert the left axis values formatting, ie 2 min
        return "\(formattedString)"
    }
}

Then activate for your chart

let marker = PillMarker(color: .white, font: UIFont.boldSystemFont(ofSize: 14), textColor: .black)
chartView.marker = marker
DogCoffee
  • 19,820
  • 10
  • 87
  • 120