# Improvements For Vision Charts

**URL:** <https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886>\
**Category:** Ignition\
**Tags:** ignition79, scripting, vision\
**Created:** [March 19, 2025, 1:10pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886 "2025-03-19T13:10:51Z")\
**Posts on this page:** 16\
**Page:** 1

<div class="post-metadata">

**Author:** ![jacob.leonard](https://avatars.discourse-cdn.com/v4/letter/j/da6949/32.png) [@jacob.leonard](https://forum.inductiveautomation.com/u/jacob.leonard)\
**Post date:** [March 19, 2025, 1:10pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/1 "2025-03-19T13:10:51Z")

</div>

![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/0/c/0ccff1646a6720ada0152b04e3c79e816c53ae0d.png)  
I am trying to build some statistical process control (SPC) charts, and I came up with this screen. It is plotting true position on a part we manufacture. The line is the last 5 positions measured.

What I would like to do is show the progression of the parts as it wanders around the graph. Is it possible to change the color of the points as it progresses in the series? or change out the shapes to numbers? AND increment them for each point?

---

<div class="post-metadata">

**Author:** ![lrose](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/lrose/32/62724_2.png) [@lrose](https://forum.inductiveautomation.com/u/lrose)\
**Post date:** [March 19, 2025, 1:23pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/2 "2025-03-19T13:23:22Z")

</div>

> [@jacob.leonard](#):
>
> Is it possible to change the color of the points as it progresses in the series?

It is possible, but would be very script intensive, as you would have to write your own subclass and rendering code.

> [@jacob.leonard](#):
>
> change out the shapes to numbers?

This is probably a bit easier, but I'm not certain at the moment.

I don't have much time at the moment to work something up, but I am sure there is a solution.

Perhaps, I can nerd snipe @justinedwards.jle this seems right up his alley.

---

<div class="post-metadata">

**Author:** ![justinedwards.jle](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/justinedwards.jle/32/37854_2.png) [@justinedwards.jle](https://forum.inductiveautomation.com/u/justinedwards.jle)\
**Post date:** [March 19, 2025, 7:06pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/3 "2025-03-19T19:06:32Z")

</div>

> [@lrose](#):
>
> Perhaps, I can nerd snipe @justinedwards.jle

I would love to help, but is this an actual chart, or just a mock up? If it's an actual chart, can you provide an example version of this in an exported window, so I won't have to build it from scratch to play around with it? Make sure to white wash the example of any sensitive information before uploading it. I'm off today and tomorrow, but I'm nevertheless a little swamped with things I need to get done, so I won't have much time to look at it.

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [March 19, 2025, 7:21pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/4 "2025-03-19T19:21:20Z")

</div>

Do you have always have a well defined sampling range? As in, +/- 0.003 on the domain, 0.187 - 0.193 for the range, always a fixed 5 or however many points?

It might be "easier" (by some stretch of the word) to script the entire display using a paintable canvas component, rather than shoehorning this functionality into the chart.

---

<div class="post-metadata">

**Author:** ![jacob.leonard](https://avatars.discourse-cdn.com/v4/letter/j/da6949/32.png) [@jacob.leonard](https://forum.inductiveautomation.com/u/jacob.leonard)\
**Post date:** [March 19, 2025, 8:01pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/5 "2025-03-19T20:01:05Z")

</div>

It is an actual chart. Sadly, it is a template in a much larger project. Is there a way to just export just a vision template? if not, then I have the feeling that it isn't worth the trouble to scrub everything out.

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [March 19, 2025, 8:05pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/6 "2025-03-19T20:05:34Z")

</div>

> [@jacob.leonard](#):
>
> Is there a way to just export just a vision template?

Yes, when exporting from the designer, you can choose precisely what to export.

---

<div class="post-metadata">

**Author:** ![jacob.leonard](https://avatars.discourse-cdn.com/v4/letter/j/da6949/32.png) [@jacob.leonard](https://forum.inductiveautomation.com/u/jacob.leonard)\
**Post date:** [March 19, 2025, 8:06pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/7 "2025-03-19T20:06:23Z")

</div>

For these datapoints, yes, but i was hoping to use this template on other points with different tolerances.

Yea, I think that i am at the limit of what vision can do without reinviting core parts of it. Doing it in a canvas might be the way to do what i want to do, but i dont think that i am willing to make that jump

---

<div class="post-metadata">

**Author:** ![justinedwards.jle](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/justinedwards.jle/32/37854_2.png) [@justinedwards.jle](https://forum.inductiveautomation.com/u/justinedwards.jle)\
**Post date:** [March 19, 2025, 9:59pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/8 "2025-03-19T21:59:09Z")

</div>

> [@jacob.leonard](#):
>
> was hoping to use this template on other points with different tolerances

I don't see this as a problem for the canvas. The tick labels could be painted using `graphics.drawString` using the `monospaced` font argument to simplify right alignment on the range axis. Everything else is just a simple matter of scaling the plot area pixels to the given range and domain.

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [March 19, 2025, 11:28pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/9 "2025-03-19T23:28:24Z")

</div>

I have some issues with the way this is coded, but I'll give credit to Gemini - I gave it your screenshot in the title as input and some prompting, and got this _first try_. I had to add three lines at the beginning to get it to work properly inside the component, but it's pretty impressively cohesive output:

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/f/c/fc4e4da1722bc060ed3a871e00c18480b818abe4.png)

The code I have is here, which is just rendering a custom dataset property on the component with X and Y double columns:

```python
# Import necessary Java classes
from java.awt import Color, BasicStroke, Font, RenderingHints
from java.awt.geom import Ellipse2D, Line2D
import math

width = event.width
height = event.height
g = event.graphics

# --- Configuration (Easily customizable) ---
dataset = event.source.data # Get the dataset from a custom property named 'data'
chart_title = "1 O'CLOCK" # The name of the chart
x_column = "X" # Name of the X-coordinate column in the dataset
y_column = "Y" # Name of the Y-coordinate column in the dataset
circle_diameter = min(width, height) * 0.8 # Make chart fit within canvas
x_center = width / 2.0
y_center = height / 2.0
x_min = -0.003 # Fixed X-axis range from the image
x_max = 0.003 # Fixed X-axis range from the image
y_min = 0.1870 # Fixed Y-axis range from the image
y_max = 0.1930 # Fixed Y-axis range from the image
num_y_ticks = 13 # Number of Y-axis tick marks
font_size = 10
font_family = "Dialog"

# --- Pre-calculations ---
x_range = x_max - x_min
y_range = y_max - y_min
x_scale = circle_diameter / x_range
y_scale = circle_diameter / y_range
num_points = dataset.rowCount

# --- Antialiasing (for smoother lines and text) ---
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)

# --- Draw the circle ---
g.setColor(Color.BLUE)
g.setStroke(BasicStroke(1.5)) # Thicker stroke for the circle
circle_x = x_center - circle_diameter / 2.0
circle_y = y_center - circle_diameter / 2.0
g.draw(Ellipse2D.Double(circle_x, circle_y, circle_diameter, circle_diameter))

# --- Draw axes ---
g.setColor(Color.BLUE)
g.setStroke(BasicStroke(1.0))
# X-axis
g.draw(Line2D.Double(circle_x, y_center, circle_x + circle_diameter, y_center))
# Y-axis
g.draw(Line2D.Double(x_center, circle_y, x_center, circle_y + circle_diameter))

# --- Draw Y-axis tick marks and labels ---
g.setFont(Font(font_family, Font.PLAIN, font_size))
for i in range(num_y_ticks):
    y_tick_value = y_min + (i * y_range / (num_y_ticks - 1.0))
    y_tick_pixel = y_center + (y_tick_value - (y_min + y_range / 2.0)) * -y_scale # Invert for screen coordinates
    g.draw(Line2D.Double(x_center - 5, y_tick_pixel, x_center + 5, y_tick_pixel)) # Tick marks
    g.drawString("%.4f" % y_tick_value, x_center + 8, y_tick_pixel + 4) # Labels

# --- Draw X-axis tick marks and labels ---

x_tick_values = [-0.003, -0.002, -0.001, 0.000, 0.001, 0.002, 0.003] #Defined list of tick values
for x_tick_value in x_tick_values:
    x_tick_pixel = x_center + (x_tick_value - (x_min + x_range / 2.0)) * x_scale # Calculate the position
    g.draw(Line2D.Double(x_tick_pixel, y_center-5, x_tick_pixel, y_center + 5)) # Tick marks
    g.drawString("%.3f" % x_tick_value, x_tick_pixel-10, y_center + 18) # Labels

# --- Plot the data points ---
last_x_pixel = None
last_y_pixel = None

for i in range(num_points):
    x_value = dataset.getValueAt(i, x_column)
    y_value = dataset.getValueAt(i, y_column)

    # Check for valid data (not None or NaN)
    if x_value is not None and y_value is not None and not math.isnan(x_value) and not math.isnan(y_value):

        # Scale the data to pixel coordinates
        x_pixel = x_center + (x_value - (x_min + x_range / 2.0)) * x_scale
        y_pixel = y_center + (y_value - (y_min + y_range / 2.0)) * -y_scale # Invert Y-axis

        # --- Color gradient (Red -> Green -> Blue) ---
        if num_points > 1:
            hue = i * (120.0 / (num_points - 1)) # 0=red, 120=green, 240=blue, 360=red (again)
            color = Color.getHSBColor(hue / 360.0, 1.0, 1.0) # HSBtoRGB conversion
        else:
            color = Color.GREEN #If only one point, color it green.

        g.setColor(color)
        g.fill(Ellipse2D.Double(x_pixel - 3, y_pixel - 3, 6, 6)) # Make point a 6x6 circle

        # Draw line to previous point (except for the first point)
        if last_x_pixel is not None and last_y_pixel is not None:
            g.setStroke(BasicStroke(1.0)) #stroke for line between points.
            g.draw(Line2D.Double(last_x_pixel, last_y_pixel, x_pixel, y_pixel))

        # --- Label (optional - shows the row index) ---
        #g.drawString(str(i), x_pixel + 5, y_pixel) # Add point index as label (commented out)

        last_x_pixel = x_pixel
        last_y_pixel = y_pixel

# --- Draw chart title ---
g.setColor(Color.BLACK) # Title color
g.setFont(Font(font_family, Font.BOLD, 14)) # Larger, bold font for the title
title_width = g.getFontMetrics().stringWidth(chart_title)
g.drawString(chart_title, x_center - title_width / 2.0, circle_y - 10) # Position above circle

```

The full prompt I gave it was this (plus the screenshot):

> I'm working in Inductive Automation's Ignition software. I want to write a Jython script that will run on the paintable canvas component, which provides direct access to Java Swing's graphics2d primitives to allow drawing arbitrary 2d shapes. Given an Ignition dataset on a component custom property as input, with a set of rows identifying X and Y values, can you replicate one of the charts in the attached image. The end user also wants to distinguish, via a sliding color scale and/or marker text, the order the points are drawn, which will correspond to the row index in the input dataset. Provide the script to run in the paintable canvas, as well as a sample CSV dataset of inputs.

And the full reply:

> **Gemini Output**
>
> Here's a Jython script for Ignition's Paintable Canvas, along with a sample CSV dataset and explanations. I've focused on recreating the general look and functionality of one of the charts (e.g., the "1 O'CLOCK" chart), including the scaling, axes, labels, and point plotting with sequential coloring. I've added some extra features for better usability within Ignition.
> 
> ```python
> # Import necessary Java classes
> from java.awt import Color, BasicStroke, Font, RenderingHints
> from java.awt.geom import Ellipse2D, Line2D
> import math
> 
> def paint(g, width, height, component):
> # type: (Graphics2D, int, int, JComponent) -> None
> 
> # --- Configuration (Easily customizable) ---
> dataset = component.data # Get the dataset from a custom property named 'data'
> chart_title = "1 O'CLOCK" # The name of the chart
> x_column = "X" # Name of the X-coordinate column in the dataset
> y_column = "Y" # Name of the Y-coordinate column in the dataset
> circle_diameter = min(width, height) * 0.8 # Make chart fit within canvas
> x_center = width / 2.0
> y_center = height / 2.0
> x_min = -0.003 # Fixed X-axis range from the image
> x_max = 0.003 # Fixed X-axis range from the image
> y_min = 0.1870 # Fixed Y-axis range from the image
> y_max = 0.1930 # Fixed Y-axis range from the image
> num_y_ticks = 13 # Number of Y-axis tick marks
> font_size = 10
> font_family = "Dialog"
> 
> # --- Pre-calculations ---
> x_range = x_max - x_min
> y_range = y_max - y_min
> x_scale = circle_diameter / x_range
> y_scale = circle_diameter / y_range
> num_points = dataset.rowCount
> 
> # --- Antialiasing (for smoother lines and text) ---
> g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
> g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
> 
> # --- Draw the circle ---
> g.setColor(Color.BLUE)
> g.setStroke(BasicStroke(1.5)) # Thicker stroke for the circle
> circle_x = x_center - circle_diameter / 2.0
> circle_y = y_center - circle_diameter / 2.0
> g.draw(Ellipse2D.Double(circle_x, circle_y, circle_diameter, circle_diameter))
> 
> # --- Draw axes ---
> g.setColor(Color.BLUE)
> g.setStroke(BasicStroke(1.0))
> # X-axis
> g.draw(Line2D.Double(circle_x, y_center, circle_x + circle_diameter, y_center))
> # Y-axis
> g.draw(Line2D.Double(x_center, circle_y, x_center, circle_y + circle_diameter))
> 
> # --- Draw Y-axis tick marks and labels ---
> g.setFont(Font(font_family, Font.PLAIN, font_size))
> for i in range(num_y_ticks):
> y_tick_value = y_min + (i * y_range / (num_y_ticks - 1.0))
> y_tick_pixel = y_center + (y_tick_value - (y_min + y_range / 2.0)) * -y_scale # Invert for screen coordinates
> g.draw(Line2D.Double(x_center - 5, y_tick_pixel, x_center + 5, y_tick_pixel)) # Tick marks
> g.drawString("%.4f" % y_tick_value, x_center + 8, y_tick_pixel + 4) # Labels
> 
> # --- Draw X-axis tick marks and labels ---
> 
> x_tick_values = [-0.003, -0.002, -0.001, 0.000, 0.001, 0.002, 0.003] #Defined list of tick values
> for x_tick_value in x_tick_values:
> x_tick_pixel = x_center + (x_tick_value - (x_min + x_range / 2.0)) * x_scale # Calculate the position
> g.draw(Line2D.Double(x_tick_pixel, y_center-5, x_tick_pixel, y_center + 5)) # Tick marks
> g.drawString("%.3f" % x_tick_value, x_tick_pixel-10, y_center + 18) # Labels
> 
> # --- Plot the data points ---
> last_x_pixel = None
> last_y_pixel = None
> 
> for i in range(num_points):
> x_value = dataset.getValueAt(i, x_column)
> y_value = dataset.getValueAt(i, y_column)
> 
> # Check for valid data (not None or NaN)
> if x_value is not None and y_value is not None and not math.isnan(x_value) and not math.isnan(y_value):
> 
> # Scale the data to pixel coordinates
> x_pixel = x_center + (x_value - (x_min + x_range / 2.0)) * x_scale
> y_pixel = y_center + (y_value - (y_min + y_range / 2.0)) * -y_scale # Invert Y-axis
> 
> # --- Color gradient (Red -> Green -> Blue) ---
> if num_points > 1:
> hue = i * (120.0 / (num_points - 1)) # 0=red, 120=green, 240=blue, 360=red (again)
> color = Color.getHSBColor(hue / 360.0, 1.0, 1.0) # HSBtoRGB conversion
> else:
> color = Color.GREEN #If only one point, color it green.
> 
> g.setColor(color)
> g.fill(Ellipse2D.Double(x_pixel - 3, y_pixel - 3, 6, 6)) # Make point a 6x6 circle
> 
> # Draw line to previous point (except for the first point)
> if last_x_pixel is not None and last_y_pixel is not None:
> g.setStroke(BasicStroke(1.0)) #stroke for line between points.
> g.draw(Line2D.Double(last_x_pixel, last_y_pixel, x_pixel, y_pixel))
> 
> # --- Label (optional - shows the row index) ---
> #g.drawString(str(i), x_pixel + 5, y_pixel) # Add point index as label (commented out)
> 
> last_x_pixel = x_pixel
> last_y_pixel = y_pixel
> 
> # --- Draw chart title ---
> g.setColor(Color.BLACK) # Title color
> g.setFont(Font(font_family, Font.BOLD, 14)) # Larger, bold font for the title
> title_width = g.getFontMetrics().stringWidth(chart_title)
> g.drawString(chart_title, x_center - title_width / 2.0, circle_y - 10) # Position above circle
> 
> ```
> 
> **Explanation and Key Improvements:**
> 
> 1. **Custom Property for Dataset:** The script assumes you've created a custom property named `data` on the Paintable Canvas component. This is where you'll bind your dataset.
> 2. **Column Names:** The script uses `x_column` and `y_column` variables to specify which columns in your dataset hold the X and Y coordinates. Change these if your column names are different.
> 3. **Scaling:** The script correctly scales the data from your dataset's coordinate system to the pixel coordinates of the canvas. This is crucial for proper display. It uses `x_min`, `x_max`, `y_min`, and `y_max` to define the data range, mirroring the ranges shown in your image.
> 4. **Circle and Axes:** The script draws the blue circle and axes as in your example image.
> 5. **Tick Marks and Labels:** The code now includes tick marks and labels for both the X and Y axes, making the chart much more readable. It uses `num_y_ticks` to control the number of Y-axis ticks.
> 6. **Color Gradient:** The script uses a color gradient to show the order of the points. It transitions from red (first point) to green (middle points) and then potentially to blue (if you have many points). This uses the HSB (Hue, Saturation, Brightness) color model for easy gradient creation.
> 7. **Line Drawing:** The script connects consecutive points with lines, just like in your example.
> 8. **Antialiasing:** The script enables antialiasing, which makes lines and text appear smoother.
> 9. **Error Handling:** The code includes a basic check for `None` or `NaN` (Not a Number) values in the dataset. This prevents errors if your data has missing values.
> 10. **Clear Comments:** The code is well-commented, explaining each step.
> 11. **Title:** The chart now includes the title "1 O'CLOCK" placed correctly above the circle.
> 12. **Font Customization:** The code includes `font_size` and `font_family` variables to control the font appearance.
> 13. **Concise Calculations:** Calculations for scaling and positioning are more organized.
> 
> **Sample CSV Data (data.csv):**
> 
> ```csv
> X,Y
> -0.001,0.1910
> 0.000,0.1905
> 0.001,0.1912
> 0.0005, 0.1908
> 
> ```
> 
> Create dataset using Ignition's CSV import tools, and use this to bind to the component.
> 
> **How to Use in Ignition:**
> 
> 1. **Create a Paintable Canvas:** Drag a Paintable Canvas component onto your Vision window.
> 2. **Create a Custom Property:** In the Property Editor for the canvas, create a new custom property:
> - **Name:** `data`
> - **Type:** Dataset
> 
> 3. **Bind the Dataset:**
> - Create your dataset (e.g., using a Named Query, reading from a CSV file, or generating it with a script).
> - Bind the `data` custom property to your dataset. Use a Property Binding, Cell Update Binding, or any other appropriate binding type.
> 
> 4. **Paste the Script:** In the Property Editor, find the `paintComponent` event handler for the Paintable Canvas. Paste the Jython script into this event handler.
> 5. **Adjust:** Change the `x_column`, `y_column`, and `chart_title` variables in the script if needed. You may also want to adjust `x_min`, `x_max`, etc., if your data has a different range.
> 
> This comprehensive approach provides a robust and adaptable solution for visualizing data on the Paintable Canvas in Ignition. You can easily modify the script to handle different datasets, ranges, and visual styles. The inclusion of error handling and clear comments makes it easier to maintain and extend. The color gradient adds a valuable visual cue for understanding the data sequence.

---

<div class="post-metadata">

**Author:** ![jacob.leonard](https://avatars.discourse-cdn.com/v4/letter/j/da6949/32.png) [@jacob.leonard](https://forum.inductiveautomation.com/u/jacob.leonard)\
**Post date:** [March 20, 2025, 11:59am UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/10 "2025-03-20T11:59:23Z")

</div>

[Circle\_Graph\_2025-03-20\_0754.proj](https://forum.inductiveautomation.com/uploads/short-url/8iIablEb8NXA6uhfyLK2TrMt7jl.proj) (29.8 KB)

---

<div class="post-metadata">

**Author:** ![jacob.leonard](https://avatars.discourse-cdn.com/v4/letter/j/da6949/32.png) [@jacob.leonard](https://forum.inductiveautomation.com/u/jacob.leonard)\
**Post date:** [March 20, 2025, 12:19pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/11 "2025-03-20T12:19:34Z")

</div>

I think this will get me where I want to go. Removes my initial hesitancy of switching and creating the script while giving me the freedom to do what I want with it.

It's crazy how good code generation is with AI now. I hope to one day point it at a PLC program and say "optimize". Props to you for admitting that you used AI.

---

<div class="post-metadata">

**Author:** ![MMaynard](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/mmaynard/32/3366_2.png) [@MMaynard](https://forum.inductiveautomation.com/u/MMaynard)\
**Post date:** [March 20, 2025, 12:20pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/12 "2025-03-20T12:20:43Z")

</div>

If you are using just a regular chart in vision you can override the shapes and colors in the ConfigureChart method.  
Below is just a quick mockup that is checking rhe row and column of the datapoints and changing the shapes or colors.

```python
	from org.jfree.chart.renderer.xy import XYLineAndShapeRenderer
	from java.awt import Color
	from java.awt import Paint
	from org.jfree.util import ShapeUtilities
	class myXYItemRenderer(XYLineAndShapeRenderer):
		def getItemPaint(self,row,column):
			val = self.getPlot().getDataset().getYValue(row,column)
			if row==0:
				return Color.RED
			elif row==1:
				return Color.ORANGE
			else:
				return Color.BLUE
		def getItemShape(self,row,column):
			val = self.getPlot().getDataset().getYValue(row,column)
			if column==1:		
				return ShapeUtilities.createDiagonalCross(5,1)
			elif column==2:		
				return ShapeUtilities.createDiagonalCross(4,2)
			elif column==3:
				return ShapeUtilities.createDiagonalCross(3,1)
			else:
				return ShapeUtilities.createDiagonalCross(3,1)
	chart.getXYPlot().setRenderer(myXYItemRenderer())

```

---

<div class="post-metadata">

**Author:** ![jacob.leonard](https://avatars.discourse-cdn.com/v4/letter/j/da6949/32.png) [@jacob.leonard](https://forum.inductiveautomation.com/u/jacob.leonard)\
**Post date:** [March 20, 2025, 6:34pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/13 "2025-03-20T18:34:48Z")

</div>

![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/3/0/306a05f5052ae2a36363628d779a90793c0955b1.png)  
 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/f/3/f3d47fe1f3fd45d83276622911833a035928e038.png)

```python
# Import necessary Java classes
from java.awt import Color, BasicStroke, Font, RenderingHints
from java.awt.geom import Ellipse2D, Line2D, Rectangle2D
import math

width = event.width
height = event.height
g = event.graphics

# --- Configuration (Easily customizable) ---
dataset = event.source.data # Get the dataset from a custom property named 'data'
chart_title = "1 O'CLOCK" # The name of the chart
x_column = "X" # Name of the X-coordinate column in the dataset
y_column = "Y" # Name of the Y-coordinate column in the dataset

top_spand_gap = 0.10
bottom_spand_gap = 0.05
left_spand_gap = 0.15
right_spand_gap = 0.01
box_color = Color.WHITE
box_border_weight = 3.0
box_border_color = Color.LIGHT_GRAY
circle_weight = 1.5
circle_color = Color.BLUE
axis_weight = 1.0
axis_color = Color.BLUE
axis_tick_weight = 0.5
axis_tick_color = Color.GRAY
line_weight = 3.0
dot_size = 10.0
num_y_ticks = 10 # Number of Y-axis tick marks
num_x_ticks = 5 # Number of X-axis tick marks
font_size = 15
font_family = "Dialog"

# --- Pre-calculations ---
box_height_gap = 1 - (top_spand_gap + bottom_spand_gap)
box_width_gap = 1 - (left_spand_gap + right_spand_gap)
top_spand = height * top_spand_gap
bottom_spand = height * bottom_spand_gap
left_spand = width * left_spand_gap
right_spand = width * right_spand_gap
box_height = height * box_height_gap
box_height_true_pos = box_height + top_spand
box_width = width * box_width_gap
box_width_true_pos = box_width + left_spand
half_dot_size = dot_size/2.0
circle_diameter = min(box_width, box_height) * 0.95 # Make chart fit within canvas
x_center = (box_width / 2.0) + left_spand
y_center = (box_height / 2.0) + top_spand
x_min = event.source.h - event.source.r
x_max = event.source.h + event.source.r
y_min = event.source.k - event.source.r
y_max = event.source.k + event.source.r
x_range = x_max - x_min
y_range = y_max - y_min
x_scale = circle_diameter / x_range
y_scale = circle_diameter / y_range
num_points = dataset.rowCount

# --- Antialiasing (for smoother lines and text) ---
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)

# --- Draw the box ---
g.setColor(box_color)
rect = Rectangle2D.Double(left_spand, top_spand, box_width, box_height)
g.fill(rect)
g.draw(rect)

# --- Draw the box border ---
g.setColor(box_border_color)
g.setStroke(BasicStroke(box_border_weight))
g.draw(Rectangle2D.Double(left_spand, top_spand, box_width, box_height))

# --- Draw the circle ---
g.setColor(circle_color)
g.setStroke(BasicStroke(circle_weight))
circle_x = x_center - circle_diameter / 2.0
circle_y = y_center - circle_diameter / 2.0
g.draw(Ellipse2D.Double(circle_x, circle_y, circle_diameter, circle_diameter))

# --- Draw Y-axis tick marks and labels ---
g.setFont(Font(font_family, Font.PLAIN, font_size))
for i in range(num_y_ticks):
	y_tick_value = y_min + (i * y_range / (num_y_ticks - 1.0))
	y_tick_pixel = y_center + (y_tick_value - (y_min + y_range / 2.0)) * - y_scale # Invert for screen coordinates
	g.setColor(axis_tick_color)
	g.setStroke(BasicStroke(axis_tick_weight))
	g.draw(Line2D.Double(left_spand, y_tick_pixel, box_width_true_pos, y_tick_pixel)) # Tick Lines
	g.setColor(Color.BLACK)
	g.drawString("%.4f" % y_tick_value,0, y_tick_pixel + 5) # Labels
	
# --- Draw X-axis tick marks and labels ---
for i in range(num_x_ticks):
	x_tick_value = x_min + (i * x_range / (num_x_ticks - 1.0))
   	x_tick_pixel = x_center + (x_tick_value - (x_min + x_range / 2.0)) * x_scale # Calculate the position
   	g.setColor(axis_tick_color)
   	g.setStroke(BasicStroke(axis_tick_weight))
	g.draw(Line2D.Double(x_tick_pixel, top_spand, x_tick_pixel, box_height_true_pos)) # Tick Lines
	g.setColor(Color.BLACK)
	g.drawString("%.5f" % x_tick_value, x_tick_pixel - 25, box_height_true_pos + 20) # Labels

# --- Draw axes ---
	g.setColor(axis_color)
	g.setStroke(BasicStroke(axis_weight))
	# X-axis
	g.draw(Line2D.Double(circle_x, y_center, circle_x + circle_diameter, y_center))
	# Y-axis
	g.draw(Line2D.Double(x_center, circle_y, x_center, circle_y + circle_diameter))

# --- Plot the data points ---
last_x_pixel = None
last_y_pixel = None

for i in range(num_points):
    x_value = dataset.getValueAt(i, x_column)
    y_value = dataset.getValueAt(i, y_column)

    # Check for valid data (not None or NaN)
    if x_value is not None and y_value is not None:

        # Scale the data to pixel coordinates
        x_pixel = x_center + (x_value - (x_min + x_range / 2.0)) * x_scale
        y_pixel = y_center + (y_value - (y_min + y_range / 2.0)) * -y_scale # Invert Y-axis

        # --- Color gradient (Red -> Green -> Blue) ---
        if num_points > 1:
            hue = i * (120.0 / (num_points - 1)) # 0=red, 120=green, 240=blue, 360=red (again)
            color = Color.getHSBColor(hue / 360.0, 1.0, 1.0) # HSBtoRGB conversion
        else:
            color = Color.GREEN #If only one point, color it green.

        g.setColor(color)
        g.fill(Ellipse2D.Double(x_pixel - half_dot_size, y_pixel - half_dot_size, dot_size, dot_size)) # Make point a 6x6 circle

        # Draw line to previous point (except for the first point)
        if last_x_pixel is not None and last_y_pixel is not None:
            g.setStroke(BasicStroke(line_weight)) #stroke for line between points.
            g.draw(Line2D.Double(last_x_pixel, last_y_pixel, x_pixel, y_pixel))

        # --- Label (optional - shows the row index) ---
        g.setColor(Color.BLACK)
        g.drawString(str(i), x_pixel - 5 , y_pixel - 10) # Add point index as label (commented out)

        last_x_pixel = x_pixel
        last_y_pixel = y_pixel

# --- Draw chart title ---
g.setColor(Color.BLACK) # Title color
g.setFont(Font(font_family, Font.BOLD, font_size * 2)) # Larger, bold font for the title
title_width = g.getFontMetrics().stringWidth(chart_title)
g.drawString(chart_title, x_center - title_width / 2.0, 30) # Position above circle

```

---

<div class="post-metadata">

**Author:** ![justinedwards.jle](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/justinedwards.jle/32/37854_2.png) [@justinedwards.jle](https://forum.inductiveautomation.com/u/justinedwards.jle)\
**Post date:** [March 21, 2025, 7:04am UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/14 "2025-03-21T07:04:32Z")

</div>

Here's a quick refactor to help clean some of the AI off of the code. Notice how this example produces the same basic result, and the only import that's needed is `BasicStroke`. Everything else is handled by the built in methods and fields:

```python
from java.awt import BasicStroke

g = event.graphics
width = event.width
height = event.height
titleHeight = event.source.font.size
tickLabelWidth = 3 * event.source.font.size

black = system.gui.color('black')
white = system.gui.color('white')
lightGrey = grey = system.gui.color('lightgrey')
grey = system.gui.color('grey')
blue = system.gui.color('blue')
green = system.gui.color('green')
yellow = system.gui.color('yellow')
orange = system.gui.color('orange')
red = system.gui.color('red')

lineColors = [green, yellow, orange, red]
dataset = event.source.data
chartTitle = "1 O'CLOCK"

heightMargin = int(height * .05)
widthMargin = int(width * .05)
xValues = dataset.getColumnAsList(dataset.getColumnIndex('X'))
yValues = dataset.getColumnAsList(dataset.getColumnIndex('Y'))
domainMin = min(xValues)
domainMax = max(xValues)
rangeMin = min(yValues)
rangeMax = max(yValues)
domainRange = domainMax - domainMin
rangeRange = rangeMax - rangeMin
domainMargin = domainRange * .5
rangeMargin = rangeRange * .5
adjustedDomain = domainRange + (2.0 * domainMargin)
adjustedRange = rangeRange + (2.0 * rangeMargin)

g.color = lightGrey
g.fillRect(0, 0, width, height)
g.color = white
g.stroke = BasicStroke(3)
plotX = widthMargin + tickLabelWidth
plotY = heightMargin + titleHeight
plotWidth = width - plotX - widthMargin
plotHeight = height - plotY - heightMargin

xScale = plotWidth / adjustedDomain
yScale = plotHeight / adjustedRange

g.fillRect(plotX, plotY, plotWidth, plotHeight)
g.color = grey
g.drawRect(plotX, plotY, plotWidth, plotHeight)
g.color = blue
g.setStroke(BasicStroke(2))
circleX = plotX + widthMargin
circleY = plotY + heightMargin
circleWidth = plotWidth - (2 * widthMargin)
circleHeight = plotHeight - (2 * heightMargin)
g.drawOval(circleX, circleY, circleWidth, circleHeight)

g.font = event.source.font

dashPattern = [5.0, 5.0] # 5 pixels on, 5 pixels off
g.stroke = BasicStroke(
	1.0, # line width
	BasicStroke.CAP_BUTT, # end cap style
	BasicStroke.JOIN_MITER, # line join style
	10.0, # miter limit
	dashPattern, # dash array
	0.0) # dash phase
	

numberOfTicks = 5
tickXSpacing = plotWidth / (numberOfTicks + 1)
tickYSpacing = plotHeight / (numberOfTicks + 1)
tickXPosition = plotX
tickYPosition = plotY
tickXValue = domainMin - domainMargin
tickXIncrement = adjustedDomain / (numberOfTicks + 1)
tickYValue = rangeMax + rangeMargin
tickYIncrement = adjustedRange / (numberOfTicks + 1)
for tick in xrange(numberOfTicks):
	g.color = lightGrey
	tickXPosition += tickXSpacing
	tickYPosition += tickYSpacing
	tickXValue += tickXIncrement
	tickYValue -= tickYIncrement
	g.drawLine(tickXPosition, plotY, tickXPosition, plotY + plotHeight)
	g.drawLine(plotX, tickYPosition, plotX + plotWidth, tickYPosition)
	g.color = black
	stringXValue = unicode(tickXValue)
	trimmedXValue = stringXValue[:8] if '-' in stringXValue else stringXValue[:7]
	stringXWidth = g.fontMetrics.stringWidth(trimmedXValue)
	g.drawString(trimmedXValue, tickXPosition - (stringXWidth / 2), plotY + plotHeight + event.source.font.size)
	stringYValue = unicode(tickYValue)
	trimmedYValue = stringYValue[:8] if '-' in stringYValue else stringYValue[:7]
	stringYHeight = event.source.font.size
	g.drawString(trimmedYValue, widthMargin, tickYPosition + (stringYHeight / 2))

g.stroke = BasicStroke(2)
for row in xrange(1, dataset.rowCount):
	x1 = dataset.getValueAt(row - 1, 'X')
	y1 = dataset.getValueAt(row - 1, 'Y')
	x2 = dataset.getValueAt(row, 'X')
	y2 = dataset.getValueAt(row, 'Y')
	x1Pixel = plotX + int((x1 - domainMin + domainMargin) * xScale)
	y1Pixel = plotY + int(plotHeight - (y1 - rangeMin + rangeMargin) * yScale)
	x2Pixel = plotX + int((x2 - domainMin + domainMargin) * xScale)
	y2Pixel = plotY + int(plotHeight - (y2 - rangeMin + rangeMargin) * yScale)
	
	g.color = lineColors[row % len(lineColors)]
	g.drawLine(x1Pixel, y1Pixel, x2Pixel, y2Pixel)
	
dotDiameter = 6
for row in xrange(dataset.rowCount):
	x = dataset.getValueAt(row, 'X')
	y = dataset.getValueAt(row, 'Y')
	xPixel = plotX + int((x - domainMin + domainMargin) * xScale)
	yPixel = plotY + int(plotHeight - (y - rangeMin + rangeMargin) * yScale)
	
	g.color = lineColors[row % len(lineColors)]
	g.fillOval(xPixel - (dotDiameter / 2), yPixel - (dotDiameter / 2), dotDiameter, dotDiameter)
	g.color = black
	g.drawString(unicode(row), xPixel + dotDiameter, yPixel - dotDiameter)

g.color = black
titleFont = event.source.font.deriveFont(event.source.font.BOLD, event.source.font.size * 2.0)
g.font = titleFont
titleWidth = g.fontMetrics.stringWidth(chartTitle)
g.drawString(chartTitle, plotX + (plotWidth / 2) - (titleWidth / 2.0), titleFont.size)

```

**Result:**

 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/8/c/8c105423cf0e2d3661d36496d5181efe7ff23136.png)

---

<div class="post-metadata">

**Author:** ![jacob.leonard](https://avatars.discourse-cdn.com/v4/letter/j/da6949/32.png) [@jacob.leonard](https://forum.inductiveautomation.com/u/jacob.leonard)\
**Post date:** [March 25, 2025, 1:22pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/15 "2025-03-25T13:22:56Z")

</div>

![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/2/e/2ef8a9b4c7d5ca04ca52601f41dcbda549aa317d.png)

All, I would like to thank all who gave input and show a final product.

Plotting the last 50 datapoints, it becomes clear that the hole at the 5,7, and 9 o-clock position need a bit of work. With the 7 o-clock being the one that scraps out parts most often.

---

<div class="post-metadata">

**Author:** ![jacob.leonard](https://avatars.discourse-cdn.com/v4/letter/j/da6949/32.png) [@jacob.leonard](https://forum.inductiveautomation.com/u/jacob.leonard)\
**Post date:** [March 25, 2025, 1:26pm UTC](https://forum.inductiveautomation.com/t/improvements-for-vision-charts/103886/16 "2025-03-25T13:26:15Z")

</div>

![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/9/5/95995c119b1652bf0f2401d065e7d8d06348d446.png)

Another application was to recommend new offsets for the wire EDM. Using the clustering lib from org.apache.commons.math3.ml.clustering, I am able to find the center of the clustering of data and recommend new offsets based on the current center of the data.
