Search code examples
swiftios-charts

How do I show data other than the chart points (x, y) in a Marker?


I have figured out how to modify the text and x, y values that appear in a marker, but I can't figure out how to show other data in the marker.

Here's my marker currently:

enter image description here

Instead of the x and y values, I want to show two values that were used to calculate the y value:

"5 reps @ 200 lb"


Update: In other words, the point being shown on the chart is 233.3 (the y value). But I need to make the label display the 5 and the 200 that were used to calculate the result of 233.3 that is the point on the chart.


It looks like I would use func refreshContent(entry: ChartDataEntry, highlight: Highlight) but this takes a ChartDataEntry and the values I want to show are not data points (ChartDataEntry).

I've looked through all the Issues on the Github project and the closest thing I could find to a solution was this thread, but the answer doesn't really help a beginner level developer.

Can someone educate me about how to display non-data point data in a marker?

Update: Thanks to @RazibMollick for directing me to the solution which was to use the ChartDataEntry initializer that takes an additional argument ( ChartDataEntry(x:y:data:) ) in my generatedLineData method:

let numberFormatter = NumberFormatter()
let repetitions = numberFormatter.string(from: liftEvents[index].repetitions)
let weightLifted = numberFormatter.string(from: liftEvents[index].weightLifted)
let liftData = LiftData(repetitions: repetitions!, weightLifted: weightLifted!)

return ChartDataEntry(x: (thisDateInSeconds! - beginningDateInSeconds) / (3600.0 * 24.0), y: yValue, data: (liftData as AnyObject))

And in refreshContent(entry:highlight) I now have this:

open override func refreshContent(entry: ChartDataEntry, highlight: Highlight)
{
    let unit = UserDefaults.weightUnit()
    let liftData = entry.data as! LiftData
    setLabel(String(format: "%@ reps @ %@ \(unit)", liftData.repetitions, liftData.weightLifted))
}

Now I have this:

enter image description here

Now if I can just get those corners on the BalloonMarker to be rounded...


