A circular gauge / meter

This is the updated Speedometer that was first posted here:

Since then it has been updated to provide a gauge that is reversible, orientable and invertible
Based on the original work of Kevin Schlosser

Copyright Kevin Schlosser April 2019
GNU General Public License v3.0

Amendments by RolfofSaxony 2023
Enabling the volume knob to become an all-round circular gauge, which can have multiple uses
e.g. as a speedometer, a meter for voltage, amperes, revolutions per minute, psi, a thermometer etc

It has become reversible, orientable and invertible in Version 2.0

In addition to knob.py there are a variety of example programs showing not only its proposed uses but also providing examples of how to use the majority of its functions.

Also provided is knobdemo_styles.py, which provides a demonstration of the various styles available, in addition to the visual adjustments that can be made using the functions.
Varying colours and thumb size etc., I leave you to your own devices.

It should be noted that knob.py, has its own built in demonstration, if knob.py is run directly.

The souce code:
Knob_2.1.zip (42.7 KB)

Given that I haven’t had to handle trigonometry since I last had to conjugate Latin and Greek verbs, over 40 years, there’s room in this code for oversights and errors.

“Romani ite domum” versus “Romanes eunt domus” :wink:

As usual, submit bugs, comments and insults on a postcard please

Examples of gauges/meters:

knobdemo_styles.py

Styles

Examples of selectable style combinations:

Style1_none Style2_glow Style3_glow_depression Style4_glow_depession_handleglow Style5_glow_depression_handleglow_ticks Style6_rim Style7_rim_ticks Style8_glow_ticks Style9_glow_rim_ticks StyleA_ticks StyleB_Scale_without_Ticks StyleC_Inner_Scale

Changelog:

Amended Version 2.1
    Fixed bug in mouse drag.
    Attempting to prevent unnecessary events once the meter's minimum value was hit, I failed to process the fact that it had hit the minimum - Thanks go to RichardT for pointing it out.

    Addition of variable DisableMinMaxMouseDrag - True or False - Default False
        If set to True, disables the ability, when dragging, to instantly flip from Min to Max or Max to Min
        The maximum valid mouse drag, by default, is set to 50% of the difference in degrees between start and end degrees.
        This can be adjusted by setting the control's variable 'mouse_max_move' to a value of your choice e.g.
            self.ctrl.mouse_max_move = 50
        Any attempt to drag by more than that, is cancelled.
        Obviously, this also means that you can disable mouse dragging altogether, simply by setting mouse_max_move to zero.

