Friday, January 8, 2016

Medusa...

Because I didn't have any summer vacation last year I had to take the whole vacation at the end of 2015 which gave me 4 weeks of spare time...
And because I really needed some new motivation I did what I can do best...gauges :)

If you take a look at JavaFX you will find a nice feature set which also contains charts but unfortunately you won't find gauges. This was the reason why I've started porting gauges from Java Swing to the JFXtras Project 4 years ago. And that was also the reason why I've started the Enzo project. Those are nice projects but I always wanted to create something more general and so I've started another JavaFX library...Medusa



So where are the differences between Enzo and Medusa you might ask yourself, well there are a few differences...

1. No CSS in the standard GaugeSkin
When I've started with JavaFX I was totally convinced of the code only approach (means all graphics are done in code). After a year or two I've started to use CSS and used the Region+CSS approach for nearly all of my controls. This is totally fine and it works...but the more I thought about it the more unconvinced I became. The reasons are as follows, the idea to use CSS in JavaFX was to improve the Designer-Developer workflow and I like this. But the CSS approach in JavaFX is not Standard CSS which means no Designer really understands it directly which means it will end at the Developer side again. The other problem is that the CSS approach in JavaFX is based on CSS 2.1 and not on CSS 3.0. This means it is only a part of the current CSS spec. So why should I learn this part of CSS if I can also do it in code? I totally agree that CSS is fine to style an application and I will continue to use it where it makes sense. But if you create something like a gauge with all it's different options you will end up with a huge amount of boilerplate code only to support CSS where the code approach is much easier to use. It's no problem to use CSS in your own Medusa Skin, just to make that clear :)
You can simply use the .gauge CSS selector.

2. One Control different Skins
The Medusa library contains only one Gauge Control with a default GaugeSkin. The main idea is to have a more or less complete set of properties that will work for most of the gauges and create different Skins for the different use cases. The standard Medusa GaugeSkin comes without any fancy style but only with the basics that are really needed like Tick Marks, Tick Labels, Sections, Areas, Markers and a Needle. It also comes without a background and a border (they are just set to transparent). The reason for this simple Skin is the fact that you can use this to embed it in your own controls or apps very easily. Means you could create a CSS stylable JavaFX Control that comes with it's own Skin and embeds a Medusa Control to visualize the Tick Marks etc. I've put some very basic quick hacks in the skins folder of the project to give you an idea (the FramedGauge definitely needs some improvements because it uses CSS inlining UPDATE: Now there is the FGauge class which uses Borders instead of CSS inlining). If you know the Enzo library you will recognize that I've also ported some of the Enzo Skins to Medusa and I will port more of them in the future.

3. Better scales
The most complex part of a Gauge is the scale, it sounds easy but it could get complex. So you want to define a start angle and an angle range, apply a given value range to it and would like to see a nice scale right? Well I did my best to make that happen in Medusa. In Enzo I used the same approach but it did not work with ranges like 0 - 1 or 0 - 10. So I've found and fixed that problem in Medusa which means you will hopefully get nice scales for your given range of values. Another new feature is the ability to create scales with a counter clockwise direction. This is not always needed but sometimes it is really useful. This feature is at the moment only available in the standard Medusa GaugeSkin and not in the other Skins.

4. Different usage of properties
All properties of the Gauge Control are implemented with "shadow" variables which are using primitives. Only if you call the property method the properties will be initialized. There are only three exceptions

  • value (contains the value that was set by the setValue method)
  • currentValue (contains the current value during animations)
  • oldValue (contains the old value)

So in principle every property is implemented as follows...


private String         _title = "";
private StringProperty title;



public String getTitle() { return null == title ? _title : title.get(); }
public void setTitle(final String TITLE) {
    if (null == title) {
        _title = TITLE;
    } else {
        title.set(TITLE);
    }
    fireUpdateEvent(REDRAW_EVENT);
}
public StringProperty titleProperty() {
    if (null == title) { title = new SimpleStringProperty(Gauge.this, "title", _title); }
    return title;
}

