Change each bar width in histogram chart

Yes in my example chart time column is not used
but right now i need to use the timeframe for bar width adjustment

Y axis is electric and gas percentage value will be ploted based on work order number

This is a non-trivial chunk of code you are asking volunteers to write. They keep telling you to try to write it yourself, and then they can review. Show your code.

4 Likes

I have tired one thing passing start and end time in different column but i got a error

I have created x axis with time

You shouldn't need to do this. You are going to have to calculate the start position of each bar relative to width of your chart, so you will need to develop some kind of time ratio relative to the total amount of time in your dataset. The code needed at the beginning of the configure chart extension function will look something like this:

	data = self.data
	width = self.width
	maxTimeValue = data.getRowCount()-1
	chartStartTime = data.getValueAt(0,0)
	chartEndTime = data.getValueAt(maxTimeValue,0)
	totalTime = system.date.minutesBetween(chartStartTime,chartEndTime)
	timeRatio = float(width)/float(totalTime)

The time ratio can then be used to calculate the start positions of each bar based upon the end time of the previous value, and this will be done within the calculateBarW0 definition. It will look something like this:

	barStartTime = data.getValueAt(column,0)
	barStartingLocation = float(system.date.minutesBetween(chartStartTime, barStartTime))*timeRatio

The if statements used to define column positions in my original example will need to be deleted. They were only necessary because the numbers I used were arbitrary and uncalculated. To calculate the bar width, some sort of scale parameter will have to be developed. I imagine that that calculation will look something like this:

	barEndTime = data.getValueAt((column + 1), 0
	barScaleParameter = float(system.date.minutesBetween(barStartTime, barEndTime))*scaleParameter

Although looking at the untested code above, I can foresee that at some point an out of bounds exception is going to have to be handled in some way.

As pturmel and lrose indicated, this is not trivial development, and it will require quite a bit of time and experimentation to perfect. I find this problem interesting, so I'm inclined to mess around with it, and if I find the time, I probably will, but unfortunately, I don't have that kind of time today. If I do get some time to experiment, and I learn something worth sharing, I will post it here. When you figure this out, please do the same.

1 Like

I did find some time to play around with this, and I applied the ideas I listed in the previous post to the example I gave above using a modified version of your dataset. I discovered that some padding is necessary to get the timestamps to render properly.

Here is the full code:
def configureChart(self, chart):
	from org.jfree.chart.renderer.category import BarRenderer, StandardBarPainter
	from java.awt import Color
	chart.setTitle("Histogram")
	padding = self.width * .25
	data = self.data
	width = self.width - padding
	maxTimeValue = data.getRowCount()-1
	chartStartTime = data.getValueAt(0,0)
	chartEndTime = data.getValueAt(maxTimeValue,0)
	totalTime = system.date.minutesBetween(chartStartTime,chartEndTime)
	timeRatio = float(width)/float(totalTime)
	scaleRatio = float(self.width)*0.018
	class DynamicWidthRenderer(BarRenderer):
		def	calculateBarW0(BarRenderer, plot, orientation, dataArea, domainAxis, state, row, column):
			barStartTime = data.getValueAt(column,0)
			barStartingLocation = float(system.date.minutesBetween(chartStartTime, barStartTime))*timeRatio + padding * .5
			try:
				barEndTime = data.getValueAt((column + 1), 0)
				barScaleParameter = float(system.date.minutesBetween(barStartTime, barEndTime))*scaleRatio
			except:
				barScaleParameter = float(width + padding - barStartingLocation)*scaleRatio
			dataArea.width = barScaleParameter
			BarRenderer.calculateBarWidth(plot, dataArea, 0, state)
			return barStartingLocation			
		def getItemPaint(BarRenderer, row, column):
			if column % 2 == 0:
				return Color.orange
			else:
				return Color.yellow
	chart.getCategoryPlot().setRenderer(DynamicWidthRenderer())
	chart.getCategoryPlot().getRenderer().setBarPainter(StandardBarPainter())
	chart.getCategoryPlot().getRenderer().setShadowVisible(False)

The scale calculation needs to be developed a little further, but the arbitrary number I stuck in there gets the bars pretty close in my designer.

The next step for your usage case will probably be to develop a label generator to add the work order numbers and time stamps to the bar chart. Just read the java doc I linked to and experiment with the methods listed.

It will probably be easiest to put your full dataset in a custom propery, and then just access or move the data from that dataset as needed to render the chart. In the example above, I only used two columns from your dataset to render the chart:
image

This was the result:

Shrinking the width of the chart and adding/removing bars was also tested with positive results:
image

2 Likes

one question to show work order number in bar. we need to create separate dataset?

No separate dataset is needed. You’re providing the renderer implementation so you get to determine how it processes the provided dataset.

This question was actually addressed earlier in the thread.

1 Like

Yaa i understood i will try to implement that one

I have tired your same dataset but i am getting chart like this

Its it something i missing?

I have created category axis for X axis and y axis is number axis

and chart type is category chart

i have checked the console i am not getting any error

If the main dataset in a custom property, then it can be called in the same way the data is called from the chart's rendering dataset. For example, if the custom property is called rawData and the work order numbers are in the second column from the left [column index 1], then the script to access the first row of data would be:
self.rawData.getValueAt(0,1).

I like the idea of building a string for the label. It would be something like:

label =  'WO# ' + str(self.rawData.getValueAt(column,1)) + '\n' + str(system.date.format(barStartTime, "MM/dd/yy HH:mm:ss")) + '\nto\n' + str(system.date.format(barEndTime, "HH:mm:ss"))

The output should look like this:
image
I haven't worked on this at all, so I'm not sure if html would be applicable, but if so, then the code would probably look like this instead:

label =  '<html><center>WO# ' + str(self.rawData.getValueAt(column,1)) + '<br>' + str(system.date.format(barStartTime, "MM/dd/yy HH:mm:ss")) + '<br>to<br>' + str(system.date.format(barEndTime, "HH:mm:ss"))

It looks like the code is working properly. You will probably want to use a print statement to troubleshoot your starting positions:

print str(column) + ', '+ str(chartStartTime) + ', '+ str(barStartTime) + ', '+ str(barStartingLocation)

This should help you identify any meridian reciprocations or other datetime anomalies in your dataset. Also, that scale ratio I threw in there was based on what worked with my dataset and not any reasonable algorithm. It will almost certainly need to be experimented with and developed further.

1 Like

I played around with this a little more this morning, and came up with a more flexible approach to changing the tick labels.

First, I added the following method to the beginning of the configureChart extension function to hide the stock labels and create a set of new labels to take their place:

	def dynamicLabelCreator(self, chart, rawData):
		domainAxis = chart.getCategoryPlot().getDomainAxis()
		for row, category in enumerate(chart.getCategoryPlot().getCategories()):
			for component in self.getComponents():
					if isinstance(component, PMILabel) and component.name == category:
						self.remove(component)
			domainAxis.setTickLabelPaint(category, Color(0,0,0,0))
			customLabel = PMILabel()
			labelName = category
			customLabel.setName(labelName)
			customLabel.setVisible(True)
			self.add(customLabel)
	rawData = self.rawData
	dynamicLabelCreator(self, chart, rawData)

Then, I set the stock labels to a 90 degree orientation to give myself more room at the bottom of the chart, and while experimenting, I found that the bars rendered better if I set the lower margin of the category axis to .1

Finally, I modified the calculateW0 method to set the labels based on the center of the bar minus 1/2 the width of the label. Initially, the result looked good, but as I added more bars, the labels began to overlap:


Consequently, I reduced the number of line breaks, and changed the rotation of the labels. I also decided to go ahead and employ an innate label generator to put the actual values on top of the bars:

Below is the full code. I am certain this will still require quite a bit of tweaking, changing, and refactoring to produce an acceptable product for your usage case, but there should be more than enough directions here to go off of. Good Luck!

Full Code:
def configureChart(self, chart):
	from com.inductiveautomation.factorypmi.application.components import PMILabel
	from java.awt import Color
	from org.jfree.chart.renderer.category import BarRenderer, StandardBarPainter
	from java.text import NumberFormat
	from org.jfree.chart.labels import StandardCategoryItemLabelGenerator
	def dynamicLabelCreator(self, chart, rawData):
		domainAxis = chart.getCategoryPlot().getDomainAxis()
		for row, category in enumerate(chart.getCategoryPlot().getCategories()):
			for component in self.getComponents():
					if isinstance(component, PMILabel) and component.name == category:
						self.remove(component)
			domainAxis.setTickLabelPaint(category, Color(0,0,0,0))
			customLabel = PMILabel()
			labelName = category
			customLabel.setName(labelName)
			customLabel.setVisible(True)
			customLabel.rotation = 290
			self.add(customLabel)
	rawData = self.rawData
	dynamicLabelCreator(self, chart, rawData)	
	chart.setTitle("Histogram")
	data = self.data
	padding = self.width * .25
	width = self.width - padding
	maxTimeValue = data.getRowCount()-1
	chartStartTime = data.getValueAt(0,0)
	chartEndTime = data.getValueAt(maxTimeValue,0)
	totalTime = system.date.minutesBetween(chartStartTime,chartEndTime)
	timeRatio = float(width)/float(totalTime)
	scaleRatio = float(self.width)*0.018
	class DynamicWidthRenderer(BarRenderer):
		def	calculateBarW0(BarRenderer, plot, orientation, dataArea, domainAxis, state, row, column):
			from com.inductiveautomation.factorypmi.application.components import PMILabel
			barStartTime = data.getValueAt(column,0)
			barStartingLocation = float(system.date.minutesBetween(chartStartTime, barStartTime))*timeRatio + padding * .5
			customLabel = self.getComponent(column)
			try:
				barEndTime = data.getValueAt((column + 1), 0)
				barScaleParameter = float(system.date.minutesBetween(barStartTime, barEndTime))*scaleRatio
				customLabel.text = '<html><b><center>WO# ' + str(self.rawData.getValueAt(column,1)) + '<br>' + str(system.date.format(barStartTime, "MM/dd HH:mm:ss")) + '<br>to ' + str(system.date.format(barEndTime, "HH:mm:ss"))
				labelOffset = customLabel.width * .5
				labelLocation = barStartingLocation+((float(system.date.minutesBetween(barStartTime, barEndTime))*.5)*timeRatio)-labelOffset
			except:
				barScaleParameter = float(width + padding - barStartingLocation)*scaleRatio
				customLabel.text = '<html><b><center>WO# ' + str(self.rawData.getValueAt(column,1)) + '<br>' + str(system.date.format(barStartTime, "MM/dd HH:mm:ss")) + '<br>to ' + '[...]'
				labelOffset = customLabel.width * .5
				labelLocation = barStartingLocation - (.5 * labelOffset)
			heightJustification = float(len(chart.getCategoryPlot().getCategories()[column]))*9
			customLabel.setLocation(int(labelLocation), self.height - int(heightJustification))
			customLabel.setSize(250,250)		
			dataArea.width = barScaleParameter
			BarRenderer.calculateBarWidth(plot, dataArea, 0, state)
			return barStartingLocation			
		def getItemPaint(BarRenderer, row, column):
			if column % 2 == 0:
				return Color.blue
			else:
				return Color.yellow
	chart.getCategoryPlot().setRenderer(DynamicWidthRenderer())
	numberFormat = NumberFormat.getInstance()
	defaultFormat = StandardCategoryItemLabelGenerator.DEFAULT_LABEL_FORMAT_STRING
	labelGenerator = StandardCategoryItemLabelGenerator(defaultFormat, numberFormat)
	chart.getCategoryPlot().getRenderer().setBaseItemLabelGenerator(labelGenerator)
	chart.getCategoryPlot().getRenderer().setBaseItemLabelsVisible(True)
	chart.getCategoryPlot().getRenderer().setBarPainter(StandardBarPainter())
	chart.getCategoryPlot().getRenderer().setShadowVisible(False)
1 Like

@justinedwards.jle @lrose

I tired using area chart instead of bar chart. width of the bar is adjusting ok now

but line chart showing one hour difference from the actula time

I have attached my project file for your reference
can you please correct which place i am doing worng
BarWidth.zip (16.1 KB)

I don't see anything wrong with the way the data is actually displayed. The problem is the xTrace is getting its date from the bar dataset, which is actually accurate for the bar that the trace is over even if it's not accurate for the depicted domain position. Is there no way to correlate the dates between the two datasets?

Hi

I think data is populated correct only. I have checked with Mark mode. and checked the timing. It shows correct time

one more question

image

i have another dataset with WO number and i have populate the wO numbers in Bar chart

Note - both dataset timing are same only

I want to populate like this

You could experiment with adding a custom label generator. Expanding upon the configureChart extension function code that you are using to create that border, the code will end up looking something like this:

#def configureChart(self, chart):
	from java.awt import BasicStroke, Color
	from org.jfree.chart import labels
	from org.jfree.chart.renderer.xy import XYStepAreaRenderer
	
	#This will be your WO datset, so correct the path accordingly
	labelData = self.parent.getComponent('Power Table 3').data
	
	class CustomLabelGenerator(labels.XYItemLabelGenerator):
		def generateLabel(self, dataset, series, item):
			woNum = labelData.getValueAt(item, 1)
			label = str(woNum)
			return label
	plot = chart.plot
	plot.setOutlineStroke(BasicStroke(1))
	plot.setOutlinePaint(Color.black)
	renderer = plot.renderer
	generator = CustomLabelGenerator()
	renderer.setSeriesItemLabelGenerator(0, generator)
	renderer.setSeriesItemLabelsVisible(0, True)

That chart is going to have multiple renderers, so it is possible that you will need to build a function that finds the specific renderer that you want to set the label generator to. It will look like this:

from org.jfree.chart.renderer import #The renderer you are looking for
def getRenderer(plot):
	for renderer in range(plot.rendererCount):
		plotRenderer = plot.getRenderer(renderer)
		print plotRenderer
		if isinstance(plotRenderer, #The renderer you are looking for):
			return plotRenderer
plot = chart.plot
renderer = getRenderer(plot)

Since this is an XYPlot, XYText annotations would also be an option. Just convert the date in the dataset to millis for the domain axis location, and use the bar chart value for the range axis location. Here is an example of this that I developed for a status chart:

2 Likes

Hi have tired this script

#def configureChart(self, chart):
	from java.awt import BasicStroke, Color
	from org.jfree.chart import labels
	from org.jfree.chart.renderer.xy import XYStepAreaRenderer
	
	#This will be your WO datset, so correct the path accordingly
	labelData = self.parent.getComponent('Power Table 3').data
	
	class CustomLabelGenerator(labels.XYItemLabelGenerator):
		def generateLabel(self, dataset, series, item):
			woNum = labelData.getValueAt(item, 1)
			label = str(woNum)
			return label
	plot = chart.plot
	plot.setOutlineStroke(BasicStroke(1))
	plot.setOutlinePaint(Color.black)
	renderer = plot.renderer
	generator = CustomLabelGenerator()
	renderer.setSeriesItemLabelGenerator(0, generator)
	renderer.setSeriesItemLabelsVisible(0, True)

this above script adding labels to line chart. Not in area bar

then tired your tooltip script for area bar

but that also not working.

I'm sorry you had trouble figuring out. I was able to adapt the tool tip script to your usage case. Here is the script:

#def configureChart(self, chart):
	from java.awt import BasicStroke, Color
	from org.jfree.chart.annotations import XYTextAnnotation
	plot = chart.getPlot()
	plot.setOutlineStroke(BasicStroke(1))
	plot.setOutlinePaint(Color.black)
	for annotation in plot.getAnnotations():
		if isinstance(annotation, XYTextAnnotation):
			plot.removeAnnotation(annotation)

	#This will be your WO datset, so correct the path accordingly
	labelData = self.parent.getComponent('Power Table 1').data
	
	barData = self.parent.getComponent('Power Table').data
	for row in range(0, labelData.rowCount - 1):
		text = str(labelData.getValueAt(row, 1))
		startTime = float(barData.getValueAt(row, 0).getTime())
		endTime = float(barData.getValueAt(row + 1 , 0).getTime())
		topBound = float(barData.getValueAt(row, 2))
		bottomBound = float(barData.getValueAt(row, 1))
		x = (startTime + endTime) * .5
		y = (topBound + bottomBound) * .5
		annotation = XYTextAnnotation(text, x, y)
		plot.addAnnotation(annotation)

Here is the result:

1 Like

working perfectly thank you soo much

1 Like

@justinedwards.jle
one issue
last row label is not showing in bar

i changed for loop like this and checked - this error is coming

for row in range(labelData.getRowCount()):

14:40:15.528 [AWT-EventQueue-0] ERROR Vision.ClassicChart - Error invoking extension method.
org.python.core.PyException: java.lang.ArrayIndexOutOfBoundsException: java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
	at org.python.core.Py.JavaError(Py.java:547)
	at org.python.core.Py.JavaError(Py.java:538)
	at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:192)
	at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:208)
	at org.python.core.PyObject.__call__(PyObject.java:494)
	at org.python.core.PyObject.__call__(PyObject.java:498)
	at org.python.core.PyMethod.__call__(PyMethod.java:156)
	at org.python.pycode._pyx171.configureChart$1(<extension-method configureChart>:35)
	at org.python.pycode._pyx171.call_function(<extension-method configureChart>)
	at org.python.core.PyTableCode.call(PyTableCode.java:173)
	at org.python.core.PyBaseCode.call(PyBaseCode.java:306)
	at org.python.core.PyFunction.function___call__(PyFunction.java:474)
	at org.python.core.PyFunction.__call__(PyFunction.java:469)
	at org.python.core.PyFunction.__call__(PyFunction.java:459)
	at org.python.core.PyFunction.__call__(PyFunction.java:454)
	at com.inductiveautomation.vision.api.client.components.model.ExtensionFunction.invoke(ExtensionFunction.java:151)
	at com.inductiveautomation.factorypmi.application.components.PMIChart.createChartImpl(PMIChart.java:481)
	at com.inductiveautomation.factorypmi.application.components.chart.PMILineChartPanel.createChart(PMILineChartPanel.java:135)
	at com.inductiveautomation.factorypmi.application.components.PMIChart.setExtensionFunctions(PMIChart.java:466)
	at com.inductiveautomation.factorypmi.designer.eventhandling.ComponentScriptEditor.applyChanges(ComponentScriptEditor.java:596)
	at com.inductiveautomation.factorypmi.designer.eventhandling.ComponentScriptEditor$4.actionPerformed(ComponentScriptEditor.java:327)
	at java.desktop/javax.swing.AbstractButton.fireActionPerformed(Unknown Source)
	at java.desktop/javax.swing.AbstractButton$Handler.actionPerformed(Unknown Source)
	at java.desktop/javax.swing.DefaultButtonModel.fireActionPerformed(Unknown Source)
	at java.desktop/javax.swing.DefaultButtonModel.setPressed(Unknown Source)
	at java.desktop/javax.swing.plaf.basic.BasicButtonListener.mouseReleased(Unknown Source)
	at java.desktop/java.awt.Component.processMouseEvent(Unknown Source)
	at java.desktop/javax.swing.JComponent.processMouseEvent(Unknown Source)
	at java.desktop/java.awt.Component.processEvent(Unknown Source)
	at java.desktop/java.awt.Container.processEvent(Unknown Source)
	at java.desktop/java.awt.Component.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.Container.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.Component.dispatchEvent(Unknown Source)
	at java.desktop/java.awt.LightweightDispatcher.retargetMouseEvent(Unknown Source)
	at java.desktop/java.awt.LightweightDispatcher.processMouseEvent(Unknown Source)
	at java.desktop/java.awt.LightweightDispatcher.dispatchEvent(Unknown Source)
	at java.desktop/java.awt.Container.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.Window.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.Component.dispatchEvent(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
	at java.desktop/java.awt.EventQueue$5.run(Unknown Source)
	at java.desktop/java.awt.EventQueue$5.run(Unknown Source)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEvent(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.run(Unknown Source)
Caused by: java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
	at com.inductiveautomation.ignition.common.BasicDataset.getValueAt(BasicDataset.java:122)
	at jdk.internal.reflect.GeneratedMethodAccessor480.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
	at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:190)
	... 53 common frames omitted