halfhp / androidplot

Charts and plots for Android
http://androidplot.com
Apache License 2.0
505 stars 159 forks source link

Multicolor line format #92

Closed vasyl91 closed 5 years ago

vasyl91 commented 5 years ago

Is there any way to render line color depending on Y value? I found this but sadly it doesn't work.

vasyl91 commented 5 years ago

I would need something like this: 5tzm3

vasyl91 commented 5 years ago

Hmm, tried many combinations with code from the link I posted. Even created modified LineAndPointRenderer and LineAndPointFormatter (changes pointed with arrows <===):

    class MyLineAndPointFormatter extends LineAndPointFormatter{
        public MyLineAndPointFormatter() {
            super(Color.rgb(154, 223, 130), null, null, null);
        }

        @Override
        public Class<? extends SeriesRenderer> getRendererClass() {
            return MyLineAndPointRenderer.class;
        }

        @Override
        public SeriesRenderer getRendererInstance(XYPlot plot) {
            return new MyLineAndPointRenderer(plot);
        }
    }

    class MyLineAndPointRenderer extends LineAndPointRenderer<MyLineAndPointFormatter> {

        private final Path path = new Path();

        public MyLineAndPointRenderer(XYPlot plot) {
            super(plot);
        }

        @Override
        public void drawSeries(Canvas canvas, RectF plotArea, XYSeries series, LineAndPointFormatter formatter) {
            PointF thisPoint;
            PointF lastPoint = null;
            PointF firstPoint = null;
            path.reset();
            final List<PointF> points = getPointsCache(series);

            int iStart = 0;
            int iEnd = series.size();
            if(SeriesUtils.getXYOrder(series) == OrderedXYSeries.XOrder.ASCENDING) {
                final Region iBounds = SeriesUtils.iBounds(series, getPlot().getBounds());
                iStart = iBounds.getMin().intValue();
                if(iStart > 0) {
                    iStart--;
                }
                iEnd = iBounds.getMax().intValue() + 1;
                if(iEnd < series.size() - 1) {
                    iEnd++;
                }
            }
            for (int i = iStart; i < iEnd; i++) {
                final Number y = series.getY(i);
                final Number x = series.getX(i);
                PointF iPoint = points.get(i);

                if (y != null && x != null) {
                    if(iPoint == null) {
                        iPoint = new PointF();
                        points.set(i, iPoint);
                    }
                    thisPoint = iPoint;
                    getPlot().getBounds().transformScreen(thisPoint, x, y, plotArea);
                } else {
                    thisPoint = null;
                    iPoint = null;
                    points.set(i, iPoint);
                }

                // don't need to do any of this if the line isnt going to be drawn:
                if(formatter.hasLinePaint() && formatter.getInterpolationParams() == null) {
                    if (thisPoint != null) {

                        // record the first point of the new Path
                        if (firstPoint == null) {
                            path.reset();
                            firstPoint = thisPoint;

                            // create our first point at the bottom/x position so filling will look good:
                            path.moveTo(firstPoint.x, firstPoint.y);
                        }

                        if (lastPoint != null) {
                            appendToPath(path, thisPoint, lastPoint);
                        }

                        lastPoint = thisPoint;
                    } else {
                        if (lastPoint != null) {
                            renderPath(canvas, plotArea, path, firstPoint, lastPoint, formatter);
                        }
                        firstPoint = null;
                        lastPoint = null;
                    }
                }
            }

            if(formatter.hasLinePaint()) {
                if(formatter.getInterpolationParams() != null) {
                    List<XYCoords> interpolatedPoints = getInterpolator(
                            formatter.getInterpolationParams()).interpolate(series,
                            formatter.getInterpolationParams());
                    firstPoint = convertPoint(interpolatedPoints.get(ZERO), plotArea);
                    lastPoint = convertPoint(interpolatedPoints.get(interpolatedPoints.size()-ONE), plotArea);
                    path.reset();
                    path.moveTo(firstPoint.x, firstPoint.y);
                    for(int i = 1; i < interpolatedPoints.size(); i++) {
                        thisPoint = convertPoint(interpolatedPoints.get(i), plotArea);
                        path.lineTo(thisPoint.x, thisPoint.y);
                    }
                }

                if(firstPoint != null) {
                    renderPath(canvas, plotArea, path, firstPoint, lastPoint, formatter, series); // <=== added series
                }
            }
            renderPoints(canvas, plotArea, series, iStart, iEnd, points, formatter);
        }

        public void renderPath(Canvas canvas, RectF plotArea, Path path, PointF firstPoint, PointF lastPoint, LineAndPointFormatter formatter, XYSeries series) { // <=== added series
            Path outlinePath = new Path(path);

            final RectRegion bounds = getPlot().getBounds();
            final RectRegion plotRegion = new RectRegion(plotArea);

            // determine how to close the path for filling purposes:
            // We always need to calculate this path because it is also used for
            // masking off for region highlighting.
            switch (formatter.getFillDirection()) {
                case BOTTOM:
                    path.lineTo(lastPoint.x, plotArea.bottom);
                    path.lineTo(firstPoint.x, plotArea.bottom);
                    path.close();
                    break;
                case TOP:
                    path.lineTo(lastPoint.x, plotArea.top);
                    path.lineTo(firstPoint.x, plotArea.top);
                    path.close();
                    break;
                case RANGE_ORIGIN:
                    float originPix = (float) getPlot().getBounds().getxRegion()
                            .transform(getPlot().getRangeOrigin()
                                    .doubleValue(), plotArea.top, plotArea.bottom, true);
                    path.lineTo(lastPoint.x, originPix);
                    path.lineTo(firstPoint.x, originPix);
                    path.close();
                    break;
                default:
                    throw new UnsupportedOperationException(
                            "Fill direction not yet implemented: " + formatter.getFillDirection());
            }

            if (formatter.getFillPaint() != null) {
                canvas.drawPath(path, formatter.getFillPaint());
            } else 

            // draw each region:
            for (RectRegion thisRegion : bounds.intersects(formatter.getRegions().elements())) {
                XYRegionFormatter regionFormatter = formatter.getRegionFormatter(thisRegion);
                RectRegion thisRegionTransformed = bounds
                        .transform(thisRegion, plotRegion, false, true);
                thisRegionTransformed.intersect(plotRegion);
                if(thisRegion.isFullyDefined()) {
                    RectF thisRegionRectF = thisRegionTransformed.asRectF();
                    if (thisRegionRectF != null) {
                        try {
                            canvas.save();
                            canvas.clipPath(path);
                            canvas.drawRect(thisRegionRectF, regionFormatter.getPaint());
                        } finally {
                            canvas.restore();
                        }
                    }
                }
            }

            // finally we draw the outline path on top of everything else:
            if(formatter.hasLinePaint()) { // <===
                for (int i = 0; i < series.size(); i++) {
                    int y = (series.getY(i)).intValue();

                    if (y <= 30) {
                        Paint lowPaint = new Paint();
                        lowPaint.setColor(Color.RED);
                        canvas.drawPath(outlinePath, lowPaint);
                    } else {
                        Paint regularPaint = new Paint();
                        regularPaint.setColor(Color.rgb(154, 223, 130));
                        canvas.drawPath(outlinePath, regularPaint);
                    }

                }
            }

            path.rewind();
        }
    }