To make sure that events will also be fired when using the primitives in the setTitle() method I use my own events that will always be fired. The advantage is that I really don't need to initialize the JavaFX properties as long as I don't call the titleProperty() method. This could be helpful when using the library on embedded devices. On a desktop machine you won't notice a difference but on an embedded device you could see an effect when initializing a lot of properties. I'm not 100% sure if this approach works in all use cases but time will tell :)


But now let's take a look at the Medusa standard gauge...



As mentioned it doesn't look really fancy and that's on purpose. To create this kind of gauge you simply can use the following code:


Gauge gauge = GaugeBuilder.create().build();


Ok, that's not really impressive isn't it? So let's play around with the standard Medusa GaugeSkin a bit...

Let's try the following features...

  • Scale direction CCW (CLOCKWISE, COUNTER_CLOCKWISE)
  • Tick labels outside of tick marks (OUTSIDE, INSIDE)
  • Start at the bottom (0-360)
  • Angle range of 270° (0-360)
  • Range from -1 to 2
  • Highlight the 0 (zeroColor property)
  • Get other types for major tickmarks ( LINE, DOT, TRIANGLE, TICK_LABEL)
So to get this gauge you need the following code...


Gauge gauge = GaugeBuilder.create()
                          .scaleDirection(ScaleDirection.COUNTER_CLOCKWISE)
                          .tickLabelLocation(TickLabelLocation.OUTSIDE)
                          .startAngle(0)
                          .angleRange(270)
                          .minValue(-1)
                          .maxValue(2)
                          .zeroColor(Color.ORANGE)
                          .majorTickMarkType(TickMarkType.TRIANGLE)
                          .build();


And the result will look like this...




So as you can see the angle starts at the bottom (startAngle == 0) and the direction is counter clockwise. You could now also add Sections, Areas and Markers where Sections will be drawn like a part of a doughnut and Areas like slices of a pie. For Markers you can choose between three different MarkerTypes. You could also disable the visibility of the minor-, medium- and major tick marks which sometimes makes sense.
For the needle I have only one version implemented at the moment but more will come in the future.
To make it at least a bit more interesting you could set the needle color and choose between three values for the needle size (THIN, MEDIUM, THICK). This could be helpful if the gauge is very small.
So, another one...


Gauge gauge = GaugeBuilder.create()
                          .prefSize(500,500)
                          .scaleDirection(ScaleDirection.COUNTER_CLOCKWISE)
                          .tickLabelLocation(TickLabelLocation.OUTSIDE)
                          .startAngle(0)
                          .angleRange(270)
                          .minValue(-1)
                          .maxValue(2)
                          .zeroColor(Color.ORANGE)
                          .majorTickMarkType(TickMarkType.TRIANGLE)
                          .sectionsVisible(true)
                          .sections(new Section(1.5, 2, Color.rgb(200, 0, 0, 0.5)))
                          .areasVisible(true)
                          .areas(new Section(-0.5, 0.5, Color.rgb(0, 200, 0, 0.5)))
                          .markersVisible(true)
                          .markers(new Marker(0.75, "Marker 1", Color.MAGENTA))
                          .needleColor(Color.DARKCYAN)
                          .needleSize(NeedleSize.THICK)
                          .build();

And the result will look like this...




Well these are just a few options that are available at the moment and I will add more, so please stay tuned.
But like I mentioned before I've also ported some Skins from Enzo to Medusa and also created new Skins. 
So here is a little screenshot of what is available at the moment...



The new Skins are 

  • BulletChartSkin
  • SpaceXSkin
  • ModernSkin
The Gauge in the upper right corner is good for visualizing KPI's (Key Performance Indicators) but that really depends on the user. An analyst mostly prefers a BulletChart because it saves space and doesn't use fancy graphics. But when you talk to Managers who would like to show the KPI's in a meeting you will find that they most of the times prefer Gauges because they look better. And because a BulletChart visualizes the same data as a Gauge I decided to also add a BulletChartSkin to Medusa. To make use of the Skins I've added the possibility to select the Skin the GaugeBuilder. If you would like to create the Gauge in the upper right corner you simply do the following


