Charts: ์ฑ„์›Œ์ง„ ๊บพ์€์„ ํ˜• ์ฐจํŠธ

์— ๋งŒ๋“  2017๋…„ 10์›” 10์ผ  ยท  17์ฝ”๋ฉ˜ํŠธ  ยท  ์ถœ์ฒ˜: danielgindi/Charts

์ฑ„์›Œ์ง„ ๊บพ์€์„ ํ˜• ์ฐจํŠธ๋ฅผ ๊ทธ๋ฆฌ๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ๋” ๋ช…ํ™•ํ•˜๊ฒŒ ๊ทธ๋ฆผ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
screen shot 2017-10-09 at 23 02 38
๊ทธ๋ž˜์„œ ๋‚˜๋Š” ์˜ˆ์ œ๋ฅผ ๋ณด์•˜๊ณ  ๊ทธ๋“ค์€ IFillFormatter ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์„ธํŠธ์˜ ์ฑ„์›Œ์ง„ ์ค„์ด ๋๋‚˜๋Š” y์ถ• ์œ„์น˜๋ฅผ ์–ป์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ทธ๊ฒƒ์€ ๊ณ ์ •๋œ ์œ„์น˜์˜€๊ณ  ์ „ํ˜€ ์—ญ๋™์ ์ด์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด ์œ„์น˜๋Š” ๋ฐ์ดํ„ฐ ์„ธํŠธ์˜ ๊ฐ ์š”์†Œ์— ๋”ฐ๋ผ ์–ด๋–ป๊ฒŒ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ? ์˜ˆ๋ฅผ ๋“ค์–ด ์ธ๋ฑ์Šค == 0์ด๋ฉด ์ฑ„์›Œ์ง„ ์œ„์น˜ A์˜ ๋์ด ์žˆ๊ณ  ์ธ๋ฑ์Šค == 1์ด๋ฉด ์ฑ„์›Œ์ง„ ์œ„์น˜์˜ ๋‹ค๋ฅธ ๋ ๊ฐ’์ด ์žˆ์Šต๋‹ˆ๋‹ค.
๋˜๋Š” ์œ„์˜ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์€ ์ฐจํŠธ๋ฅผ ๊ทธ๋ฆฌ๋Š” ๋‹ค๋ฅธ ์ œ์•ˆ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

Good Example

๊ฐ€์žฅ ์œ ์šฉํ•œ ๋Œ“๊ธ€

LineChartRenderer ๋ฅผ ์„œ๋ธŒํด๋ž˜์‹ฑํ•˜๊ณ  IFillFormatter ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์— ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ @liuxuan30 ์˜ ์ œ์•ˆ์— ๋”ฐ๋ผ ์ด๋ฅผ ๋‹ฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‘ ๋ฒˆ์งธ ๋ฐ์ดํ„ฐ ์„ธํŠธ๋ฅผ ์ฑ„์šฐ๊ธฐ ๋ผ์ธ์— ๋Œ€ํ•œ ํ‘œ์‹œ๊ธฐ๋กœ ์ „๋‹ฌํ•˜๊ณ  ๊ทธ์— ๋”ฐ๋ผ ๊ฒฝ๋กœ๋ฅผ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค(์„ ํ˜•์—๋งŒ ํ•ด๋‹นํ•˜์ง€๋งŒ ๋ฒ ์ง€์–ด์— ๋Œ€ํ•ด์„œ๋„ ๋™์ผํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค).

class AreaFillFormatter: IFillFormatter {

    var fillLineDataSet: LineChartDataSet?

    init(fillLineDataSet: LineChartDataSet) {
        self.fillLineDataSet = fillLineDataSet
    }

    public func getFillLinePosition(dataSet: ILineChartDataSet, dataProvider: LineChartDataProvider) -> CGFloat {
        return 0.0
    }

    public func getFillLineDataSet() -> LineChartDataSet {
        return fillLineDataSet ?? LineChartDataSet()
    }

}
class CustomLineChartRenderer: LineChartRenderer {

    override open func drawLinearFill(context: CGContext, dataSet: ILineChartDataSet, trans: Transformer, bounds: XBounds) {
        guard let dataProvider = dataProvider else { return }

        let areaFillFormatter = dataSet.fillFormatter as? AreaFillFormatter

        let filled = generateFilledPath(
            dataSet: dataSet,
            fillMin: dataSet.fillFormatter?.getFillLinePosition(dataSet: dataSet, dataProvider: dataProvider) ?? 0.0,
            fillLineDataSet: areaFillFormatter?.getFillLineDataSet(),
            bounds: bounds,
            matrix: trans.valueToPixelMatrix)

        if dataSet.fill != nil
        {
            drawFilledPath(context: context, path: filled, fill: dataSet.fill!, fillAlpha: dataSet.fillAlpha)
        }
        else
        {
            drawFilledPath(context: context, path: filled, fillColor: dataSet.fillColor, fillAlpha: dataSet.fillAlpha)
        }
    }

