How to trend the future

Good morning guys,

I’m searching in the old topics but I can’t find anything. I would like to know if is possibile to show in a chart future values that I’m predicting.

I think maybe I could create a dataset with values and timestamp but I have no idea how to put them in a trend or even if this is the right way to do it.

Thanks for the help.

Sure you can do this. If you are using current data to then predict future values then you would write a script to first get your current data, then run your prediction algorithm to get new values, append these to the current dataset (if you want to show current trend along side the predictions) and set the data property of the chart to your new dataset. If you have some sample data or details I can help you get started if needed.

3 Likes

Thanks for your reply!

Actually I would like to do some test to see if is possible to do it, because the data will be available not in ignition but from an external source, I should just manage them to insert in the chart with a script I guess as you say.

I spent some time to figure out how to do what you suggest, I see that using a component “Chart” I can insert the dataset in the custom property and automatically I see them in the Chart Customizer page, I would like to ask if should be possibile doing something like this with an “Easy Chart”.

Thanks again for the help.

Any ideas about how to pass custom dataset to an Easy Chart?

Thanks for the help guys

Your problem has little to do with the chart. It is all to do with the dataset. Forget about the chart and concentrate on your data.

You said you have an external source. You need to figure out how to connect to that. Go to your gateway, Config, Databases, Connections, Create new database connection and see if you can create a connection to your external database.

If you succeed with that then you need to decide if a Named Query will suit (best option) or a scripted query will suit better. Either way you need to write a query using the database connection created above.

You cannot.

Use the classic chart for this use-case.

This is not a problem for me, there is a person who care about it with REST calls. I will have the data and together we will figure out how to create the dataset to pass to the Chart (not the easy chart at this point).

I wonder is with Chart should be possibile create a trend exactly like this, with lines and dot with a number inside. Do you have any idea how should be possibile to do something like it?

Ok thanks for the help!

Hmm. Haven’t seen that one. But it should be possible with a custom renderer thanks to Ignition using the JFreeChart API in Vision.

Some topics to review for ideas:

https://forum.inductiveautomation.com/search?q=org.jfree.chart.renderer

1 Like

pturmel, thanks for the input.

In this days we’ve tried to figure out how to make the chart as I showed but we cannot find a way to do it.

For example, we have found a couple of topic that talk about this and we have made some tests with this code:

	from org.jfree.chart.labels import StandardXYItemLabelGenerator

	
	class MyLabelGenerator(StandardXYItemLabelGenerator):
		def generateLabel(self, xyds, series, item):
			return "s%dp%d" % (series, item)
	

	renderer = chart.plot.renderer
	renderer.setItemLabelGenerator(MyLabelGenerator())
	renderer.setBaseItemLabelsVisible(True)

and this

	from org.jfree.chart.renderer.xy import XYDotRenderer	
	
	plot = chart.getXYPlot()
	
	dotRenderer = XYDotRenderer()
	dotRenderer.setDotWidth(10)
	dotRenderer.setDotHeight(10)
	
	
	renderer = chart.plot.renderer
	renderer.setItemLabelGenerator(MyLabelGenerator())
	renderer.setBaseItemLabelsVisible(True)			
	plot.setRenderer(dotRenderer)	

If I put the code individually in the Extension Functions of the Chart they work, but together like this they don’t

	from org.jfree.chart.labels import StandardXYItemLabelGenerator

	
	class MyLabelGenerator(StandardXYItemLabelGenerator):
		def generateLabel(self, xyds, series, item):
			return "s%dp%d" % (series, item)
	

	renderer = chart.plot.renderer
	renderer.setItemLabelGenerator(MyLabelGenerator())
	renderer.setBaseItemLabelsVisible(True)
	
	
	
	from org.jfree.chart.renderer.xy import XYDotRenderer	
	
	plot = chart.getXYPlot()
	
	dotRenderer = XYDotRenderer()
	dotRenderer.setDotWidth(10)
	dotRenderer.setDotHeight(10)
	
	
	renderer = chart.plot.renderer
	renderer.setItemLabelGenerator(MyLabelGenerator())
	renderer.setBaseItemLabelsVisible(True)			
	plot.setRenderer(dotRenderer)	

How could be possible to make them work together?

In the case it works, I would like to ask you how to create the chart using this dataset:

  • the X axis should be the t_stamp
  • the Y axis should be the n_aut (so in this way I have a horizontal line)
  • and the label of the point should be fase

Thanks for the help!

I’m not entirely sure how the dataset you’ve given translates into the output that you are wanting.

  1. I believe that you should be sub-classing XYLineAndShapeRenderer, because you want to draw both the line and the shape.
  2. Because you will always be drawing a horizontal line, there is no need to provide the n_aut data, that is just how the renderer will work given the data that is needed for the dataset.
  3. In the image you provided there are 4 lines each with 2 data points. Will the data points always come in pairs? The dataset you provided has an odd number of entries so I am unsure.
  4. What determines if the data point is filled or not? Is this the same thing that determines if the line is dashed?

