Drive a tank level from an analog value

Intermediate · 12 min · Tank

A level in percent is one number, and the graphic that shows it should be one binding. This walks through moving a tank’s liquid with an arithmetic expression, recolouring it at the LO and HI limits, and putting the number next to it — three bindings for a widget you can drop on every vessel in the plant.
  • Drive a tank level from an analog value — Level = 8, LevelLow = true
    Level = 8 LevelLow = true
  • Drive a tank level from an analog value — Level = 45
    Level = 45
  • Drive a tank level from an analog value — Level = 94, LevelHigh = true
    Level = 94 LevelHigh = true

The problem

A level is usually built as a rectangle with a height dynamic, which means the rectangle grows downward from its top edge rather than upward from the tank floor. So you either fight it with a second dynamic on the Y position, or you draw the tank upside down and rotate it, and either way the next person to open the screen has to work out what you did.

The scale is the other half of the problem. Screen coordinates are pixels and the tag is in percent, so something somewhere has to do the conversion. Put it in the PLC and you have written screen geometry into control code. Put it in a script and it is invisible to the next engineer.

A dynamic SVG puts it in the graphic, where it belongs — one expression, stored in the widget, that nothing else in the project needs to know about.

What you need

A tank graphic whose liquid is a single group, drawn at full height and clipped to the vessel. That is how the sample below is built: the liquid slides down out of its clip rather than changing size, so the surface and the body move together and one binding does the whole job.

From the PLC you need the level as a number in percent, and the two limit bits your alarm logic already produces.

ParameterTypeWhat it does
LevelstringFill level in percent, 0 to 100.
LevelHighbooleanHI limit. True when the level is above the high setpoint.
LevelLowbooleanLO limit. True when the level is below the low setpoint.
LevelTextstringThe number shown in the readout, already formatted.

Build it

  1. 1. Move the liquid with an expression

    Select the liquid group and add a Custom Attribute binding. Set the attribute to transform and the expression to:

    translate(0, {{216 - ParamProps.Level * 2.16}})

    216 is the height of the clipped area in the drawing, and 2.16 is that height divided by 100. At Level = 100 the offset is zero and the tank reads full; at 0 the liquid has slid its whole height out of view.

    Drive a tank level from an analog value — Move the liquid with an expressionDrive a tank level from an analog value — Move the liquid with an expression
  2. 2. Recolour it at the limits

    Select the liquid body and add a Cond. Fill (Multi-Bool) binding with two rows: LevelHigh in red, then LevelLow in amber. Leave the fallback the normal blue.

    The colour is on the liquid rather than on a separate warning lamp because a tank that is too full should look wrong from across the room, not require reading a small indicator next to it.

    Drive a tank level from an analog value — Recolour it at the limitsDrive a tank level from an analog value — Recolour it at the limits
  3. 3. Show the number

    Select the readout text and add a Text Content binding named LevelText. Keep it a string rather than a number: the formatting — decimal places, the percent sign, whether an unreliable reading shows as --- — is a decision that belongs wherever the tag is scaled, not inside the graphic.

    Drive a tank level from an analog value — Show the numberDrive a tank level from an analog value — Show the number

Check it

Open Simulate and drag Level from 0 to 100. The surface should travel the full height of the vessel and stop exactly at the top and bottom marks. If it stops short, or slides past, the two numbers in the expression do not match the drawing.

Then set LevelHigh and watch the whole body turn, not just an outline.

Simulate needs a free account. Everything up to this point does not.

Variations

  • A horizontal vessel. The same expression with the offset in X instead of Y: translate({{…}}, 0), using the clip width rather than its height.
  • Fill from the top. Drop the minus: translate(0, {{ParamProps.Level * 2.16 - 216}}) slides the liquid in from above, which is what a hopper discharging looks like.
  • Limits from a state number. If your alarm logic publishes a single state word rather than separate bits, swap the multi-bool for Cond. Fill (Number) and give each value its own row.

Questions

Why move the liquid instead of resizing it?
Because a rectangle grows from its top edge, so a height dynamic alone makes the liquid hang from the top of the tank instead of sitting on the bottom. Sliding a full-height body out of a clip needs one binding rather than two, and the surface detail comes along for free.
Where do 216 and 2.16 come from?
The drawing. 216 is the height of the clipped area in the tank, and 2.16 is that divided by 100, because the level arrives in percent. Read both off your own graphic rather than copying these.
Can I scale the value in the expression too?
Yes — if the tag is in litres rather than percent, divide by the capacity in the same expression. But a widget that knows one tank's capacity only fits that tank, so it is usually better to pass in a percentage and keep the graphic reusable.
Why is the clip on a different element than the transform?
Because a clip is resolved in the element's own coordinate space. Put it on the group that moves and it moves too, so nothing is ever clipped and the liquid runs straight out of the tank. The outer group holds the clip and stays still; the inner one carries the transform.
Open this example in the editorDownload the .svghmi

Related tutorials

Drive a tank level from an analog value — WinCC Unified & WinCC V8+ | DynSVG