Amended Version 2.0

    Addition of variable UseHotSpots, which notes the dial thumb position and the Odometer position
    If the mouse is hovered over those positions whilst UseHotSpots is True, a ToolTip
        is displayed of the current value of the control for the dial thumb and the elapsed running time for the Odometer.
        The Odometer may also have Text set in addition, see below

    Addition of variable OdometerToolTip
        If given a string value this will be display when hovering over the Odometer, if SetHotSpot is active,
         in addition to the running time.

    Addition of function SetHotSpots()
        SetHotSpots toggles UseHotSpots between True and False
         additionally it turns off standard ToolTip if it is Set

    Addition of function GetHotSpots()
        returns current value of UseHotSpots

    Addition of function SetDefaultTickColour(colour)
        overrides the default tick colour, the colour used beyond the current value (normally Dark Grey)
        or the foreground colour of the parent panel.

    Addition of function GetDefaultTickColour(colour)
        returns the current default tick colour

    Addition of function SetTickRangePercentage(True/False) - Default True
        By default the tick range values given are converted to percentages to colour the gauge
        If this is set to False the values are used as discrete values
        This reinstates the original method as written by Kevin Schlosser

    Addition of variable Caption.
        If given a string value, this text is shown centred, at the foot of the meter.
        If positioned at the foot, there is only room for a single line
        The Caption gets it's colour from the DefaultTickColour.

    Addition CaptionPos = int Default 0
        The Caption by default is positioned centrally at the bottom of the gauge
        If you set this variable the caption will be positioned:
            1 - Top Left
            2 - Top Right
            3 - Bottom Left
            4 - Bottom Right
        In these positions there is more room for text, which may include newlines.

    Addition of coping with numeric keypad input of digits, as well as ordinary digits, to jump by a percentage
        0 = min value, 1 = 10% ...... 9 = 90%

    Addition of SetStartEndDegrees(start=n, end=n) - Permitting you to orient the gauge and decide how much of the circle to use
        Defaults 135.0 (7:30) and 405.0 (4:30) respectively
        Positions are specified in degrees with 0 degree angle corresponding to the positive horizontal axis (3 o’clock) direction
            following the convention in drawing wx.DC arcs.
        i.e. to set the minimum position at 9 o'clock the start angle would be 180.0
             to set the maximum position at 3 o'clock the end angle would be 360.0
        Each hourly position is +30°, so 7:30 would be 4.5 * 30 i.e. 7:30 minus 3:00 = 4.5 hours * 30 = 135.0
                                      with an end position of 4:30 = (1.5 + 12) * 30 = 405.0

        Both the Start and the End positions must both be Positive or both be Negative and the Range cannot exceed 360°

        Negative values are permitted but remember they are inverse:
            so -135° is 10:30 and -405° is 1:30

        Reversed Start and End positions are allowed, to make the gauge appear to run from positive to negative
         e.g. start 405.0 end 135.0 or -405.0 to -135.0

        i.e.
            Start   End        Min position   Max Position        Where is Midway
            135     405         7:30            4:30                Top
            405     135         4:30            7:30                Top
           -135    -405        10:30            1:30                Bottom
           -405    -135         1:30           10:30                Bottom

    Addition  GetStartEndDegrees() - returns a tuple

    Addition  SetAlwaysTickColours(True or False)
        Normally the ticks are only coloured based on the current value.
        If this is set to True, the ticks permanently have the colours assigned by the TickColourRanges and
            the current position is denoted by the current value tick, being the default tick colour.

    Addition ShowScale True or False
        ShowScale now makes a best effort to show text scale values on the gauge, without overcrowding the image
        By default the values are displayed on the exterior of the gauge

    Addition InsideScale True or False
        If set in combination with either ShowScale or ShowMinMax, the scale is displayed on the interior
         of the gauge.
        Given the restricted space available, this can be slightly hit and miss, best results are achieved
         by adjusting the tick frequency.

    Addition GaugeImage = a wx.Image
        This, much like GaugeText displays the input centrally in the gauge.
        The image should be a wx.Image and is expected to be transparent.
        The image is resized, based on the control's size, although it makes sense to ensure
         that the image is as small as is appropriate, beforehand, just for efficiency.
        If you are displaying text too, you may wish to display that further up the gauge,
         include 1 or 2 linefeeds in the text, to separate it from the image.

    Addition GaugeImagePos = int Default 0
        The image by default is positioned centrally
        If you set this variable the image will be positioned:
            1 - Top Left
            2 - Top Right
            3 - Bottom Left
            4 - Bottom Right

    Addition of EVT_SCROLL_CHANGED, activated if the value changes via SetValue()
        This caters for events to be monitored if you feed value changes into the gauge, rather than treating it
         purely as an input control.
        Events available for Binding are:
            wx.EVT_SCROLL_TOP, the gauge has hit maximum value;
            wx.EVT_SCROLL_BOTTOM, the gauge has hit minimum value;
            wx.EVT_SCROLL_LINEUP, gauge moved up one increment:;
            wx.EVT_SCROLL_LINEDOWN', gauge moved down one increment
            wx.EVT_SCROLL_PAGEUP, the user hit page up;
            wx.EVT_SCROLL_PAGEDOWN, the user hit page down;
            wx.EVT_SCROLL_THUMBRELEASE, the mouse has been released;
            wx.EVT_SCROLL_CHANGED, the gauge value has changed;
        and the catch all, wx.EVT_SCROLL, an event occurred.

    Addition of variable OdometerBackgroundColour - Default None

    Bug fixes:
    The knobStyle values have been corrected.
     Default: KNOB_GLOW | KNOB_DEPRESSION | KNOB_HANDLE_GLOW | KNOB_TICKS | KNOB_SHADOW
     Originally the knobStyle values always seemed to produce True, so everything was always turn On, as the values
      assigned were incorrect.
     The knobStyle is one of the initial parameters but can be  overridden with SetKnobStyle(...)
        e.g. self.ctrl.SetKnobStyle(knob.KNOB_TICKS | knob.KNOB_SHADOW | knob.KNOB_DEPRESSION)
      valid style values are:
        KNOB_GLOW = 1                   # Adds a neon glow around the gauge
        KNOB_DEPRESSION = 2             # Adds the central depression
        KNOB_HANDLE_GLOW = 4            # Adds a neon glow to the thumb
        KNOB_TICKS = 8                  # Adds ticks to the gauge
        KNOB_SHADOW = 16                # Adds a shadow to the gauge
        KNOB_RIM = 32                   # Adds a coloured rim to indicate the position of the gauge

        KNOB_RIM is a new style, as an alternative to KNOB_GLOW, (although they can be used together)
        Whereas KNOB_GLOW lights up the gauge rim with the relevant diffused colour range, KNOB_RIM lights up
         the gauge rim with the relevant solid colour range, only up to the current value.
         In essence it acts as a separate value indicator.

    Changes to the methods used to calculate the tick positions
        calculating tick positions for integer values was fine but trying to set the increments for fine values,
            such as a meter measuring from -1.0 to 1.0 with an increment of 0.01 and a tick frequency of 0.02,
            for example, runs into floating point Modulo issues ( a horror story in its own right)
        Either the tickfrequency could be refused or there would be missing ticks or too many ticks
        I've attempted to resolve those by importing Decimal and using round() and some other tweaks, including
         defining the required precision.
         The increment determines the precision that will be used e.g. 0.01 would set precision to 2, 0.005 to 3.
        Hopefully, I haven't broken anything. :)

    SetTickColours now handles the various ways of defining a colour e.g.
        (77, 77, 255)
        wx.Colour('#7777ff')
        '#7777ff'
        wx.BLUE
     The Bug was in setting the neon_colour for the ticks and body of the meter which expects a tuple.

    Changes:

    Change to existing function GetAverageSpeed()
     This now returns a tuple of Average value and the elapsed time period in seconds

    Change PageUp, PageDown to simple + or - 10%

    Event reporting can no longer issue double events e.g. SCROLL_PAGEUP and SCROLL_CHANGED
        Now the specific event is reported or SCROLL_CHANGED not both.

    Change TickColourRanges to cope with values < 1
        The tickcolourranges didn't handle ranges which strayed negative, they calculated a percentage of the maximum value.
        They now handle a range which goes from negative to positive e.g.
            SetTickColourRanges([10, 50, 75]) for a minimum value of -10.0 and max of 20.0,
             would set the values at -7.0, 5.0 and 12.5 as the range is actually 30 ( -10 -> 20 )
        It should also handle purely negative ranges

    GaugeText replaces SpeedoText in a variable name change - sorry about that, just more appropriate

    GaugeText has also become more vertically centrally located.
        If you wish the text to display further up the gauge include 1 or 2 linefeeds in the text.
        This is especially true if including a centrally located image with GaugeImage()

