Shape That Scales With Zoom

Discussion about JFreeChart related to stockmarket charts.
Locked
etfloyd
Posts: 2
Joined: Tue Mar 02, 2010 1:24 am
antibot: No, of course not.

Shape That Scales With Zoom

Post by etfloyd » Tue Mar 02, 2010 3:11 am

I'm working on displaying a Parabolic SAR indicator on a candlestick chart - a series of dots, one above or below each candle. Originally, I just created a small Ellipse2D circle and set it into the XYLineAndShapeRenderer and turned shape display on, line display off. This displayed the dots well enough. However, I found no satisfactory way to choose an appropriate dot size that worked well for all charts at all zoom levels and I could not find a renderer that would scale arbitrary item shapes, like my little circle. (If anyone is aware of such a renderer, or a better way to do this, please tell me and I'll happily convert to the officially supported way!) So, after much scouring of source code, I worked out a simple hack for scalable dot renderer. It's not a general-purpose shape-scaling renderer, but probably could form the basis for one with some tinkering (i.e., with an appropriate affine transform.) As it is, maybe this will be of use for someone else, or maybe provide an idea for solving a similar problem:

Code: Select all

     /**
     * ScalableDotRenderer is an XYLineAndShapeRenderer extension that produces a single circular dot per item whose size scales depending on the
     * zoom factor for the plot axes.  (This version also sets the base default so that only the shapes are visible, no lines.)
     */
     class ScalableDotRenderer extends XYLineAndShapeRenderer {
        /** Default factor by which to multiply bar width to obtain dot size */
        protected static final double DEFAULT_WIDTH_FACTOR = 0.5;
        /** Factor by which to multiply bar width to obtain dot size */
        protected double barwidthfactor = DEFAULT_WIDTH_FACTOR;
        /** The dot to use for the current pass */
        protected Shape thedot = null;

        /**
         * Constructor that sets the dot size as a proportion of the bar width
         *
         * @param factor the factor by which to multiply bar width to obtain dot size
         */
        public ScalableDotRenderer(double factor) {
            this();
            this.barwidthfactor = factor;
        }

        /**
         * Default constructor, leave bar width factor set to default;
         */
        private ScalableDotRenderer() {
            super();
            setBaseLinesVisible(false);
            setBaseShapesVisible(true);
        }

        /**
         * Override XYLineAndShapeRenderer.initialize() to ensure that thedot is recreated on next drawItem().
         */
        @Override
        public XYItemRendererState initialise(Graphics2D g2,
                Rectangle2D dataArea,
                XYPlot plot,
                XYDataset data,
                PlotRenderingInfo info) {
            thedot = null;
            return super.initialise(g2, dataArea, plot, data, info);
        }

        /**
         * Override the XYLineAndShapeRenderer.drawItem() to create and cache the scaled dot.
         */
        @Override
        public void drawItem(Graphics2D g2, XYItemRendererState state,
            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
            int series, int item, CrosshairState crosshairState, int pass) {

            if (thedot == null) {
                double width = 0.0;
                RectangleEdge domainEdge = plot.getDomainAxisEdge();
                double widthms = source.getWidth();
                double left = dataset.getXValue(series, item) - widthms;
                for (int j = 0; j < 2; j++) { // check at least two bars to ensure an accurate width
                    double right = left + widthms;
                    double lpos = domainAxis.valueToJava2D(left, dataArea, domainEdge);
                    double rpos = domainAxis.valueToJava2D(right, dataArea, domainEdge);
                    width = Math.max(width, Math.abs(rpos - lpos));
                    left += widthms;
                }
                width *= barwidthfactor;
                thedot = new Ellipse2D.Double(-width * 0.5, -width * 0.5, width, width);
            }

            super.drawItem(g2, state, dataArea, info, plot, domainAxis, rangeAxis, dataset, series, item, crosshairState, pass);
        }

        /**
         * Override XYLineAndShapeRenderer.getItemShape() to return the properly scaled dot.
         */
        @Override
        public Shape getItemShape(int series, int item) {
            return thedot;
        }
    }

skunk
Posts: 1087
Joined: Thu Jun 02, 2005 10:14 pm
Location: Brisbane, Australia

Re: Shape That Scales With Zoom

Post by skunk » Tue Mar 02, 2010 2:10 pm

I use an XYDotRenderer for my parabolic

etfloyd
Posts: 2
Joined: Tue Mar 02, 2010 1:24 am
antibot: No, of course not.

Re: Shape That Scales With Zoom

Post by etfloyd » Tue Mar 02, 2010 4:51 pm

Yea, I tried that and it makes little rectangular dots just fine, but it doesn't automatically scale the dot size on zoom. (I use zoom a lot!) It would have been a bit more intrusive to extend XYDotRenderer to do what I want since it draws its own dots inside drawItem() and it draws only rectangles. I'd have to completely replace drawItem() in the extension or hack the original source neither of which I'm willing to do due to maintainability consequences. Do you use dots of constant size, or do you have some other mechanism to set the dot size?

Update: FWIW, at first glance, it looks rather easy to change the above code to extend XYDotRenderer instead, if you're OK with rectangular dots. We can get rid of the getShape() override and replace "theshape" with a simple boolean. Then, instead of building the shape in drawItem(), just call setDotWidth() and setDotHeight(). This actually works and produces scaled rectangular dots on zoom. However, if you try it, you're bound to notice CPU utilization going through through the roof! This is because setDotWidth/Height() calls fireChangeEvent() every time it's called. XYDotRenderer does not provide a version of these methods with a "notify" parameter, and the internal variables dotWidth and dotHeight are private, so you can't override this behavior without hacking the original source.

Locked