Called with:

MyLineAndPointFormatter lineFormatter = new MyLineAndPointFormatter();

Mostly I received such result (sometimes line was drawn properly, but color was set depending on first Y value I guess): 1

I'm out of ideas and definitely need your help. I've also no clue why threshold visible on chart was set to ~21(?). Wolud apreciate any answer ;)

halfhp commented 5 years ago

Hi vasyl91 - As the github issue submission guidelines state, this isn't the place to ask how-to questions; feel free to post your question(s) under the Androidplot tag on Stack Overflow. Having said that, the short answer is yes it's possible by using a Shader on your formatter's line paint.

vasyl91 commented 5 years ago

Shader works, but the "threshold" doesn't change accordingly to whats currently shown on Y axis. I rather mean sth like RectRegion applied to color of the line.

halfhp commented 5 years ago

That should just be a matter of adjusting the Shader. You can either fix your boundaries to a static range and then define a gradient or background image that properly encompasses that static range, or you can dynamically generate the gradient based on the current visible range (obtainable via Plot.getBounds()). The later is a bit harder to do but still not too terribly difficult. If you plan to use something besides linear scaling, say logarithmic, its even trickier, but still possible.

halfhp commented 5 years ago

For illustration purposes, here's a screen from a quick tweak to XYPlotWithBgImgActivity (available in the demo app source):

screenshot_1549583981

screenshot_1549584294