    fileprivate func generateFilledPath(dataSet: ILineChartDataSet, fillMin: CGFloat, fillLineDataSet: ILineChartDataSet?, bounds: XBounds, matrix: CGAffineTransform) -> CGPath
    {
        let phaseY = animator?.phaseY ?? 1.0
        let isDrawSteppedEnabled = dataSet.mode == .stepped
        let matrix = matrix

        var e: ChartDataEntry!
        var fillLineE: ChartDataEntry?

        let filled = CGMutablePath()

        e = dataSet.entryForIndex(bounds.min)
        fillLineE = fillLineDataSet?.entryForIndex(bounds.min)

        if e != nil
        {
            if let fillLineE = fillLineE
            {
                filled.move(to: CGPoint(x: CGFloat(e.x), y: CGFloat(fillLineE.y * phaseY)), transform: matrix)
            }
            else
            {
                filled.move(to: CGPoint(x: CGFloat(e.x), y: fillMin), transform: matrix)
            }

            filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix)
        }

        // Create the path for the data set entries
        for x in stride(from: (bounds.min + 1), through: bounds.range + bounds.min, by: 1)
        {
            guard let e = dataSet.entryForIndex(x) else { continue }

            if isDrawSteppedEnabled
            {
                guard let ePrev = dataSet.entryForIndex(x-1) else { continue }
                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(ePrev.y * phaseY)), transform: matrix)
            }

            filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix)
        }

        // Draw a path to the start of the fill line
        e = dataSet.entryForIndex(bounds.range + bounds.min)
        fillLineE = fillLineDataSet?.entryForIndex(bounds.range + bounds.min)
        if e != nil
        {
            if let fillLineE = fillLineE
            {
                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(fillLineE.y * phaseY)), transform: matrix)
            }
            else
            {
                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: fillMin), transform: matrix)
            }
        }

        // Draw the path for the fill line (backwards)
        if let fillLineDataSet = fillLineDataSet {
            for x in stride(from: (bounds.min + 1), through: bounds.range + bounds.min, by: 1).reversed()
            {
                guard let e = fillLineDataSet.entryForIndex(x) else { continue }

                if isDrawSteppedEnabled
                {
                    guard let ePrev = fillLineDataSet.entryForIndex(x-1) else { continue }
                    filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(ePrev.y * phaseY)), transform: matrix)
                }

                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix)
            }
        }

        filled.closeSubpath()

        return filled
    }
}

๋ชจ๋“  17 ๋Œ“๊ธ€

์ฑ„์šฐ๊ธฐ ์‚ฌ๊ฐํ˜•์„ ๊ทธ๋ฆฌ๋Š” drawLinearFill ์‚ดํŽด๋ณด์‹ญ์‹œ์˜ค. generateFilledPath ๋ฐ getFillLinePosition ๋‚ด์˜ ์ฑ„์šฐ๊ธฐ ์‚ฌ๊ฐํ˜•์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. ์ด์ƒ์ ์œผ๋กœ๋Š” ์ค„ ์‚ฌ์ด์— rect๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋ฅผ ์žฌ์ •์˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

LineChartRenderer ๋ฅผ ์„œ๋ธŒํด๋ž˜์‹ฑํ•˜๊ณ  IFillFormatter ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์— ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ @liuxuan30 ์˜ ์ œ์•ˆ์— ๋”ฐ๋ผ ์ด๋ฅผ ๋‹ฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‘ ๋ฒˆ์งธ ๋ฐ์ดํ„ฐ ์„ธํŠธ๋ฅผ ์ฑ„์šฐ๊ธฐ ๋ผ์ธ์— ๋Œ€ํ•œ ํ‘œ์‹œ๊ธฐ๋กœ ์ „๋‹ฌํ•˜๊ณ  ๊ทธ์— ๋”ฐ๋ผ ๊ฒฝ๋กœ๋ฅผ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค(์„ ํ˜•์—๋งŒ ํ•ด๋‹นํ•˜์ง€๋งŒ ๋ฒ ์ง€์–ด์— ๋Œ€ํ•ด์„œ๋„ ๋™์ผํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค).

class AreaFillFormatter: IFillFormatter {

    var fillLineDataSet: LineChartDataSet?

