I appeal to all java gurus in this community for help...
I have a Pie Chart with a legend on the right side, where the text is sometimes too long and therefore the Pie Chart itself shrinks.
But I need fixed width for the Pie Chart component.
Is it possible somehow to implement a multiline or word wrap to the legend of a Pie Chart?
This is my code for configuring the chart in the configureChart Extension Functions:
Summary
def configureChart(self, chart):
# Put your code here
from org.jfree.chart.labels import PieSectionLabelGenerator
from org.jfree.ui import RectangleEdge
from org.jfree.chart.block import BlockBorder
from org.jfree.chart import LegendItem
from java.awt import Color
class customLabelGenerator(PieSectionLabelGenerator):
def __init__(self):
self.skupaj = 0.0
self.vrednost = 0.0
self.procent = 0.0
def generateSectionLabel(self,dataset,key):
self.skupaj = 0.0
for kljuc in dataset.getKeys():
self.skupaj += dataset.getValue(kljuc)
self.vrednost = dataset.getValue(key)
stringvalue = myscripts.secondsToText3(self.vrednost)
self.procent = int(round((self.vrednost * 100.0) / self.skupaj, 0))
#print "self.procent1= ", self.procent
#print "###################################"
#result = key + " = " + stringvalue + " (" + str(self.procent) + "%)"
#result = stringvalue + " (" + str(self.procent) + "%)"
result = str(self.procent) + "%"
#print "result= ", result
return result
plot = chart.getPlot()
legend = chart.getLegend()
plot.setLabelGenerator(customLabelGenerator())
plot.setSimpleLabels(True)
plot.setLabelBackgroundPaint(None)
plot.setLabelOutlinePaint(None)
plot.setLabelShadowPaint(None)
plot.setBackgroundPaint(None)
plot.setOutlineVisible(False)
plot.setShadowPaint(None)
legend.setFrame(BlockBorder.NONE)
legend.setBackgroundPaint(None)
legend.setPosition(RectangleEdge.RIGHT)
I was searching the forum and found a couple of topics about a chart legend, but none for Pie Chart.
Also, the internet is all about java code for org.jfree.chart.JFreeChart which is like Klingon language to me...
Huh. I'd turn the legend off and use a template repeater beside it to show the legend names.
Extract the names and colors for the legends in the configureChart method and write a dataset to a custom property with the results. Use that custom property to drive the template repeater.
I haven't done it myself, so I don't either. It would take some time diving into the JFreeChart docs and introspecting Ignition's implementations of them. Not rocket science, but not trivial either.
I believe he meant power table. This is a good idea and a simple approach. The legend names are taken from column zero of the chart.data dataset, and the colors occur in the order specified in one of the other chart properties. I don't remember the name off the top of my head,The property is called sectionColorsbut the colors wouldn't need to be extracted, just specified in the power table's configureCell extension function with if, elif `rowIndex % 10 == ' 0 through 9
Edit: if done from a power table, this would be the code to use on the configureCell extension function:
#def configureCell([...]): add the following code to the power table's configure cell extension function
colors = self.parent.getComponent('Pie Chart').sectionColors #or the correct path to the chart if it's different
color = colors[rowView % len(colors)]
if colIndex == 0:
return {'foreground': color}
I finally got an opportunity to look at this, and I was able to accomplish the goal inherently with the StandardPieSectionLabelGenerator class and textwrap.
Here is the modified code example:
def configureChart(self, chart):
#add this
import textwrap
# and add StandardPieSectionLabelGenerator to the following line
from org.jfree.chart.labels import PieSectionLabelGenerator, StandardPieSectionLabelGenerator
from org.jfree.ui import RectangleEdge
from org.jfree.chart.block import BlockBorder
from org.jfree.chart import LegendItem
from java.awt import Color
# add this class and modify the width as needed
class WrappedStandardPieSectionLabelGenerator(StandardPieSectionLabelGenerator):
def generateSectionLabel(self, dataset, key):
label = StandardPieSectionLabelGenerator.generateSectionLabel(self, dataset, key)
wrapped_label = '\n'.join(textwrap.wrap(label, width=20))
return wrapped_label
class customLabelGenerator(PieSectionLabelGenerator):
def __init__(self):
self.skupaj = 0.0
self.vrednost = 0.0
self.procent = 0.0
def generateSectionLabel(self,dataset,key):
self.skupaj = 0.0
for kljuc in dataset.getKeys():
self.skupaj += dataset.getValue(kljuc)
self.vrednost = dataset.getValue(key)
stringvalue = myscripts.secondsToText3(self.vrednost)
self.procent = int(round((self.vrednost * 100.0) / self.skupaj, 0))
result = str(self.procent) + "%"
return result
plot = chart.getPlot()
legend = chart.getLegend()
#Add these two lines
legendLabelGenerator = WrappedStandardPieSectionLabelGenerator()
plot.setLegendLabelGenerator(legendLabelGenerator)
plot.setLabelGenerator(customLabelGenerator())
plot.setSimpleLabels(True)
plot.setLabelBackgroundPaint(None)
plot.setLabelOutlinePaint(None)
plot.setLabelShadowPaint(None)
plot.setBackgroundPaint(None)
plot.setOutlineVisible(False)
plot.setShadowPaint(None)
legend.setFrame(BlockBorder.NONE)
legend.setBackgroundPaint(None)
legend.setPosition(RectangleEdge.RIGHT)
This community is the best... Thank you @justinedwards.jle
That is exactly what I need except...
the text in the legend is centered and should be left aligned...
I'm looking into this 'textwrap' but I can't find anything for alignment...
Also for 'PieSectionLabelGenerator' I can't find anything for text alignment...
I couldn't find a native way to get left alignment either, but I found a way to closely emulate it.
At first I tried simply making all of the lines the same length with whitespace, but that didn't work presumably because of kerning and variable letter widths. I ended up finding a way to get the width of a string in pixels using fontMetrics, and I created a comparative string of underscores that is arbitrarily longer than any line that can be generated from a maxWidth I specify in the second line of the generateSectionLabel function. The for each line, I find the difference between the width of the comparative string and the width of the line, and then I divide that difference by the width of a single space to calculate how many spaces I have to add to each line to make all of the lines the same width.
It's not perfect, and if you look closely, you can see slight misalignments, but it's pretty close. Here is the result:
Here is the source code:
Source Code
import textwrap
from org.jfree.chart.labels import PieSectionLabelGenerator, StandardPieSectionLabelGenerator
from org.jfree.ui import RectangleEdge
from org.jfree.chart.block import BlockBorder
from org.jfree.chart import LegendItem
from java.awt import Color, FontMetrics, Graphics
from java.awt.image import BufferedImage
pieChart = self
class WrappedStandardPieSectionLabelGenerator(StandardPieSectionLabelGenerator):
def generateSectionLabel(self, dataset, key):
label = StandardPieSectionLabelGenerator.generateSectionLabel(self, dataset, key)
maxWidth = 20
wrapped_lines = textwrap.wrap(label, width=maxWidth)
font = pieChart.legendFont
image = BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB)
g = image.createGraphics()
g.setFont(font)
fm = g.getFontMetrics()
alignmentwidth = maxWidth + 10
comparitiveString = '_' * alignmentwidth
comparitiveLength = fm.stringWidth(comparitiveString)
whiteSpaceWidth = fm.stringWidth(' ')
aligned_lines = [line + ' ' * int(float((comparitiveLength - fm.stringWidth(line)))/float(whiteSpaceWidth)) for line in wrapped_lines]
aligned_label = '\n'.join(aligned_lines)
g.dispose()
return aligned_label
class customLabelGenerator(PieSectionLabelGenerator):
def __init__(self):
self.skupaj = 0.0
self.vrednost = 0.0
self.procent = 0.0
def generateSectionLabel(self,dataset,key):
self.skupaj = 0.0
for kljuc in dataset.getKeys():
self.skupaj += dataset.getValue(kljuc)
self.vrednost = dataset.getValue(key)
stringvalue = myscripts.secondsToText3(self.vrednost)
self.procent = int(round((self.vrednost * 100.0) / self.skupaj, 0))
result = str(self.procent) + "%"
return result
plot = chart.getPlot()
legend = chart.getLegend()
legendLabelGenerator = WrappedStandardPieSectionLabelGenerator()
plot.setLegendLabelGenerator(legendLabelGenerator)
plot.setLabelGenerator(customLabelGenerator())
plot.setSimpleLabels(True)
plot.setLabelBackgroundPaint(None)
plot.setLabelOutlinePaint(None)
plot.setLabelShadowPaint(None)
plot.setBackgroundPaint(None)
plot.setOutlineVisible(False)
plot.setShadowPaint(None)
legend.setFrame(BlockBorder.NONE)
legend.setBackgroundPaint(None)
legend.setPosition(RectangleEdge.RIGHT)
Edit: Changed pieChart.labelFont to pieChart.legendFont
I had another idea on this, and I did some looking into font properties. Consequently, I found a list of monospace fonts that don't kern and keep the same width no matter the letter. I did a quick experiment and discovered a mistake in my code. Originally, I was using the labelFont field to set the font metric, but this is not correct. The legend gets its font from a separate legendFont field.
In any case, after correcting that, I set the font to Courier New, Plain and eliminated the arbitrary +10 from my alignmentwidth variable. Afterwards, the left align came out perfect every time. Of course, this means that the alignmentwidth variable is not needed, and the maxWidth variable could be used exclusively.
The top Pie Chart is with your code and the other two are Pie Charts without legend (disabled) and template repeater on the right side (as @pturmel suggested).
It turns out that with a template repeater wasn't so hard to do...
But I prefer everything done with only one component if it's possible (Pie Chart) and @justinedwards.jle proved that is possible.
Now I (we) have two solutions instead of one (choices, choices,...).
...so he DID mean template repeater; my apologies for the mistake. The repeater choice looks good, but does it remain centered on the graph when there is something other than 5 items? I actually did do a mock up with a power table, but I abandoned the approach because it didn't look like it was going to be as simple as I had initially envisioned. Seven or more items generated a scroll bar, and one or two items looked funny because it was off center. Also, getting a bullet point to render correctly looked like it was going to be a problem.
I suppose if anybody wanted to pursue a third option, here are the notes I had typed up from that attempt along with the scripting I had developed:
Power Table Approach Notes
#custom method on the power table within a template to be triggered from the table's initialize extension function, and the pie charts data property change event:
#def populateTabel(self)
headers = ['bullets', 'label']
bulletPoint = '•'
chartData = []
pieData = self.parent.getComponent('Pie Chart').data
for row in range(pieData.rowCount):
rowData = []
rowData.append(bulletPoint)
rowData.append(pieData.getValueAt(row, 0))
chartData.append(rowData)
self.data = system.dataset.toDataSet(headers, chartData)
#develop a way to remove the table's outer border and add it here
#preset bullet column width here or do it by triggering the defaultTableConfig event
#=========================
#Power Table to be configured in this way:
#Header Visible = False
#bind Background Color to its container's background color
#Show Vertical Grid = False
#Show Horizontal Grid = False
#Row Selection Allowed = False
#Column Selection Allowed = False
#preadd columns bullets and label to table's data property
#Set the wrapText property of the label Column to true in the Column Attributes Data dataset
#=========================
#def configureCell([...]): add the following code to the power table's configure cell extension function
colors = self.parent.getComponent('Pie Chart').sectionColors
color = colors[rowView % len(colors)]
if colIndex == 0:
return {'foreground': color}
Well, yes, the template repeater is also not a universal approach.
If there is a need for more than 5 items, then the template will need a 'correction' like font size, label height, width, ...
Every individual need will need the adoption of a template and repeater.
But then, more than five or six items in Pie Chart will look... crowded...
Quick, pedantic note - if you use the "logical" font name "Monospaced" you will get a platform-agnostic monospace font 'for free'. In Ignition's case, it will be Noto Sans Mono which should look pretty cohesive with the rest of the platform.