Gauge gauge = GaugeBuilder.create()
                          .skin(SimpleSkin.class)
                          .sections(new Section(0, 16.66666, "0", Color.web("#11632f")),
                                    new Section(16.66666, 33.33333, "1", Color.web("#36843d")),
                                    new Section(33.33333, 50.0, "2", Color.web("#67a328")),
                                    new Section(50.0, 66.66666, "3", Color.web("#80b940")),
                                    new Section(66.66666, 83.33333, "4", Color.web("#95c262")),
                                    new Section(83.33333, 100.0, "5", Color.web("#badf8d")))
                          .title("Simple")
                          .threshold(50)
                          .animated(true)
                          .build();


And you will get the following result...


To create a BulletChart with the same data you simply change the parameter in the skin() call from SimpleSkin.class to BulletChartSkin.class like this...


Gauge gauge = GaugeBuilder.create()
                          .skin(BulletChartSkin.class)
                          .sections(new Section(0, 16.66666, "0", Color.web("#11632f")),
                                    new Section(16.66666, 33.33333, "1", Color.web("#36843d")),
                                    new Section(33.33333, 50.0, "2", Color.web("#67a328")),
                                    new Section(50.0, 66.66666, "3", Color.web("#80b940")),
                                    new Section(66.66666, 83.33333, "4", Color.web("#95c262")),
                                    new Section(83.33333, 100.0, "5", Color.web("#badf8d")))
                          .title("Simple")
                          .threshold(50)
                          .animated(true)
                          .build();


And instead of the Gauge you will get this nice little BulletChart...



Each Skin needs a special set of parameters to be set to the correct values to make sure it looks ok. For example the SimpleSkin uses white for the value and title where the standard color of the GaugeSkin is nearly black.
So if you use the GaugeBuilder to create your Gauge, the GaugeBuilder will take about setting the needed default values for you which makes it easier for you.

Another new Skin is the SpaceXSkin which I saw when the Space X Falcon 9 started to it's liftoff and landing maneuver last month. It's a simple flat UI gauge with just a few features. So if you set a threshold value and a threshold color it will visualize the bar after the threshold with the given color. The interesting part was to visualize the bar when it exceeds the threshold because it changes it's color. So to use the SpaceXSkin you again use the .skin() method in the GaugeBuilder as follows.


Gauge gauge = GaugeBuilder.create()
                    .skin(SpaceXSkin.class)
                    .animated(true)
                    .title("SpaceX")
                    .unit("km/h")
                    .maxValue(30000)
                    .threshold(25000)
                    .build();


And the result will look like follows...



Like I said a really simple Skin but it also just has 8 nodes on the Scene Graph which makes it great for dashboards full of Gauges :)

The last new Skin that I've added is the ModernSkin which is my personal favourite, here I use another new approach where I highlight the tick marks which value is smaller than the current value. I saw that design in an app on my iPhone and copied most of it but also added more features to it. So this is the way you initialize it...


Gauge gauge = GaugeBuilder.create()
                         .skin(ModernSkin.class)
                         .sections(new Section(85, 90, "", Color.rgb(204, 0, 0, 0.5)),
                                   new Section(90, 95, "", Color.rgb(204, 0, 0, 0.75)),
                                   new Section(95, 100, "", Color.rgb(204, 0, 0)))
                         .title("MODERN")
                         .unit("UNIT")
                         .threshold(85)
                         .thresholdVisible(true)
                         .animated(true)
                         .build();


And this is how it will look like...


This gauge doesn't work for everything but form some use cases it might be really nice. It also only comes in the color scheme above but I'm also working on a bright scheme.


All other Skins are ports from Enzo and not really new but I will blog about the all controls within the next weeks but for today I guess that's it.

