Circular Chart!

Been seeing different posts/requests/gripes about circular charts for while now. Yes, we know that they’re not really value-added over the standard charts. But sometimes, it’s the perceived value that is king. So here is my first run-through.

Here’s a quick explanation for the less obvious custom properties:

CCW: counter-clockwise. sets the direction of the graph.
Data: timestamp plus up to four data columns. You can have more, but they’ll be ignored. You can have less, just be sure to turn off the extra pens… :wink:
GapTime: number of seconds between data points before a blank section is given.
HiGraphLimit/LoGraphLimit: sets the upper and lower bounds of the graph.
PenEnableStatus: decimal representation of the pen enables. If you look closer, it’s tied to the checkboxes.
Precision: number of decimal points used on the displayed values.

Everything else affects colors, so adjust them to whatever works for you!

The usual rules apply. No warranty is expressed or implied.

One more thing to note. The Data property isn’t bound to anything, but here’s the original query used to populate the dataset:

SELECT t_stamp, Aft_Height_1 as p1, Aft_Height_2 as p2, Fore_Height_1 as p3, Fore_Height_2 as p4 FROM C489_Riveter WHERE t_stamp >= '{Root Container.Popup Calendar.Start}' AND t_stamp < '{Root Container.Popup Calendar.End}'
Circular Graph_2018-05-04_1126.proj (48.1 KB)

EDIT 2: Refreshed link

EDIT: had a pic all ready to go, but I forgot to post it. :blush:


2 Likes

Very nice, Jordan!

I am impressed. I probably won’t use it, but I am impressed none the less :wink:

You should make that into a template and upload it to the cloud template browser!

[quote=“Duffanator”]Very nice, Jordan!

I am impressed. I probably won’t use it, but I am impressed none the less :wink:

You should make that into a template and upload it to the cloud template browser![/quote]

I have a definite application for this!
just need Ethernet based I/O to input data directly into Ignition without a PLC.

Looking for EtherNet I/P or Modbus TCP IO modules

I'm working on this, actually. To support Allen-Bradley 1734- and 1794- series Ethernet/IP to start. Might be ready by ICC. Certainly in Q4.

Curlyandshemp

Take a look at the WAGO 750 series, I've had good results with these, the 7500352 is great as its modbus with two Ethernet ports that act like a mini switch so you can daisy chain if your system is not critical. you can a use any WAGO I/O in the range and there are quite cheep.

http://www.wago.com/wagoweb/documentation/navigate/nm0dc__e.htm
http://www.wago.com/wagoweb/documentation/750/eng_dat/d07500352_00000000_0en.pdf

We use Beckhoff IO. Tons of connectivity options.

Jordan, very interesting!

Been playing around with chart.
All pens configuration data part of database
Chart configuration data in database,
added Zoom, and Pan X Pan Y axis sliders
Export to Excel button,
Reset sets Zoom and PanX PanY back to defaults:




2 Likes

project file

[attachment=0]Charts_2016-04-01_1533_partial.proj[/attachment]

1 Like

I just found this Circular Chart and I’m very impressed. :+1:

There is a potential use for this with one of our regular customers. They want us to build a device for measuring the pulley circularity. And this ‘Circular Chart’ is ideal for this.


I’ve been playing with it to adapt to our needs and it’s working great.

(the blue line represent test data; not real measurements)
Kudos to @JordanCClark.

But our customer has one more request:
They want to have the areas, where the blue line (circularity measurement) intersect with the black circle in the middle (ideal circularity, the Diameter field on the picture), colored.


(I tried to color by hand in gimp with green color…)

Is that even possible in Paintable canvas?
If it is, then I would be very grateful if someone (@JordanCClark ?) can help/show me how…?
This is my project window, Ignition 7.9.13…
Circular Graph1_2021-03-30_0959.proj (24.8 KB)

1 Like

@Curlyandshemp Is this still available somewhere by any chance?

Sorry for the late reply, Anton, our family got Covid-tized, so we’re quarantined for a while. The bright side is that I get to help out with this sort of stuff. :wink:

I don’t have a 7.9 system to play with at the moment. You’ll need to make an 8.1 installation if you want to use the file I posted. Note that I trimmed it down to just one pen.

This uses clip methods. Basically, a clip acts as a window so that only things inside the clip shape will render. Different clips were needed for above and below the nominal. After the clip renderings are done, we can then then set the clip to the entire graphic, so we can finish it off as before.

Cgraph_with_highlight_2021-04-03_1727.zip (17.8 KB)

Peek 2021-04-03 19-48

from java.awt import Color
from java.awt import GradientPaint
from java.awt.geom import GeneralPath
from java.awt.geom import Rectangle2D
from java.awt.geom import Ellipse2D

def scp(x, rawLo, rawHi, scaledLo, scaledHi, precision=0):
  m=(float(scaledHi)-float(scaledLo))/(float(rawHi)-float(rawLo))
  b=scaledLo-(rawLo*m)
  if precision<1:
    y=int(round(m*x+b,precision))
  else:
    y=round(m*x+b,precision)
  return y
  
