Vision Chart (not EasyChart) Min & Max Date

This is in Vision and I have 2 components, a Chart (not easychart) and Date Range.

My dataset range from Jan 1st to March 2nd on the Chart component.

Instead of reloading the data every time I change the date range.

I want to bind the start and end date from the Date Range component to the X-Axis properties Max Date and Min Date, so the Chart would only display the range the user pick.

but primary the data would not need to be call every time the range changes. So I wonder if anyone has dynamically changes Max and Mix from the Chart X-Axis properties using a Date Range component.

thanks,

Query the data once, stash it in a custom property, then use scripting to filter your original dataset down to apply to the actual chart component.

Hello @PGriffith
I am not sure if I follow the process, so add the data into a dataset custom property, than write a script to parse the data inside and add to the chart dataset, so it will display?

I am trying to see if this approach would be fast if someone is sliding the date range, which was one of the reasons I was trying to change the chart properties max and min, since the data is already load.

If the dataset is ordered by timestamp, it's quite efficient to 'filter' it by timestamp. You could use system.dataset.deleteRows, or Phil's Simulation Aids' view function, or convert to a PyDataset and slice; there's lots of possible approaches. The key thing is that by having the data in memory, you're avoiding the significantly slower network transfer of data. Slicing a dataset in-memory is much, much faster than issuing a new query to the DB.

@PGriffith the dataset will be order by timestamp and I don't want to delete, since the user will be able to slide the date range back in time or forward or increase to see all the data.

which one of the reason I start to explore the Max and Min properties, since it can change the view range on the chart and the dataset doesn't change.

I will try to figure out how to get to that property and see if I can make that work.

thanks for the help so far.

I'm suggesting you don't modify the chart properties at all.

Let's say you have a containing group/template/container/whatever. Within it you have your Chart component, and your Date Range.
Have a custom property on the group that runs your tag history query with the hardcoded start and end date. Cache the entire dataset in a custom property.
Then on your date range, use a script so that whenever the date range is changed, your 'filtering' script will kick off. This will copy all the rows from the cached dataset into a new dataset on the chart component. The original dataset is never changed; but a new dataset is created and applied directly to the chart.

Changing the min/max properties of the component is probably possible, but requires diving into JFreeChart's API.

If this is really what you want to do, here is how:

Add the following script to your Date Range component's propertyChange event handler:

from org.jfree.chart.axis import DateAxis
chartComponent = event.source.parent.getComponent('Chart')#Change this to the actual path of your chart
plot = chartComponent.chart.plot
dateAxis = DateAxis()
if event.propertyName == 'selectedDateRange':
	minDate = event.source.startDate
	maxDate = event.source.endDate
	dateAxis.setRange(minDate.getTime(), maxDate.getTime())
	plot.setDomainAxis(dateAxis)
2 Likes

@justinedwards.jle thanks for the info, I try that and it work great.

@PGriffith I originally want go with the option @justinedwards.jle recommend, since we have several thousands of data points, possible hundred of thousands. and have to parse and copy and paste every time, I believe it will not be efficient.

But I still going to try both scenarios and add timers to see which one is faster.

thanks again for all the help, also have a small second question if any of you might know.

I set Chart component mode to X-Trace, and I want change the background color on the label, by any chance any of you guys might know are that property is?

below is a picture which the graph, the way it look is hard to ready the labels
image

1 Like

Pretty sure you're stuck with that. Among other things, that is a behavior that I fixed when I re-implemented X-Trace in my NoteChart module.

A simple option would be to experiment with annotation fonts.

Example: #using the configureChart extension function

#def configureChart(self, chart):
	from java.awt import Font
	plot = chart.plot
	font = Font('Verdana Bold', Font.BOLD, 18)
	plot.setAnnotationFont(font)

Result:
image

A problem I just worked on gave me the idea of building my own annotation generator to produce the effect you are looking for. EDIT: a slightly more developed version was posted here that prevents label overlap

image

image

To get this effect, add the following script to the chart's getXTraceLabel extension function:

getXTraceLabel extension function
#def getXTraceLabel(self, chart, penName, yValue):
	return ''#This prevents the stock annotations from being visible

Then, add the following script to the chart's propertyChange event handler:

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
	data = event.source.Data #or whatever your custom data property is
	plot = event.source.chart.plot
	domainAxis = plot.domainAxis
	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
		domainLocation = plot.domainCrosshairValue
		backgroundFont = Font(Font.MONOSPACED, Font.PLAIN, 18)
		annotationFont = Font(Font.MONOSPACED, Font.BOLD, 18)
		for row in range(data.rowCount):
			if data.getValueAt(row, 0).getTime() == domainLocation:
				for column in range(1, data.columnCount):
					penName = data.getColumnName(column)
					yValue = data.getValueAt(row, column)
					text = penName + ': ' +str(yValue)
					background = XYTextAnnotation(u'█' * len(text), domainLocation, yValue)
					background.setPaint(Color.WHITE)
					background.setFont(backgroundFont)
					plot.addAnnotation(background)
					annotation = XYTextAnnotation(text, domainLocation, yValue)
					textColor = plot.renderer.getSeriesPaint(column-1)
					annotation.setPaint(textColor)
					annotation.setFont(annotationFont)
					plot.addAnnotation(annotation)
					#The following hocus pocus is needed to keep the label within the bounds of the chart when the selected x position is near the edge:
					domainWidth = domainAxis.upperBound - domainAxis.lowerBound
					midPoint = domainAxis.lowerBound + (domainWidth * .5)
					chartWidth = event.source.width
					layout = TextLayout(text, annotationFont, FontRenderContext(None, False, False))
					pixelOffset = layout.getBounds().width *.5
					offsetRatio = pixelOffset/float(chartWidth)
					domainOffset = domainWidth * offsetRatio
					if domainLocation > midPoint:
						annotation.setX(domainLocation - domainOffset)
						background.setX(domainLocation - domainOffset)
					else:
						annotation.setX(domainLocation + domainOffset)
						background.setX(domainLocation + domainOffset)

The chart's XYTextAnnotations do not have a background paint property, so this script simply adds two annotations for every datapoint: one with a solid row of rectangle characters to simulate a background color, and the other with the actual desired label text.

1 Like

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

@justinedwards.jle this is super nice, I tested and works really well.

thanks again for the help :slight_smile:

1 Like