Moving Analog Indicator extra process values

Hi folks

For the Moving Analog Indicator, is there any way of having additional process value arrows along the same bar?
I want to show live production against targets for 5 machines. The desired values/ targets etc are the same, and I'd like to have an arrow for each machine showing respective performance.

I've prodded the JSON but I can't tell if replicating any part of it would work.
Does anyone know a way to do this or have an alternative way to do something similar? (Is it possible to set the opacity of the process indicator separately, so they all show up if I stack 5 of them, for instance?)

Any and all ideas welcome. Thanks :slight_smile:

You could add on to this resource:

I haven't looked at it, but its and SVG, so you could probably just copy and paste the existing arrow into the elements and bind it separately.

Some things to consider:

  • What if the values overlap? Will it be confusing to the user if all 5 of the values are the same and look like one arrow?
  • Also, how will you visually differentiate between the meaning of each arrow; by color or label perhaps? With either, I would caution that it would be confusing if the values overlap.

An HMI should ideally clearly communicate the value and meaning of the value at first glance. An analog indicator with 5 different arrows may not always be clear.

1 Like

You will not be able to do this with the Analog Indicator component. It's designed for just a single Analog value.

Given what you're trying to do, I assume this is for a Dashboard rather than a SCADA system.

Besides the 5 different arrows, is there anything else that you're wanting to show?

Oh, cool. I'll see if that works, thanks!

Differentiation of indicators is going to be by colour - there's a consistent colour allocation for each machine across kpi data displays, so hopefully that'll be clear enough in that regard.

Overlapping is something I'll be monitoring in testing before I let the project go live. I'm fairly sure there are enough parameters in the data creation that the indicators won't completely overlap, and may not overlap at all, but yes, that's certainly a question mark over this particular approach.

I might use gradients and make them stripey... :stuck_out_tongue:

I still want to see if it works though.
For Science!

Just remember that Color Blindness is a thing, so even for dashboards you want to have multiple ways that things are differentiated.

3 Likes

I don't think there's anything else for this particular idea.
It's to try to show how the 5 machines are each performing against identical production targets over time.
The idea is that the indictor range will start with the shift target amount, and increase if any machines exceed the target. The set point indicator will stay at the shift target. The desired range will be the pro rata shift target for the current time through the shift, up to the full shift target (so moving along the indicator bar during the shift), and the arrows will show where against those parameters each machine is.