def rect360(rho,theta):
	import math
	theta=float((theta-360)*math.pi/180)
	x=float(0)
	y=float(0)
	y=rho*math.sin(theta)
	x=rho*math.cos(theta)
	return x,y

dataIn=system.dataset.toPyDataSet(event.source.Data)
dataLength = len(dataIn)

if len(dataIn)>0:
  g = event.graphics
  print type(g)
  axes=GeneralPath()  # path for axes and major gridlines
  grid=GeneralPath()  # path for minor gridlines
  pen = GeneralPath() # path for data
  penColor=event.source.Pen1Color
  x=0         # Current x-ordinate for each pen
  y=0         # Current y-ordinate for each pen
  x1=0        # Storage for first x-ordinate for each pen
  y1=0        # Storage for first x-ordinate for each pen
  
  FirstPointFlag=0
  lowerInBound=float(event.source.LoGraphLimit)
  upperInBound=float(event.source.HiGraphLimit)
  precision=int(event.source.Precision)
  gapTime=event.source.GapTime
  if gapTime==0:
    gapTime=100000
  lowerRhoBound=0.0
  upperRhoBound=500.0  #Set size of graph
  center=500
  UpperXY=center*2
  UpperXYBound=UpperXY*1.05
  gOffset=(UpperXYBound-UpperXY)/2
  hourOffset=event.source.parent.parent.getComponent('Popup Calendar').HourOffset
  ccw=event.source.CCW
  startTime=event.source.parent.parent.getComponent('Popup Calendar').StartTime.getTime()
  penEnabledWord=event.source.PenEnableStatus
  penEnabled=[bool(penEnabledWord & 1),bool(penEnabledWord & 2),bool(penEnabledWord & 4),bool(penEnabledWord & 8)]
  index = 0.0
  minValue = None
  maxValue = None
  minValueX = 0.0
  minValueY = 0.0
  maxValueX = 0.0
  maxValueY = 0.0

#create data path
  for row in dataIn:
    if ccw:    
      theta = 360-index
    else:
      theta = index
      
    rho=scp(row[1],lowerInBound,upperInBound,lowerRhoBound,upperRhoBound)
    x,y=rect360(rho,theta)
    if minValue is None or row[1] < minValue:
    	minValue = row[1]
       	minValueX, minValueY = x, y
    if maxValue is None or row[1] > maxValue:
       	maxValue = row[1]
       	maxValueX, maxValueY = x, y
    if FirstPointFlag==0:	
        pen.moveTo(center+x+gOffset,center+y+gOffset)
		  # Store first point values to complete the graph	
        x1 = x
        y1 = y     
    else:
        diff=(index-lastTimeIn)/1000
        if diff > gapTime:
          pen.moveTo(center+x+gOffset,center+y+gOffset)
        else:
          pen.lineTo(center+x+gOffset,center+y+gOffset)
    if FirstPointFlag==0:
      FirstPointFlag=1
    lastTimeIn=index
    index += 360.0/dataLength
  # Close the loop
  pen.lineTo(center + x1 + gOffset, center + y1+ gOffset)


  
# create gridlines path
  for i in range(0,360,10):
    coord=rect360(500,i)
    axes.moveTo(center+gOffset,center+gOffset)
    axes.lineTo(center+gOffset+coord[0], center+gOffset+coord[1])
    coord=rect360(500,i)      
  
# set graph scaling to size of the canvas
  dX = float(event.width-1)/UpperXYBound
  dY = float(event.height-1)/UpperXYBound
  g.scale(dX,dY)

#draw everything-- ;)
# draw background
  g.setColor(event.source.BackgroundColor)
  background=Rectangle2D.Float(0,0,UpperXYBound,UpperXYBound)
  g.fill(background)

# This section draws the highlighted areas
  
  #draw filled cirlce at Ideal Diameter, then use it to clip the next graphics
  # This lets us highlight the areas below nominal.
  radius = event.source.Diameter 
  n=scp(radius,lowerInBound,upperInBound,lowerRhoBound,upperRhoBound*2)
  g.setColor(Color.GREEN) 
  g.fill(Ellipse2D.Float(center-n/2+gOffset, center-n/2+gOffset, n, n))
  g.clip(Ellipse2D.Float(center-n/2+gOffset, center-n/2+gOffset, n, n))
  
  g.setColor(event.source.BackgroundColor)
  g.fill(pen)
  
  # set clip back to full graphic
  g.setClip(background)
  #Set the pen graphic as the clip area.
  g.clip(pen)
  
  # draw highlight cicles between max and nominal
  radius = maxValue
  n=scp(radius,lowerInBound,upperInBound,lowerRhoBound,upperRhoBound*2)
  g.setColor(Color.GREEN) 
  g.fill(Ellipse2D.Float(center-n/2+gOffset, center-n/2+gOffset, n, n))

  radius = event.source.Diameter
  n=scp(radius,lowerInBound,upperInBound,lowerRhoBound,upperRhoBound*2)
  g.setColor(event.source.BackgroundColor) 
  g.fill(Ellipse2D.Float(center-n/2+gOffset, center-n/2+gOffset, n, n))

