android mpandroidchart

¿Cómo funcionan los renderizadores de MPAndroidChart y cómo escribo un renderizador personalizado?



(1)

Estoy usando la biblioteca MPAndroidChart pero no tiene toda la funcionalidad que quiero fuera de la caja.

He oído que es posible implementar la funcionalidad que quiero escribiendo un renderizador personalizado.

He mirado el código fuente de los renderizadores en el repositorio MPAndroidChart GitHub, pero no puedo entender los conceptos involucrados.

¿Cómo funcionan los renderizadores MPAndroidChart?

¿Cuál es el procedimiento de alto nivel para escribir un renderizador personalizado?

Nota: para muchas preguntas publicadas en SO para mpandroidchart la solución es implementar algún tipo de renderizador personalizado. Un comentario sobre tales preguntas "puede resolver este problema escribiendo un renderizador personalizado" no es satisfactorio si no hay una guía. Escribir una respuesta que incluya la solución completa para un requisito poco común e inusual puede llevar mucho tiempo. No existe una guía existente para escribir un renderizador personalizado y se espera que esta pregunta pueda servir como una utilidad para que los interrogadores puedan ayudarse a sí mismos, si no un objetivo duplicado. Si bien he intentado mi propia respuesta aquí, otras respuestas, correcciones y comentarios son bienvenidos.


Comprender las vistas y el lienzo

Primero, uno debe estudiar la Guía de Canvas y Drawables de la documentación oficial de Android. Particularmente, es importante tener en cuenta que LineChart , BarChart , etc. son subclases de View que se muestran anulando la devolución de llamada onDraw(Canvas c) de la superclase View. Tenga en cuenta también la definición de "lienzo":

Un lienzo funciona para usted como un pretexto o interfaz para la superficie real sobre la que se dibujarán sus gráficos: contiene todas sus llamadas de "dibujar".

Cuando trabaje con renderizadores, se ocupará de la funcionalidad para dibujar líneas, barras, etc. en el lienzo.

Traducción entre valores en el gráfico y píxeles en el lienzo

Los puntos en el gráfico se especifican como valores x e y con respecto a las unidades en el gráfico. Por ejemplo, en el cuadro a continuación, el centro de la primera barra está en x = 0 . La primera barra tiene el valor y de 52.28 .

Esto claramente no se corresponde con las coordenadas de píxeles en el lienzo. En el lienzo, x = 0 en el lienzo sería un píxel del extremo izquierdo que está claramente en blanco. Del mismo modo, debido a que la enumeración de píxeles comienza desde arriba como y = 0 , la punta de la barra claramente no está en 52.28 (el valor y en el gráfico). Si utilizamos las opciones de desarrollador / ubicación del puntero, podemos ver que la punta de la primera barra es aproximadamente x = 165 e y = 1150 .

Un Transformer es responsable de convertir los valores del gráfico en coordenadas de píxeles (en pantalla) y viceversa. Un patrón común en los renderizadores es realizar cálculos utilizando valores de gráficos (que son más fáciles de entender) y luego al final usar el transformador para aplicar una transformación para renderizar en la pantalla.

Ver puerto y límites

Un puerto de vista es una ventana, es decir, un área delimitada en el gráfico. Los puertos de visualización se utilizan para determinar qué parte del gráfico puede ver el usuario actualmente. Cada gráfico tiene un ViewPortHandler que encapsula la funcionalidad relacionada con los puertos de visualización. Podemos usar ViewPortHandler#isInBoundsLeft(float x) isInBoundsRight(float x) para determinar qué valores de x puede ver el usuario actualmente.

En el gráfico que se muestra arriba, BarChart "conoce" el BarEntry para 6 y superiores, pero debido a que están fuera de los límites y no en la vista actual, 6 y hacia arriba no se representan. Por lo tanto, los valores de x de 0 a 5 están dentro de la ventana gráfica actual.

ChartAnimator

ChartAnimator proporciona una transformación adicional que se aplicará al gráfico. Por lo general, esta es una simple multiplicación. Por ejemplo, supongamos que queremos una animación donde los puntos del gráfico comiencen en la parte inferior y aumenten gradualmente a su valor y correcto durante 1 segundo. El animador proporcionará una phaseY que es un escalar simple que comienza en 0.000 a tiempo 0 0ms y aumenta gradualmente a 1.000 a 1.000 1000ms .