So this component would do it perfectly, if I can get 5 arrows on it, and notwithstanding the potential clarity issues @Daniel.Snyder highlighted (which I'm hoping will survive the experiment).

Still open to other suggestions while I play with the SVG version :slight_smile:

(And yes, it's a dashboard rather than a SCADA)

In many places, it is often a legal obligation, even for something as simple as a dashboard. (In the U.S., per the Americans with Disabilities Act.) IANAL, yada yada.

1 Like

Yes, The machines are numbered so if the svg approach is doable (and I decide to go that way) I'll add a text element with the machine number to each arrow.

1 Like

Oh it's doable. Just give me a bit.

1 Like

@lrose Lol. Thank you for playing!

Reference image with one arrow for target etc:
image

Regarding overlap, you might be able to make the indicators "move over" if the arrows overlap to keep things readable.
Probably would go beyond simple bindings however.

Very rough mockup

Also, would look pretty ugly if all 5 arrows overlapped.

1 Like

Ooh. Or have the closest ones go to opposite sides... :thinking:

Honestly it might be cleanest and easiest to just stack the gauges next to each other. Would fix the distinction issue (1-5 left-to-right or top-to-bottom) and no chance of overlap.

image

(updated with a gradient to be a little more colorful)

3 Likes

Okay, here is a quick approximation of the Analog Indicator. There are many other things that could be added to this, however, I would honestly recommend something more like what @Felipe_CRM suggested in their last post. Or quite frankly just a simple Bar Chart.

I did it in a vertical orientation rather than Horizontal, but it wouldn't be too difficult to convert this.

Here is what it looks like:

Here is the script which does the magic. Note that while it is included completely in a script transform, I would recommend that you move this to a script library and then call this from a runScript() expression.

from copy import deepcopy
groupElement = {'type': 'group', 'elements':[]}
arrowElement = value.element
textElement = {'type': 'text', 'fill':{'paint':'#FFFFFF'},'style':{'fontFamily':'Verdana', 'fontSize':14, 'fontWeight': 'bold', 'text-anchor':'end'}, 'x':'12.5px', 'y':'','text':''}
elements = []
	
for arrow,val in value.processValues.iteritems():
	group = deepcopy(groupElement)
	text = deepcopy(textElement)
	element = deepcopy(system.util.jsonDecode(system.util.jsonEncode(arrowElement)))
	element['fill']['paint'] = val['color']
	element['style']['translate'] = '0px {}px'.format((self.custom.maxValue - val['value']) * 500 / (self.custom.maxValue - self.custom.minValue))
	text['y'] = '{}px'.format((self.custom.maxValue - val['value']) * 500 / (self.custom.maxValue - self.custom.minValue) + 25)
	text['text'] = arrow[-1:]
	group['elements'].extend([element,text])
	elements.append(group)
	
return elements

Here is the view JSON, which you can import into a project (NOTE: create a new view to import this into, then you can directly copy the SVG element.)

View JSON
{
  "custom": {},
  "params": {},
  "props": {
    "defaultSize": {
      "height": 563,
      "width": 145
    }
  },
  "root": {
    "children": [
      {
        "custom": {
          "arrow": {
            "collisionDistance": 50,
            "element": {
              "fill": {
                "paint": "#787878"
              },
              "points": "0,0 30,20 0,40",
              "type": "polygon"
            },
            "processValues": {
              "Arrow1": {
                "color": "#CDC65E",
                "value": 15
              },
              "Arrow2": {
                "color": "#0F0F0F",
                "value": 50
              },
              "Arrow3": {
                "color": "#00FF00",
                "value": 30
              },
              "Arrow4": {
                "color": "#0000FF",
                "value": 80
              },
              "Arrow5": {
                "color": "#FF0000",
                "value": 10
              }
            },
            "reverseIndicator": false,
            "reversed": {}
          },
          "desiredHigh": 65,
          "desiredLow": 40,
          "desiredRangeColor": "#8EBAE3",
          "maxValue": 100,
          "minValue": 0
        },
        "meta": {
          "name": "blank_1"
        },
        "position": {
          "height": 500,
          "rotate": {
            "anchor": "377% 50%"
          },
          "width": 100,
          "x": 22,
          "y": 23
        },
        "propConfig": {
          "custom.arrow.element.style": {
            "binding": {
              "config": {
                "struct": {
                  "transform": "if({this.custom.arrow.reverseIndicator},\u0027rotate(180deg)\u0027,\u0027\u0027)",
                  "transform-origin": "\u002750px 20px\u0027"
                },
                "waitOnAll": true
              },
              "type": "expr-struct"
            }
          },
          "props.elements[1].fill.paint": {
            "binding": {
              "config": {
                "path": "this.custom.desiredRangeColor"
              },
              "type": "property"
            }
          },
          "props.elements[1].height": {
            "binding": {
              "config": {
                "expression": "(({this.custom.desiredHigh} - {this.custom.desiredLow}) * 500 / ({this.custom.maxValue} - {this.custom.minValue}))"
              },
              "type": "expr"
            }
          },
          "props.elements[1].y": {
            "binding": {
              "config": {
                "expression": "500  - ({this.custom.desiredHigh} * 500 / ({this.custom.maxValue} - {this.custom.minValue}))"
              },
              "type": "expr"
            }
          },
          "props.elements[2].elements": {
            "binding": {
              "config": {
                "path": "this.custom.arrow"
              },
              "transforms": [
                {
                  "code": "\tfrom copy import deepcopy\n\tgroupElement \u003d {\u0027type\u0027: \u0027group\u0027, \u0027elements\u0027:[]}\n\tarrowElement \u003d value.element\n\ttextElement \u003d {\u0027type\u0027: \u0027text\u0027, \u0027fill\u0027:{\u0027paint\u0027:\u0027#FFFFFF\u0027},\u0027style\u0027:{\u0027fontFamily\u0027:\u0027Verdana\u0027, \u0027fontSize\u0027:14, \u0027fontWeight\u0027: \u0027bold\u0027, \u0027text-anchor\u0027:\u0027end\u0027}, \u0027x\u0027:\u002712.5px\u0027, \u0027y\u0027:\u0027\u0027,\u0027text\u0027:\u0027\u0027}\n\telements \u003d []\n\t\n\tfor arrow,val in value.processValues.iteritems():\n\t\tgroup \u003d deepcopy(groupElement)\n\t\ttext \u003d deepcopy(textElement)\n\t\telement \u003d deepcopy(system.util.jsonDecode(system.util.jsonEncode(arrowElement)))\n\t\telement[\u0027fill\u0027][\u0027paint\u0027] \u003d val[\u0027color\u0027]\n\t\telement[\u0027style\u0027][\u0027translate\u0027] \u003d \u00270px {}px\u0027.format((self.custom.maxValue - val[\u0027value\u0027]) * 500 / (self.custom.maxValue - self.custom.minValue))\n\t\ttext[\u0027y\u0027] \u003d \u0027{}px\u0027.format((self.custom.maxValue - val[\u0027value\u0027]) * 500 / (self.custom.maxValue - self.custom.minValue) + 25)\n\t\ttext[\u0027text\u0027] \u003d arrow[-1:]\n\t\tgroup[\u0027elements\u0027].extend([element,text])\n\t\telements.append(group)\n\t\n\treturn elements",
                  "type": "script"
                }
              ],
              "type": "property"
            }
          }
        },
        "props": {
          "elements": [
            {
              "fill": {
                "paint": "#00000000"
              },
              "height": "100%",
              "id": "backGround",
              "stroke": {
                "paint": "#c0c0c0",
                "width": 3
              },
              "type": "rect",
              "width": "25%",
              "x": "37.5%",
              "y": 0
            },
            {
              "fill": {},
              "id": "backGround",
              "stroke": {
                "paint": "#c0c0c0",
                "width": 3
              },
              "type": "rect",
              "width": "25%",
              "x": "37.5%"
            },
            {
              "id": "Arrows",
              "type": "group"
            }
          ],
          "viewBox": "0 0 100 500",
          "viewPort": {
            "height": 50,
            "width": 50
          }
        },
        "type": "ia.shapes.svg"
      }
    ],
    "meta": {
      "name": "root"
    },
    "type": "ia.container.coord"
  }
}
4 Likes

Thanks @lrose :smiley:

I shall definitely take under advisement that this way of doing it might not be the best (or even a good) way of doing it, and see how it goes once it's tracking production.
If it's not easy to read at a glance, it's not doing it's job and I'll change tack.

Thanks for all your input lovely humans.

Hi @lrose
The element['style']['translate'] doesn't seem to be actually moving the arrow positions... I don't know why.
I manually changed the elements['points'] values to add in the translate value for the green number 3 arrow, otherwise they all sit at the top like shown (although the numbers are in the right places).

I'm not sure how to fix this...
I'm on version 8.1.25 if that's relevant.

Thanks

You will need to add a binding to the custom property arrow.processValues.arrow{number}.value to give it a dynamic value.

The script transform will do the rest.

Hi @lrose
I don't mean they aren't reflecting the live values (I haven't hooked them up yet because there was just one black arrow at the top of the indicator when I pasted the JSON, so I've been prodding it to see why it looked different to your screenshot).
I mean the arrows are not positioned to the values you've defaulted them to in the custom['arrow']['processValues'] property.

The element['translate'] property seems to being set to reflect those, but that's not changing the arrow position.

elements[0][2][0] is how it looks without me changing anything after pasting the JSON.
elements[1][2][0] I've manually added the translate value to the points value.

I was thinking the translate style would just do that adjustment to the arrow position, but it doesn't seem to be doing?

That is exactly what it should do. The arrows are all copies of the same element (custom.arrow.element).

Can you post the script from the transform? Just want to make sure there isn't some kind of indentation or other error.

def transform(self, value, quality, timestamp):
	from copy import deepcopy
	groupElement = {'type': 'group', 'elements':[]}
	arrowElement = value.element
	textElement = {'type': 'text', 'fill':{'paint':'#FFFFFF'},'style':{'fontFamily':'Verdana', 'fontSize':14, 'fontWeight': 'bold', 'text-anchor':'end'}, 'x':'12.5px', 'y':'','text':''}
	elements = []
	
	for arrow,val in value.processValues.iteritems():
		group = deepcopy(groupElement)
		text = deepcopy(textElement)
		element = deepcopy(system.util.jsonDecode(system.util.jsonEncode(arrowElement)))
		element['fill']['paint'] = val['color']
		element['style']['translate'] = '0px {}px'.format((self.custom.maxValue - val['value']) * 500 / (self.custom.maxValue - self.custom.minValue))
		text['y'] = '{}px'.format((self.custom.maxValue - val['value']) * 500 / (self.custom.maxValue - self.custom.minValue) + 25)
		text['text'] = arrow[-1:]
		group['elements'].extend([element,text])
		elements.append(group)
	
	return elements

That's the transform text on the elements binding