Improvements For Vision Charts


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?

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

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.

2 Likes

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.

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.

1 Like

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.

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

1 Like

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

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.

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:

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

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

# 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):

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.

5 Likes

Circle_Graph_2025-03-20_0754.proj (29.8 KB)

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.

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.

	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())
1 Like


# 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
1 Like

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:

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:

5 Likes

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.

1 Like

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.

3 Likes