Un ejemplo de código de renderizador

Ahora que entendemos los conceptos básicos involucrados, echemos un vistazo a algunos códigos de LineChartRenderer :

protected void drawHorizontalBezier(ILineDataSet dataSet) { float phaseY = mAnimator.getPhaseY(); Transformer trans = mChart.getTransformer(dataSet.getAxisDependency()); mXBounds.set(mChart, dataSet); cubicPath.reset(); if (mXBounds.range >= 1) { Entry prev = dataSet.getEntryForIndex(mXBounds.min); Entry cur = prev; // let the spline start cubicPath.moveTo(cur.getX(), cur.getY() * phaseY); for (int j = mXBounds.min + 1; j <= mXBounds.range + mXBounds.min; j++) { prev = cur; cur = dataSet.getEntryForIndex(j); final float cpx = (prev.getX()) + (cur.getX() - prev.getX()) / 2.0f; cubicPath.cubicTo( cpx, prev.getY() * phaseY, cpx, cur.getY() * phaseY, cur.getX(), cur.getY() * phaseY); } } // if filled is enabled, close the path if (dataSet.isDrawFilledEnabled()) { cubicFillPath.reset(); cubicFillPath.addPath(cubicPath); // create a new path, this is bad for performance drawCubicFill(mBitmapCanvas, dataSet, cubicFillPath, trans, mXBounds); } mRenderPaint.setColor(dataSet.getColor()); mRenderPaint.setStyle(Paint.Style.STROKE); trans.pathValueToPixel(cubicPath); mBitmapCanvas.drawPath(cubicPath, mRenderPaint); mRenderPaint.setPathEffect(null); }

Las primeras líneas antes del bucle for son la configuración del bucle de renderizador. Tenga en cuenta que obtenemos la phaseY del ChartAnimator, el Transformador, y calculamos los límites del puerto de vista.

El bucle for básicamente significa "para cada punto que está dentro de los límites izquierdo y derecho del puerto de vista". No tiene sentido representar valores x que no se pueden ver.

Dentro del ciclo, obtenemos el valor x y el valor y para la entrada actual usando dataSet.getEntryForIndex(j) y creamos una ruta entre eso y la entrada anterior. Observe cómo la ruta se multiplica por la phaseY para la animación.

Finalmente, después de calcular las rutas, se aplica una transformación con trans.pathValueToPixel(cubicPath); y las rutas se representan en el lienzo con mBitmapCanvas.drawPath(cubicPath, mRenderPaint);

Escribir un renderizador personalizado

El primer paso es elegir la clase correcta para la subclase. Tenga en cuenta las clases en el paquete com.github.mikephil.charting.renderer incluyendo XAxisRenderer y LineChartRenderer etc. Una vez que cree una subclase, simplemente puede anular el método apropiado. Según el código de ejemplo anterior, anularíamos void drawHorizontalBezier(ILineDataSet dataSet) sin llamar a super (para no invocar la etapa de renderizado dos veces) y reemplazarlo con la funcionalidad que queremos. Si lo está haciendo bien, el método anulado debería parecerse al menos un poco al método que está anulando:

  1. Obtención de un controlador en el transformador, animador y límites.
  2. Recorriendo los valores de x visibles (los valores de x que están dentro de los límites del puerto de vista)
  3. Preparación de puntos para representar en valores de gráfico
  4. Transformando los puntos en píxeles en el lienzo
  5. Usando los métodos de la clase Canvas para dibujar en el lienzo

Debe estudiar los métodos en la clase Canvas ( drawBitmap etc.) para ver qué operaciones puede realizar en el bucle de renderizador.

Si el método que necesita anular no está expuesto, es posible que tenga que subclasificar un renderizador base como LineRadarRenderer para lograr la funcionalidad deseada.

Una vez que haya diseñado la subclase de renderizador que desea, puede consumirla fácilmente con Chart#setRenderer(DataRenderer renderer) o BarLineChartBase#setXAxisRenderer(XAxisRenderer renderer) y otros métodos.