Thanks for the help Irose.

Ok for the first 2 points.
3: the image is just an example, the lines will be continued. They will start from 0 to 6 and then go back to 0 linearly.
4. Actually this is not so important, doesn’t matter if is filled or not, we just need to show the dots when I have the dataset and the line to connect them.

Thanks again

I’m playing with the chart and setting the datasets like this:
**Renderer = XY Line/Shape Renderer **
Type = Lines and Shapes
Line Size = 1
Shape Offset = 50

I have the chart as I wish. The problem is always to show the “third” columns where I have “fase”, with the org.jfree.chart.labels I really can’t figure out how to show that column.

Using the script like this

` from org.jfree.chart.labels import StandardXYItemLabelGenerator

class MyLabelGenerator(StandardXYItemLabelGenerator):
	def generateLabel(self, xyds, series, item):
		return "s%dp%d" % (series, item)


renderer = chart.plot.renderer
renderer.setItemLabelGenerator(MyLabelGenerator())
renderer.setBaseItemLabelsVisible(True) ` 

I’ve the result as in picture and in any case, it works only with the first dataset, the others are not labelled

Thanks again for the help guys

You have to read the column from dataset.

I should have a little free time today, I will see if I can work up an example.

Ok, thank you very much for the help, I really appreciate it.

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.

2 Likes

Thank you so much for the help, it looks great!

I need to ask you one thing to work on it… We’re getting crazy trying to understand how to manage the other datasets of the same chart (the script seems to work only with the first dataset of the chart).

We have made some tests and it looks like the dataset that the script pick up is set here:

		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)

because if we make a print dataset the Console return the name of the first dataset.
How is possible to call the other datasets?

Thanks again for your help and your patience.

The magic actually happens here for the line:

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)

And here for the Items:

transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation)
transY1 = rangeAxis.valueToJava2D(series + 1, dataArea,yAxisLocation)

Usually a renderer would use code like y1 = dataset.getYValue(series,item) in order to pull the actual data value from the dataset and draw the item at that location on the chart. And in fact that is exactly what I did to get that value to draw the label

text = str(int(dataset.getYValue(series,item))

So now it becomes a question of how you want to manage this, which should be determined by how your data is received. Hopefully you notice that I am drawing the line based on the series + 1. Since, the series is 0 based, this results in the first series being drawn at 1 and each subsequent series will be drawn at a value higher than that. So if you add another series (e.g. value column) to the dataset, using this code you should see it drawn above the previous.

The way that the JFreeChart is intended to work is that each dataset is assigned a renderer, so if you want to manage this via multiple datasets then you will need to modify the code to assign the custom renderer to each dataset.

This is fairly easily done, however, this change means that we also have to modify how the line for each dataset is drawn. It will not work as you expect without modification because every dataset will have 1 series and they will be drawn on top of each other.

There are other ways to do what you want, but it depends on how you want to manage the data.

For multiple datasets the following changes will need to be made:

  1. My previous code did not account for the scale of the range axis being different than what would typically be used. So we need to address that by setting the range.
rangeAxis = plot.getRangeAxis()
rangeAxis.setAutoRange(False)
rangeAxis.setRange(0,plot.getDatasetCount() + 1)
  1. Since there will be multiple datasets we need to set the renderer for each dataset. This code assumes that you will always want all datasets to use the same custom renderer.
for dataset in range(plot.getDatasetCount() + 1):
	rend = CustomXYLineAndShapeRenderer()
	rend.setSeriesItemLabelsVisible(0,True)
	rend.setSeriesItemLabelPaint(0,Color.black)
	plot.setRenderer(dataset,rend)
  1. Modify the renderer’s draw functions
#in the drawPrimaryLine function you will need to modify the code for transY0 and transY1
transy0 = rangeAxis.valueToJava2D(plot.getIndexOf(self) + 1, dataArea,yAxisLocation)
transY1 = rangeAxis.valueToJava2D(plot.getIndexOf(self) + 1, dataArea,yAxisLocation)

#in the drawSecondaryPass function you will need to modify the code for transY1 and the call to updateCrosshairValues
transY1 = rangeAxis.valueToJava2D(plot.getIndexOf(self) + 1, dataArea,yAxisLocation)
self.updateCrosshairValues(crosshairState, x1, plot.getIndexOf(self) + 1,domainAxisIndex,rangeAxisIndex,transX1,transY1,orientation)

Make these changes to the code and the result could look something like this (NOTE: I just copied the original dataset and changed 1 value to insure everything was working as expected):

2 Likes

Thank you very much! This is really helpful, now we’re making some update to show the values we need!

Thanks again for the help!

1 Like