Amended Version 1.1
    Bug fix for incorrect Unbind of the odometer update timer
    Addition of a pointer spine
    Addition of variable ShowMinMax values
    Addition of variable OdometerColour

Amended version 1.0

Display optional Volume/Speed value as text
 To allow for very large values when using RPM for example, if the initial value is set as an integer
    only an integer is displayed, allowing for much larger numbers to be catered to.

Enable Right Click to jump to a position

Calculate tick colour as a percentage of the maximum value rather than fixed value

Optional pointer with shadow for Speedometer feel

Optional Speedo text, expected to be something like Mph, Kph, Rpm ft/s etc

Optional Odometer - defaults calculation to distance covered per hour

Optional odometer period unit - "H" per Hour, "M" per Minute, "S" per Second - Default "H"

Optional Odometer update period - expects a millisecond value like wxTimer
    a timer that updates the odometer irrespective of the value being changed
     based on the current Speed/Velocity
    A value > than 0 turns the odometer on, <= 0 turns it off

Plus minor adjustments

Variables added:
    ShowToolTip = True          Shows a tooltip of the Volume/Speed value
    ShowValue = True            Shows the Volume/Speed value in the centre of the widget
    ShowPointer = True          Shows a pointer indicating the Volume/Speed
    PointerColour = None        Sets the Colour of the pointer, allows for transparency
                                 The default is to use the current tick colour.
    SpeedoText = ''             Sets a short text to be displayed indicating the measurement
                                 e.g. Mph, Kph, Rpm
    ShowOdometer = False        True/False
    OdometerUpdate = 0          Value in milliseconds - Sets automatic odometer update
                                 without this the odometer is only updated on an event
                                Note:
                                    Showing the odometer with an auto update is expensive and the more
                                    frequent the update, the more expensive
                                    ***************************************
    OdometerPeriod = "H"        "H", "M" or "S"
                                 If you change this after setting it initially, the odometer readings
                                  will be nonsense, you will have a mixture of unit readings

Additional functions:

    GetOdometerUpdate()         return Odometer update period

    SetOdometerUpdate(value)    Set odometer update period in milliseconds
                                Sets or cancels the odometer depending on positive or negative value

    GetAverageSpeed()           Returns the average speed depending on the odometer period unit
                                 from program start to now (running time in secs, as of version 2.0)

    GetOdometerValue()          Returns current odometer value

    GetOdometerHistory()        Returns the history of speed changes as a list
4 Likes

That’s pretty spectacular!

The only issue I found was that I couldn’t left-drag any of the knobs all the way to their minimum value.

For example, in knobdemo_styles.py, on various attempts it stopped at 0.2, 0.1, 0.4 and 0.3, but I couldn’t drag it to 0.0. However, pressing the down arrow key did step it to 0.0.

I am using Python 3.10.12 + wxPython 4.2.1 gtk3 (phoenix) wxWidgets 3.2.2.1 on Linux Mint 21.2.

