Charts: Gráfico de linha preenchida

Criado em 10 out. 2017  ·  17Comentários  ·  Fonte: danielgindi/Charts

Estou tentando desenhar um gráfico de linhas preenchido, vocês podem ver a imagem para mais clareza.
screen shot 2017-10-09 at 23 02 38
Então, eu vi o exemplo e eles estão usando o protocolo IFillFormatter para obter a posição do eixo y onde a linha preenchida do conjunto de dados termina. Mas era uma posição fixa, nada dinâmica. Como posso essa posição depender de cada elemento do conjunto de dados. Por exemplo, se índice == 0 então temos o final da posição preenchida A, se índice == 1 então temos um valor final diferente de uma posição preenchida.
Ou qualquer outra sugestão de desenhar um gráfico como a imagem acima.

Good Example

Comentários muito úteis

Consegui fazer isso seguindo as sugestões de @ liuxuan30 subclassificando LineChartRenderer e adicionando uma variável à classe implementando IFillFormatter , que me permite passar um segundo conjunto de dados como indicador para a linha de preenchimento e desenhe o caminho de acordo (apenas para linear, mas presumo que você possa fazer a mesma coisa para Bezier).

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
    }
}

Todos 17 comentários

dê uma olhada em drawLinearFill onde desenhou o retângulo de preenchimento. Ele calcula o retângulo de preenchimento entre generateFilledPath e getFillLinePosition . idealmente, você deve substituir esses métodos para obter o seu reto entre as linhas, você está pronto para ir

Consegui fazer isso seguindo as sugestões de @ liuxuan30 subclassificando LineChartRenderer e adicionando uma variável à classe implementando IFillFormatter , que me permite passar um segundo conjunto de dados como indicador para a linha de preenchimento e desenhe o caminho de acordo (apenas para linear, mas presumo que você possa fazer a mesma coisa para Bezier).

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
    }
}

exemplo perfeito para dominar o projeto :)

como resolver / usar essa solução?

@ rob-k você pode fornecer um exemplo de uso para este

Claro, eu uso o código da seguinte maneira:

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)

Eu instalei isso manualmente e segui cada etapa conforme o exemplo ainda parece que não está funcionando @ rob-k

@ rob-k eu tentei depurar usando pontos de interrupção qualquer função de CustomLineChartRenderer não sendo chamada

Eu atualizei meu comentário para incluir a configuração de LineChartRenderer .

ainda sem sorte man @ rob-k

Se você quiser a cor entre ucbDataSet e lcbDataSet você precisa passar o segundo conjunto de dados para o formatador do primeiro. Você está passando o conjunto de dados para o próprio formatador. Tente fazer o seguinte:

ucbDataSet.fillFormatter = AreaFillFormatter(fillLineDataSet: lcbDataSet)

(ou o contrário)

já tentei o que estou tentando explicar é
Eu tentei depurar usando pontos de interrupção, qualquer função da classe CustomLineChartRenderer não sendo chamada

Vejo que você está usando o modo Bezier em seu código:

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

O código que forneci funciona apenas para o modo linear. Você terá que alterar o modo para .linear ou substituir drawHorizontalBezier e ajustar o código de acordo.

@ rob-k acabou de funcionar, muito obrigado cara por descobrir

Seria possível implementar isso usando CombinedChart?

Seria possível implementar isso usando CombinedChart?

Você encontrou a solução ? o CombinedChart usa o CombinedChartRender, não o LinearChartRender, então o retorno de chamada não é chamado de drawLinearFill , ele usa o Render padrão

o método drawLinearFill não existe, são necessários muitos novos renderizadores quando você pesquisa na biblioteca

Seria possível implementar isso usando CombinedChart?

solução mixedChart: https://github.com/PhilJay/MPAndroidChart/issues/338

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

sjdevlin picture sjdevlin  ·  3Comentários

guanyanlin picture guanyanlin  ·  3Comentários

PrashantKT picture PrashantKT  ·  3Comentários

BrandonShega picture BrandonShega  ·  4Comentários

newbiebie picture newbiebie  ·  3Comentários