Solution

  • [Swift 4], I used their helper class from here.

    I modified the classes like below (Setup your desire string in refreshContent that you wanted):

    import Foundation
    import Charts
    open class BalloonMarker: MarkerImage
    {
        open var color: UIColor?
        open var arrowSize = CGSize(width: 15, height: 11)
        open var font: UIFont?
        open var textColor: UIColor?
        open var insets = UIEdgeInsets()
        open var minimumSize = CGSize()
    
        fileprivate var labelns: NSString?
        fileprivate var _labelSize: CGSize = CGSize()
        fileprivate var _paragraphStyle: NSMutableParagraphStyle?
        fileprivate var _drawAttributes = [NSAttributedStringKey : Any]()
    
        public init(color: UIColor, font: UIFont, textColor: UIColor, insets: UIEdgeInsets)
        {
            super.init()
    
            self.color = color
            self.font = font
            self.textColor = textColor
            self.insets = insets
    
            _paragraphStyle = NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle
            _paragraphStyle?.alignment = .center
        }
    
        open override func offsetForDrawing(atPoint point: CGPoint) -> CGPoint
        {
            var offset = self.offset
    
            let chart = self.chartView
    
            var size = self.size
    
            if size.width == 0.0 && image != nil
            {
                size.width = image?.size.width ?? 0.0
            }
            if size.height == 0.0 && image != nil
            {
                size.height = image?.size.height ?? 0.0
            }
    
            let width = size.width
            let height = size.height
            let padding = CGFloat(8.0)
            var origin = point;
            origin.x -= width / 2;
            origin.y -= height;
            if origin.x + offset.x < 0.0
            {
                offset.x = -origin.x + padding
            }
            else if chart != nil && origin.x + width + offset.x > chart!.bounds.size.width
            {
                offset.x = chart!.bounds.size.width - origin.x - width - padding
            }
    
            if origin.y + offset.y < 0
            {
                offset.y = height + padding;
            }
            else if chart != nil && origin.y + height + offset.y > chart!.bounds.size.height
            {
                offset.y = chart!.bounds.size.height - origin.y - height - padding
            }
    
            return offset
        }
    
        open override func draw(context: CGContext, point: CGPoint)
        {
            if labelns == nil
            {
                return
            }
            let offset = self.offsetForDrawing(atPoint: point)
            let size = self.size
    
            var rect = CGRect(
                origin: CGPoint(
                    x: point.x + offset.x,
                    y: point.y + offset.y),
                size: size)
            rect.origin.x -= size.width / 2.0
            rect.origin.y -= size.height
            context.saveGState()
    
            if let color = color
            {
                context.setFillColor(color.cgColor)
                if(offset.y > 0) {
                    context.beginPath()
                    context.move(to: CGPoint(
                        x: rect.origin.x,
                        y: rect.origin.y + arrowSize.height))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x + (rect.size.width - arrowSize.width) / 2.0,
                        y: rect.origin.y + arrowSize.height))
                    //arrow vertex
                    context.addLine(to: CGPoint(
                        x: point.x,
                        y: point.y))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x + (rect.size.width + arrowSize.width) / 2.0,
                        y: rect.origin.y + arrowSize.height))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x + rect.size.width,
                        y: rect.origin.y + arrowSize.height))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x + rect.size.width,
                        y: rect.origin.y + rect.size.height))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x,
                        y: rect.origin.y + rect.size.height))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x,
                        y: rect.origin.y + arrowSize.height))
                    context.fillPath()
                } else {
                    context.beginPath()
                    context.move(to: CGPoint(
                        x: rect.origin.x,
                        y: rect.origin.y))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x + rect.size.width,
                        y: rect.origin.y))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x + rect.size.width,
                        y: rect.origin.y + rect.size.height - arrowSize.height))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x + (rect.size.width + arrowSize.width) / 2.0,
                        y: rect.origin.y + rect.size.height - arrowSize.height))
                    //arrow vertex
                    context.addLine(to: CGPoint(
                        x: point.x,
                        y: point.y))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x + (rect.size.width - arrowSize.width) / 2.0,
                        y: rect.origin.y + rect.size.height - arrowSize.height))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x,
                        y: rect.origin.y + rect.size.height - arrowSize.height))
                    context.addLine(to: CGPoint(
                        x: rect.origin.x,
                        y: rect.origin.y))
                    context.fillPath()
                }
            }
            if (offset.y > 0) {
                rect.origin.y += self.insets.top + arrowSize.height
            } else {
                rect.origin.y += self.insets.top
            }
            rect.size.height -= self.insets.top + self.insets.bottom
    
            UIGraphicsPushContext(context)
    
            labelns?.draw(in: rect, withAttributes: _drawAttributes)
    
            UIGraphicsPopContext()
    
            context.restoreGState()
        }
    
        open override func refreshContent(entry: ChartDataEntry, highlight: Highlight)
        {
            //setLabel(String(entry.y))
            setLabel(String(format: "%.1f reps @ %.01f lb", entry.x, entry.y))
        }
    
        open func setLabel(_ label: String)
        {
            labelns = label as NSString
    
            _drawAttributes.removeAll()
            _drawAttributes[NSAttributedStringKey.font] = self.font
            _drawAttributes[NSAttributedStringKey.paragraphStyle] = _paragraphStyle
            _drawAttributes[NSAttributedStringKey.foregroundColor] = self.textColor
    
            _labelSize = labelns?.size(withAttributes: _drawAttributes) ?? CGSize.zero
    
            var size = CGSize()
            size.width = _labelSize.width + self.insets.left + self.insets.right
            size.height = _labelSize.height + self.insets.top + self.insets.bottom
            size.width = max(minimumSize.width, size.width)
            size.height = max(minimumSize.height, size.height)
            self.size = size
        }
    }
    

    Then I add the Balloon marker like below:

    let marker:BalloonMarker = BalloonMarker(color: UIColor.black, font: UIFont(name: "Helvetica", size: 12)!, textColor: UIColor.white, insets: UIEdgeInsets(top: 7.0, left: 7.0, bottom: 7.0, right: 7.0))
            marker.minimumSize = CGSize(width: 75.0, height: 35.0)
            chartView.marker = marker
    

    [Edit 1]

    There is another method see below, you can consider adding additional data as an AnyObject & get the data value in refreshContent. or you can modify the BalloonMarker class by using dictionary mapping.

     ChartDataEntry(x:Double, y: Double, data: AnyObject?)