    init(fillLineDataSet: LineChartDataSet) {
        self.fillLineDataSet = fillLineDataSet
    }

    public func getFillLinePosition(dataSet: ILineChartDataSet, dataProvider: LineChartDataProvider) -> CGFloat {
        return 0.0
    }

    public func getFillLineDataSet() -> LineChartDataSet {
        return fillLineDataSet ?? LineChartDataSet()
    }

}
class CustomLineChartRenderer: LineChartRenderer {

    override open func drawLinearFill(context: CGContext, dataSet: ILineChartDataSet, trans: Transformer, bounds: XBounds) {
        guard let dataProvider = dataProvider else { return }

        let areaFillFormatter = dataSet.fillFormatter as? AreaFillFormatter

        let filled = generateFilledPath(
            dataSet: dataSet,
            fillMin: dataSet.fillFormatter?.getFillLinePosition(dataSet: dataSet, dataProvider: dataProvider) ?? 0.0,
            fillLineDataSet: areaFillFormatter?.getFillLineDataSet(),
            bounds: bounds,
            matrix: trans.valueToPixelMatrix)

        if dataSet.fill != nil
        {
            drawFilledPath(context: context, path: filled, fill: dataSet.fill!, fillAlpha: dataSet.fillAlpha)
        }
        else
        {
            drawFilledPath(context: context, path: filled, fillColor: dataSet.fillColor, fillAlpha: dataSet.fillAlpha)
        }
    }

    fileprivate func generateFilledPath(dataSet: ILineChartDataSet, fillMin: CGFloat, fillLineDataSet: ILineChartDataSet?, bounds: XBounds, matrix: CGAffineTransform) -> CGPath
    {
        let phaseY = animator?.phaseY ?? 1.0
        let isDrawSteppedEnabled = dataSet.mode == .stepped
        let matrix = matrix

        var e: ChartDataEntry!
        var fillLineE: ChartDataEntry?

        let filled = CGMutablePath()

        e = dataSet.entryForIndex(bounds.min)
        fillLineE = fillLineDataSet?.entryForIndex(bounds.min)

        if e != nil
        {
            if let fillLineE = fillLineE
            {
                filled.move(to: CGPoint(x: CGFloat(e.x), y: CGFloat(fillLineE.y * phaseY)), transform: matrix)
            }
            else
            {
                filled.move(to: CGPoint(x: CGFloat(e.x), y: fillMin), transform: matrix)
            }

            filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix)
        }

        // Create the path for the data set entries
        for x in stride(from: (bounds.min + 1), through: bounds.range + bounds.min, by: 1)
        {
            guard let e = dataSet.entryForIndex(x) else { continue }

            if isDrawSteppedEnabled
            {
                guard let ePrev = dataSet.entryForIndex(x-1) else { continue }
                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(ePrev.y * phaseY)), transform: matrix)
            }

            filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix)
        }

        // Draw a path to the start of the fill line
        e = dataSet.entryForIndex(bounds.range + bounds.min)
        fillLineE = fillLineDataSet?.entryForIndex(bounds.range + bounds.min)
        if e != nil
        {
            if let fillLineE = fillLineE
            {
                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(fillLineE.y * phaseY)), transform: matrix)
            }
            else
            {
                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: fillMin), transform: matrix)
            }
        }

        // Draw the path for the fill line (backwards)
        if let fillLineDataSet = fillLineDataSet {
            for x in stride(from: (bounds.min + 1), through: bounds.range + bounds.min, by: 1).reversed()
            {
                guard let e = fillLineDataSet.entryForIndex(x) else { continue }

                if isDrawSteppedEnabled
                {
                    guard let ePrev = fillLineDataSet.entryForIndex(x-1) else { continue }
                    filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(ePrev.y * phaseY)), transform: matrix)
                }

                filled.addLine(to: CGPoint(x: CGFloat(e.x), y: CGFloat(e.y * phaseY)), transform: matrix)
            }
        }

        filled.closeSubpath()

        return filled
    }
}

ํ”„๋กœ์ ํŠธ๋ฅผ ๋งˆ์Šคํ„ฐํ•˜๊ธฐ ์œ„ํ•œ ์™„๋ฒฝํ•œ ์˜ˆ:)

์ด ์†”๋ฃจ์…˜์„ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐ/์‚ฌ์šฉํ•ฉ๋‹ˆ๊นŒ?

@rob-k ์ด์— ๋Œ€ํ•œ ์‚ฌ์šฉ ์˜ˆ๋ฅผ ์ œ๊ณตํ•ด ์ฃผ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?

๋ฌผ๋ก  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

let maxDataSet = LineChartDataSet()
let minDataSet = LineChartDataSet()