Here are the links to the source and binaries
The binaries will also be IS available on Maven Central , it just takes some time to make it happen... :)

That's the work of my vacation and I had a lot of fun creating it, from now on I will spend most of my spare time in Medusa and will continue adding features, new Skins and blogposts.

Stay tuned and keep coding... 


14 comments:

  1. Hmmm, a single control with multiple skins for each gauge, where have I seen that before? ;-)

    ReplyDelete
    Replies
    1. I know Tom, I know, I should have put the stuff in JFXtras labs but you know me...I need the freedom to try things without being bound to given constraints. :)

      Delete
  2. Nah, just kidding. I tried to pull you back in and failed. No problem.

    But it's interesting to see that you are now using a single control with many skins as well. But also differently; by moving the graphical part in the control's API. In JFXtras I only have the data part in the control and all the graphical stuff in CSS. Many approaches to generating the same image. :-)

    ReplyDelete
  3. That's very awesome, thanks a lot for sharing!

    I have a question, if you don't mind. Is there a reason why you use "magic" numbers?

    Example:

    knob.bezierCurveTo(0.29411764705882354 * w, 0.8823529411764706 * h, 0.35294117647058826 * w, 0.9411764705882353 * h, 0.5294117647058824 * w, 0.9411764705882353 * h);

    These are rather specific than just some e. g. "start at 30% of the total width".

    By the way, regarding your great looking gauges, maybe the Martian UI Reel Demo is interesting for you: https://vimeo.com/142143279

    ReplyDelete
    Replies
    1. Hmm...where is the difference between start at 30% of total width to 0.29411 * w? Except int vs float? The problem is that converting vector drawings to code and make them scaleable you often end up with those magic numbers

      Delete
    2. Ah, that's where they come from. Thanks for the quick reply!

      Delete
  4. Thanks a lot!
    I really like the "modern" skin, and would like to turn it into a clock for Android Wear devices. Do you allow me to do so?
    Thanks in advance.

    ReplyDelete
    Replies
    1. Everything is under the Apache 2.0 license...so feel free to go...and send me the watch face :)

      Delete
    2. Ok, thank you Gerrit!
      I will give you the Play Store link as soon as it is published.

      Romain

      Delete
  5. This comment has been removed by a blog administrator.

    ReplyDelete
  6. Hi Gerrit, here you can find my watchface : https://play.google.com/store/apps/details?id=dev.niamor.gaugewatchface

    Romain

    ReplyDelete
  7. Hi there,
    Well I won't add operation controls to Medusa but I could create a new lib that contains those controls. You might want to contact me via mail to discuss this outside this blog :)
    Cheers,
    Gerrit

    ReplyDelete
  8. Hi Gerrit,

    Really like the gauges, but I'm having a little bit of trouble getting sections in the "Simple" gauge GUI. I'm trying to use Scene Builder and I don't know how exactly to add sections. Also when I try to use the GaugeBuilder the skin class seems to be missing in Medusa 6.2

    Any suggestion or insight?

    Cheers,
    Zain

    ReplyDelete
    Replies
    1. Hi Zain,
      Because I nearly never use SceneBuilder the library is not optimized for it. Here is some code that definitely works with Medusa 7.3 which is the latest release. I hope it will help you solve your problem...

      Gauge gauge = GaugeBuilder.create()
      .skinType(SkinType.SIMPLE)
      .sections(new Section(0, 16.66666, "0", Color.web("#11632f")),
      new Section(16.66666, 33.33333, "1", Color.web("#36843d")),
      new Section(33.33333, 50.0, "2", Color.web("#67a328")),
      new Section(50.0, 66.66666, "3", Color.web("#80b940")),
      new Section(66.66666, 83.33333, "4", Color.web("#95c262")),
      new Section(83.33333, 100.0, "5", Color.web("#badf8d")))
      .title("Simple")
      .sectionTextVisible(true)
      .threshold(50)
      .animated(true)
      .build();

      Cheers,
      Gerrit

      Delete