# Reset the clip to the maximum area, and draw everything else.  
  g.setClip(background)
  #draw pen
  g.setColor(penColor)
  g.draw(pen)

  # Draw nominal Cirlce
  radius = event.source.Diameter
  n=scp(radius,lowerInBound,upperInBound,lowerRhoBound,upperRhoBound*2)
  g.setColor(event.source.DiameterColor) 
  g.draw(Ellipse2D.Float(center-n/2+gOffset, center-n/2+gOffset, n, n))


  g.setColor(event.source.AxixColor)
  g.draw(axes)
  
  # gridline circles 
  for size in range(0,1001,200):
    if str(size)[-2:]=="00":
       g.setColor(event.source.MajorGridlineColor)
    else:
      g.setColor(event.source.MinorGridlineColor) 
    g.draw(Ellipse2D.Float(center-size/2+gOffset, center-size/2+gOffset, size, size))
  
  #draw circle Tolerance Minus
  radius = event.source.ToleranceMinus
  n=scp(radius,lowerInBound,upperInBound,lowerRhoBound,upperRhoBound*2)
  g.setColor(event.source.ToleranceColor) 
  g.draw(Ellipse2D.Float(center-n/2+gOffset, center-n/2+gOffset, n, n))

  #draw circle Tolerance Plus
  radius = event.source.TolerancePlus 
  n=scp(radius,lowerInBound,upperInBound,lowerRhoBound,upperRhoBound*2)
  g.setColor(event.source.ToleranceColor) 
  g.draw(Ellipse2D.Float(center-n/2+gOffset, center-n/2+gOffset, n, n))
  
  #draw min and max values circle
  diameter = 20
  g.setColor(event.source.ToleranceColor) 
  g.draw(Ellipse2D.Float(center+(minValueX-(diameter/2))+gOffset, center+(minValueY-(diameter/2))+gOffset, diameter, diameter))
  g.draw(Ellipse2D.Float(center+(maxValueX-(diameter/2))+gOffset, center+(maxValueY-(diameter/2))+gOffset, diameter, diameter))


  # values around outer cirlce
  g.setColor(Color.BLACK)
  for i in range (0,36):
    x=(i*10)+hourOffset
    if x>360:
      x-=360
    if ccw:
      theta=360-10*i
    else:
      theta=10*i
    coord=rect360(515,theta)
    g.drawString(str(x)+u'°',center+coord[0]+gOffset-10,center+coord[1]+gOffset+5) 
   
 # Axis values
  g.setColor(event.source.ValueTextColor)
  for i in range(5):
    n=str(scp((i+1),0,5,lowerInBound,upperInBound,precision))
    s=n+"0"*(precision-(len(n)-n.find(".")-1))
    #print "n= ", n , "s= ", s
    x=center+100*(i+1)+gOffset-(3+6*len(s))
    y=center+gOffset+10
    g.drawString(s,x,y)
  
    x=center+gOffset-(3+6*len(s))
    y=center+100*(i+1)+gOffset-3
    g.drawString(s,x,y)

    x=center-100*(i+1)+gOffset+5
    y=center+gOffset+10
    g.drawString(s,x,y)

    x=center+gOffset-(3+6*len(s))
    y=center-100*(i+1)+gOffset+10
    g.drawString(s,x,y)
2 Likes

Thank you very much. :+1:
That’s exactly what we need.


I also like the @Curlyandshemp idea about pan and zoom and I tried to implement it.
PanX and PanY are working very well until I change the Zoom. Then a strange thing happen:

After I change the Zoom:

I’m sure that this has to do with the clip, but I don’t understand, what to do…
Please, can you help me with this also?
CircularGraph4_Circular Graph_2021-04-04_1202.proj (37.0 KB)

2 Likes

What needs to be changed to turn this into a 7 day chart for daily vs hourly values?

The included scp function will help. It’s modeled after Rockwell’s ‘Scale with Parameters’ function. So, to calculate a theta for a t_stamp value would be something like:

scp(t_stamp, min_t_stamp, max_t_stamp, 0, 360)

Jordan,

This is what I am trying to replicate as far as the circle charts. Any help would be much appreciated.

That is in Vision right?
Is it available in Perspective?

When I worked in radio, we had some circular schedules.
I think that it would be useful, or worth experimenting with for some applications I have ideas for.

Not simply.

Is there a good way to create a static circular daily schedule?

I want to place all my material splices and process activities on a chart.

I eventually want to have some analytic abilities.
I tried this vizzlo thing, but it was terrible.
Trying add values should have been like entering values into a table, but I could not get it to work.

I am thinking if I do generate a circular schedule, I could then post at least the static as an image and then maybe make some form entry around that to obtain some datapoints.

Or I could copy the object, and then place labels around the clock. Then bind those to a dataset.

Reminds me of the temp chart from the old Partlow MRC 5000s. Very useful, I believe those are now discontinued.

1 Like