Pie chart legend wrap or multiline

I appeal to all java gurus in this community for help... :pray:
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.

Screenshot_64

But I need fixed width for the Pie Chart component.

Screenshot_65

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... :crazy_face:

1 Like

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.

1 Like

Very good suggestion, but I really don't know how to do that... (extract the names and colors). :pleading_face:

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.

1 Like

Possible via JFreeChart, but non-trivial:

Yes, I saw this, but I can't figure out how to do this in Ignition...

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 sectionColors but 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}
1 Like

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)

Here is the result:
image

6 Likes

This community is the best... Thank you @justinedwards.jle :+1:
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... :confused:

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:
image

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

2 Likes

Vau, you're truly amazing... :crazy_face:
Thank you @justinedwards.jle for your effort.
Really appreciated... :+1:

If you ever set foot in Slovenia, all the beer you can drink is waiting for you... :beers:

2 Likes

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.

Here is the result using Consolas (another font on the list)
image

As you can see, using any monospace font corrects the slight left alignment deviations from the previous example.

3 Likes

Again, thank you @justinedwards.jle for your effort. :+1:
This is what I came up with:


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,...).

2 Likes

...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...

1 Like

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.

4 Likes

Is there an option where the "g" looks more like Monaco "g"?

I like that monospaced has 0, O, I, L, l, and i all easy to identify.
It is good.