1 Like

of knob.py method _on_char_hook is more precise than _on_mouse_move :wink:
refactor or …

I was trying too hard to prevent issuing unnecessary events. :slight_smile:

So hard in fact, I didn’t process the fact that it had reached the minimum value.
Hopefully. it’s now fixed in an updated Knob.zip, without causing any new issues.

Thanks for pointing it out Richard. Herding cats is a tricky business. :wink:

Just for the record, I should perhaps mention, that this was developed and testing on Linux only.
I have no way of knowing if it will behave itself on other platforms.

1 Like

I just tested the new version and can confirm that I can now drag to the minimum value OK - thank you.

When I was testing the previous version, in my attempts to try to force it to the minimum I occasionally moved the mouse pointer closer to the maximum value and the knob responded by immediately jumping to that setting. If you were using the knob to control the volume of some actual audio gear that could be pretty disconcerting! Perhaps an option to enable/disable the wrap around effect would be useful?

Its not new issue. It is a curve.

You don’t need to tell me about a curve of a curve.
Tell me more about what you want the curve to do?

Thank you for contributing to open source, not just scraping.
Metallicow

Hi Richard

Hopefully that issue is sorted. (knob_2.1.zip)
It’s surprisingly difficult to differentiate if you’ve just dragged from the high to the low or you’re starting at the low and trying to drag higher. Turns out my ugly first thought was the winner, despite me trying numerous alternatives.
knobdemo_volume.py should now have set the variable DisableMinMaxMouseDrag to True, which should deal with the worst of it.
You can still flip from one side to the other but you have to be a lot more deliberate about it.

Regards,
Rolf

1 Like

The problematic area was the mouse positioning cutoff points for moveable start and end points, with reverse and inverted options.
The cutoff point for right clicks is always the opposite region of the halfway point between the start and end points, plus or minus the gap between those 2 points.
Allowing moveable start and end points, with reversed and inverted options, caused real headaches.
I think I’ve cracked it now but thank you for your offer.

If you feel you can improve the code, as written, in any significant way, feel free to let me know and I’ll look at it.

Regards,
Rolf

Hi Rolf

That works very nicely - thank you!

Sorry to bug you Richard but as you may have guessed, I wasn’t best pleased with the solution for preventing an accidental flip from minimum to maximum, so I’ve amended it slightly.

The maximum valid mouse drag, by default, is set to 50% of the difference in degrees
between start and end degrees.
This can be adjusted by setting the control’s variable ‘mouse_max_move’ to a value
of your choice e.g.
self.ctrl.mouse_max_move = 50

Any attempt to drag by more than that, is cancelled.
Obviously, this also means that you can disable mouse dragging altogether,
simply by setting mouse_max_move to zero.

I hope that’s an improvement.

The Circular knob meter version 2.2

Knob_2.2.zip (44.5 KB)

The changelog:

    Amended Version 2.2
        code optimisation? to dramatically reduce certain calls during OnPaint, mainly creating the tick_list
        also only bind mouse move when Hotspots are active, otherwise, the function is called needlessly.

        Change
         TickFrequency must be >= the increment
        Change
         when neon_colour is calculated, to avoid unnecessary function calls
        Change
         value is set after min_value and max_value on initial start up, to ensure that it is within those 2 values
          this prevents an illegal value being set at startup.
        Change
         Left double mouse click is treated the same as a right click i.e. jump to click position
        Change
            automatic reduction of the number of texts to be displayed, now uses slicing as is it's faster
        Change
            automatic reduction of the number of texts to be displayed, is now aware of whether you are displaying
             the text inside the gauge or outside. Outside there's a maximum of 30 texts allowed, inside only 12.
        Change
            Gauge text now takes account of the Font settings of the KnobCtrl, if set, the exception is the Odometer
            (The font size is still calculated based on the size of the control)
        Change
            An attempt has been made to limit the number of ticks to below what I consider to be excessive.
            The arbitrary limit I've set is 600 for the entire gauge and no more than 1.66 ticks per degree,
             when setting the StartEndDegrees. An over crowded gauge is not only slow but visually messy
            Additionally, the number of Large ticks attempts to stay below 30, again for visual clarity
        Addition
            DefinedScaleValues are user defined tick override values
            this is a list of values within the scope of the gauge and each divisible by the tick_frequency,
             where you want a large tick with a text value.
            It overrides the automatic calculation of ticks and tick values, replacing it with your predefined values
             e.g. self.ctrl.DefinedScaleValues = [1.0, 4.4, 9.0 11.0] will only display large ticks with those values.
            Small ticks are unaffected.
            Values that are deemed not within the gauge range or not divisible by the tick frequency, will be excluded.

Comments, bugs and abuse, on a postcard please.

Regards,
Rolf