Okay, this should be enough to get you a long way down the road you’re working towards.
This is a custom Renderer, not a custom label generator, but this gets you a look similar to what your original screen shot showed. Really the power is in your hands, it’s just a matter of knowing how to work with the API to produce what you are looking for. All of what you showed in that original snap shot is possible, although this example doesn’t produce it exactly.
I used the sample data you provided to product this chart.
Here is the script, it should be placed in the configureChart Extension Function. There is quite possibly a bit of fluff in here, but I just through this together without much thought as to optimizing it.
from org.jfree.chart.renderer.xy import XYLineAndShapeRenderer
from org.jfree.chart.plot import PlotOrientation
from java.awt.geom import AffineTransform, Ellipse2D, Rectangle2D
from java.awt import Color
import math
class CustomXYLineAndShapeRenderer(XYLineAndShapeRenderer):
def drawItem(self,g2,state,dataArea,info,plot,domainAxis,rangeAxis,dataset,series,item,crosshairState,renderPass):
#do nothing if item is not visible
if not self.getItemVisible(series,item):
return
#first pass sraws the background
if self.isLinePass(renderPass):
if self.getItemLineVisible(series,item):
self.drawPrimaryLine(state,g2,plot,dataset,renderPass,series,item,domainAxis,rangeAxis,dataArea)
elif self.isItemPass(renderPass):
entities = None
if info is not None:
entities = info.getOwner().getEntityCollection()
self.drawSecondaryPass(g2,plot,dataset,renderPass,series,item,domainAxis,dataArea,rangeAxis,crosshairState,entities)
def drawPrimaryLine(self,state,g2,plot,dataset,renderPass,series,item,domainAxis,rangeAxis,dataArea):
if item == 0:
return
x1 = dataset.getXValue(series,item)
x0 = dataset.getXValue(series,item -1)
if math.isnan(x1) or math.isnan(x0):
return
xAxisLocation = plot.getDomainAxisEdge()
yAxisLocation = plot.getRangeAxisEdge()
transX0 = domainAxis.valueToJava2D(x0,dataArea,xAxisLocation)
transY0 = rangeAxis.valueToJava2D(series + 1,dataArea,yAxisLocation)
transX1 = domainAxis.valueToJava2D(x1,dataArea,xAxisLocation)
transY1 = rangeAxis.valueToJava2D(series + 1,dataArea,yAxisLocation)
if math.isnan(transX0) or math.isnan(transY0) or math.isnan(transX1) or math.isnan(transY1):
return
orientation = plot.getOrientation()
if orientation == PlotOrientation.HORIZONTAL:
state.workingLine.setLine(transY0,transX0,transY1,transX1)
elif orientation == PlotOrientation.VERTICAL:
state.workingLine.setLine(transX0,transY0,transX1,transY1)
#if jfree.chart.util.LineUtils.clipLine(state.workingLine,dataArea):
self.drawFirstPassShape(g2,renderPass,series,item,state.workingLine)
def drawSecondaryPass(self,g2,plot,dataset,renderPass,series,item,domainAxis,dataArea,rangeAxis,crosshairState,entities):
entityArea = None
x1 = dataset.getXValue(series,item)
if math.isnan(x1):
return
orientation = plot.getOrientation()
xAxisLocation = plot.getDomainAxisEdge()
yAxisLocation = plot.getRangeAxisEdge()
transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation)
transY1 = rangeAxis.valueToJava2D(series + 1, dataArea,yAxisLocation)
if math.isnan(transX1) or math.isnan(transY1):
return
if self.getItemShapeVisible(series,item):
#shape = self.getItemShape(series,item)
shape = Ellipse2D.Float(-12.0,-12.0,24.0,24.0)
if orientation == PlotOrientation.HORIZONTAL:
transform = AffineTransform.getTranslateInstance(transY1, transX1)
shape = transform.createTransformedShape(shape)
elif orientation == PlotOrientation.VERTICAL:
transform = AffineTransform.getTranslateInstance(transX1,transY1)
shape = transform.createTransformedShape(shape)
entityArea = shape
if shape.intersects(dataArea):
if self.getItemShapeFilled(series,item):
if self.useFillPaint:
g2.setPaint(self.getItemFillPaint(series,item))
else:
g2.setPaint(self.getItemPaint(series,item))
g2.fill(shape)
if self.drawOutlines:
if self.getUseOutlinePaint():
g2.setPaint(self.getItemOutlinePaint(series,item))
else:
g2.setPaint(self.getItemPaint(series,item))
g2.setStroke(self.getItemOutlineStroke(series,item))
g2.draw(shape)
xx = transX1
yy = transY1
if orientation == PlotOrientation.HORIZONTAL:
xx = transY1
yy = transX1
if self.isItemLabelVisible(series,item):
text = str(int(dataset.getYValue(series,item)))
position = self.getPositiveItemLabelPosition(series,item)
anchorPoint = self.calculateLabelAnchorPoint(position.getItemLabelAnchor(),xx,yy,orientation)
frc = g2.getFontRenderContext()
font = g2.getFont()
fm = g2.getFontMetrics(font)
bounds = Rectangle2D.Double(0.0,-fm.getAscent(),fm.stringWidth(text),fm.getHeight())
metrics = font.getLineMetrics(text,frc)
ascent = metrics.getAscent()
descent = metrics.getDescent()
leading = metrics.getLeading()
xAdj = -bounds.getWidth() / 2.0
yAdj = -descent - leading + bounds.getHeight() / 2.0
g2.setPaint(self.getItemLabelPaint(series,item))
g2.drawString(text,anchorPoint.getX() + xAdj,anchorPoint.getY() + yAdj + 3.5)
domainAxisIndex = plot.getDomainAxisIndex(domainAxis)
rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis)
self.updateCrosshairValues(crosshairState, x1, series + 1,domainAxisIndex, rangeAxisIndex, transX1, transY1, orientation)
if entities is not None and xx >= dataArea.getMinX() and xx <= dataArea.getMaxX() and yy >= dataArea.getMinY() and yy <= dataArea.getMaxY():
self.addEntity(entities, entityArea,dataset,series,item,xx,yy)
plot = chart.getXYPlot()
rend = CustomXYLineAndShapeRenderer()
rend.setSeriesItemLabelsVisible(0,True)
rend.setSeriesItemLabelPaint(0,Color.black)
plot.setRenderer(rend)
And here is the produced chart:
Let me know if there is anything that you need more explanation on.