// ... fill the data sets

// Set the data
self.lineChart.data = LineChartData(dataSets: [maxDataSet, minDataSet])

// Set the custom line chart renderer
self.lineChart.renderer = CustomLineChartRenderer(dataProvider: self.lineChart, animator: self.lineChart.chartAnimator, viewPortHandler: self.lineChart.viewPortHandler)

maxDataSet.drawFilledEnabled = true
maxDataSet.fillFormatter = AreaFillFormatter(fillLineDataSet: minDataSet)

๋‚˜๋Š” ์ด๊ฒƒ์„ ์ˆ˜๋™์œผ๋กœ ์„ค์น˜ํ•˜๊ณ  ์˜ˆ์ œ์— ๋”ฐ๋ผ ๊ฐ ๋‹จ๊ณ„๋ฅผ ์ˆ˜ํ–‰ํ–ˆ๋Š”๋ฐ ์—ฌ์ „ํžˆ @rob-k๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ž…๋‹ˆ๋‹ค.

@rob-k ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š” CustomLineChartRenderer์˜ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๋ฅผ ์ค‘๋‹จ์ ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋””๋ฒ„๊ทธํ•˜๋ ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค.

LineChartRenderer ์„ค์ •์„ ํฌํ•จํ•˜๋„๋ก ๋‚ด ์˜๊ฒฌ์„ ์—…๋ฐ์ดํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

์—ฌ์ „ํžˆ ์šด์ด ์—†๋Š” ์‚ฌ๋žŒ @rob-k

ucbDataSet ์™€ lcbDataSet ์‚ฌ์ด์˜ ์ƒ‰์ƒ์„ ์›ํ•˜๋ฉด ๋‘ ๋ฒˆ์งธ ๋ฐ์ดํ„ฐ ์„ธํŠธ๋ฅผ ์ฒซ ๋ฒˆ์งธ ๋ฐ์ดํ„ฐ ์„ธํŠธ์˜ ํฌ๋งทํ„ฐ์— ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ์„ธํŠธ๋ฅผ ์ž์ฒด ํฌ๋งทํ„ฐ์— ์ „๋‹ฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์„ ์‹œ๋„ํ•˜์‹ญ์‹œ์˜ค.

ucbDataSet.fillFormatter = AreaFillFormatter(fillLineDataSet: lcbDataSet)

(๋˜๋Š” ๊ทธ ๋ฐ˜๋Œ€)

์ด๋ฏธ ๋‚ด๊ฐ€ ์„ค๋ช…ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•œ ๊ฒƒ์€
ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š” CustomLineChartRenderer ํด๋ž˜์Šค์˜ ํ•จ์ˆ˜ ์ค‘ ํ•˜๋‚˜๋ฅผ ์ค‘๋‹จ์ ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋””๋ฒ„๊ทธํ•˜๋ ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ์—์„œ ๋ฒ ์ง€์–ด ๋ชจ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

lineChartDataSet.mode = .horizontalBezier
ucbDataSet.mode = .horizontalBezier
lcbDataSet.mode = .horizontalBezier

๋‚ด๊ฐ€ ์ œ๊ณตํ•œ ์ฝ”๋“œ๋Š” ์„ ํ˜• ๋ชจ๋“œ์—์„œ๋งŒ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“œ๋ฅผ .linear ํ•˜๊ฑฐ๋‚˜ drawHorizontalBezier ์žฌ์ •์˜ํ•˜๊ณ  ๊ทธ์— ๋”ฐ๋ผ ์ฝ”๋“œ๋ฅผ ์กฐ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

@rob-k ๋ฐฉ๊ธˆ ์ž‘๋™ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฐพ์•„์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

CombinedChart๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๊ฒƒ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

CombinedChart๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๊ฒƒ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

ํ•ด๊ฒฐ์ฑ…์„ ์ฐพ์•˜์Šต๋‹ˆ๊นŒ? CombinedChart๋Š” LinearChartRender๊ฐ€ ์•„๋‹Œ CombinedChartRender๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์ฝœ๋ฐฑ์€ drawLinearFill ์•„๋‹ˆ๋ผ ๊ธฐ๋ณธ Render๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

drawLinearFill ๋ฉ”์†Œ๋“œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๊ฒ€์ƒ‰ํ•  ๋•Œ ๋งŽ์€ ์ƒˆ๋กœ์šด ๋ Œ๋”๋ง์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

CombinedChart๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๊ฒƒ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

์†”๋ฃจ์…˜ ๊ฒฐํ•ฉ ์ฐจํŠธ : https://github.com/PhilJay/MPAndroidChart/issues/338

์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