The above script produced a result that looked good, but after playing around with it, I didn't like how the labels overlapped making some of them unreadable, so for fun, I developed the script further. It now adjusts the label positions slightly if they overlap.
Here is the result:
Here are the updated scripts: (In case anybody wants to use them or develop them further)
propertyChange event handler
if event.propertyName == 'selectedXValue':
from org.jfree.chart.annotations import XYTextAnnotation
from java.awt.font import FontRenderContext, TextLayout
from java.awt import Color, Font
plot = event.source.chart.plot
data = event.source.Data #or whatever the custom data property is
backgroundFont = Font(Font.MONOSPACED, Font.PLAIN, 18)
annotationFont = Font(Font.MONOSPACED, Font.BOLD, 18)
def getYValues(layout, yMidpoint):
rangeAxis = plot.rangeAxis
rangeHeight = float(rangeAxis.upperBound - rangeAxis.lowerBound)
chartHeight = float(event.source.chartRenderingInfo.chartArea.height)
labelHeight = float(layout.bounds.height)
labelRange = (labelHeight/chartHeight) * rangeHeight
y1 = float(yMidpoint) - (labelRange * .5)
y2 = float(yMidpoint) + (labelRange * .5)
return [y1, y2]
def getAnnotationX(layout, domainLocation): #Ensures labels stay within the bounds of the chart even when crosshair is near the edge
domainAxis = plot.domainAxis
domainWidth = domainAxis.upperBound - domainAxis.lowerBound
midPoint = domainAxis.lowerBound + (domainWidth * .5)
chartWidth = event.source.chartRenderingInfo.chartArea.width
pixelOffset = layout.bounds.width *.5
offsetRatio = pixelOffset/float(chartWidth)
domainOffset = domainWidth * offsetRatio
if domainLocation > midPoint:
return (domainLocation - domainOffset)
else:
return (domainLocation + domainOffset)
def getAnnotationText(row, column):
penName = data.getColumnName(column)
penValue = data.getValueAt(row, column)
text = penName + ': ' +str(penValue)
return text
def adjustAnnotations(annotations):
adjustedHeaders = ['column', 'text', 'x', 'y1', 'y2']
adjustedData = []
sortedData = system.dataset.sort(annotations, 3)
lowerBound = plot.rangeAxis.lowerBound
upperBound = plot.rangeAxis.upperBound
previousY2 = None
previousY1 = None
for row in range(sortedData.rowCount):
column = sortedData.getValueAt(row, 0)
text = sortedData.getValueAt(row, 1)
x = sortedData.getValueAt(row, 2)
y1 = sortedData.getValueAt(row, 3)
y2 = sortedData.getValueAt(row, 4)
if y2 > lowerBound and y1 < upperBound:
if row == 0:
labelHeight = y2 - y1
lowerLabelY1 = float(lowerBound)
lowerLabelY2 = lowerLabelY1 + (2 * labelHeight)
if y1 < lowerLabelY2:
yOffset = float(lowerLabelY2 - y1)
offsetY1 = y1 + yOffset
offsetY2 = y2 + yOffset
adjustedData.append([column, text, x, offsetY1, offsetY2])
previousY2 = offsetY2
else:
adjustedData.append([column, text, x, y1, y2])
previousY2 = y2
else:
if y1 < previousY2:
yOffset = float(previousY2 - y1)
offsetY1 = y1 + yOffset
offsetY2 = y2 + yOffset
adjustedData.append([column, text, x, offsetY1, offsetY2])
previousY2 = offsetY2
else:
adjustedData.append([column, text, x, y1, y2])
previousY2 = y2
adjustedDataset = system.dataset.toDataSet(adjustedHeaders, adjustedData)
finalSortedData = system.dataset.sort(adjustedDataset, 3, False)
finalizedData = []
for row in range(finalSortedData.rowCount):
column = finalSortedData.getValueAt(row, 0)
text = finalSortedData.getValueAt(row, 1)
x = finalSortedData.getValueAt(row, 2)
y1 = finalSortedData.getValueAt(row, 3)
y2 = finalSortedData.getValueAt(row, 4)
if row == 0:
if y2 > upperBound:
yOffset = float(y2 - upperBound)
offsetY1 = y1 - yOffset
offsetY2 = y2 - yOffset
finalizedData.append([column, text, x, offsetY1, offsetY2])
previousY1 = offsetY1
else:
finalizedData.append([column, text, x, y1, y2])
previousY1 = y1
else:
if y2 > previousY1:
yOffset = float(y2 - previousY1)
offsetY1 = y1 - yOffset
offsetY2 = y2 - yOffset
finalizedData.append([column, text, x, offsetY1, offsetY2])
previousY1 = offsetY1
else:
finalizedData.append([column, text, x, y1, y2])
previousY1 = y1
return system.dataset.toDataSet(adjustedHeaders, finalizedData)
def getAnnotations():
domainLocation = plot.domainCrosshairValue
headers = ['column', 'text', 'x', 'y1', 'y2']
subData = []
for row in range(data.rowCount):
if data.getValueAt(row, 0).getTime() == domainLocation:
for column in range(1, data.columnCount):
text = getAnnotationText(row, column)
layout = TextLayout(u'█' * len(text), annotationFont, FontRenderContext(None, False, False))
x = getAnnotationX(layout, domainLocation)
yMidpoint = data.getValueAt(row, column)
yValues = getYValues(layout, yMidpoint)
y1 = yValues[0]
y2 = yValues[1]
subData.append([column, text, x, y1, y2])
annotationData = system.dataset.toDataSet(headers, subData)
annotations = adjustAnnotations(annotationData)
return annotations
def setAnnotation():
annotations = getAnnotations()
for row in range(annotations.rowCount):
column = annotations.getValueAt(row, 0)
text = annotations.getValueAt(row, 1)
x = annotations.getValueAt(row, 2)
y1 = annotations.getValueAt(row, 3)
y2 = annotations.getValueAt(row, 4)
y = y1 + ((y2 - y1) * .5)
background = XYTextAnnotation(u'█' * len(text), x, y)
background.setPaint(Color.WHITE)
background.setFont(backgroundFont)
plot.addAnnotation(background)
annotation = XYTextAnnotation(text, x, y)
textColor = plot.renderer.getSeriesPaint(column-1)
annotation.setPaint(textColor)
annotation.setFont(annotationFont)
plot.addAnnotation(annotation)
for annotation in plot.getAnnotations():
if isinstance(annotation, XYTextAnnotation):
plot.removeAnnotation(annotation)
if event.source.chart.plot.annotationMode == 2: #This line ensures that the labels will only be added in XTrace Mode
setAnnotation()
getXTraceLabel extension function
#def getXTraceLabel(self, chart, penName, yValue):
return ''