Forum Discussion

distra's avatar
distra
Frequent Visitor
1 year ago
Solved

Dynamic y-Axis calculation with Deneb

Hi everyone, hi giammariam,

I have recently started exploring Deneb Visuals. In my particular case I want to display Average Cost per Month while offering to switch between currencies on my Dashboard. So I decided to try and adapt a Deneb template for a KPI Card with a line chart, that shows the KPI for the month the user hovers over: Here is what I (almost) want it to look like:

 

 

This is what I achieved with the Template and some help by Claude and Copilot.
However, although I usually advocate for the zero-based approach to show the actual dimensions, in this case the changes month over month are so small, that the customers of this dashboard want this chart to be zoomed in on the area around the max and min values to actually see changes.

 

I calculated max and min  boundaries (global max + 20% of the global min value & global min value - 20% of global min value) but I can't get the axis to adapt to the borders I defined. To test the functionality I wanted to provide static values first to set my boundaries and after that apply my calculated values. But even with the static values I don't get to the desired result.

 

When I add the following section to my y-Encoding, my generated KPI card does not display any axis anymore.

My data range is from 228 to 232, so that should well be within the defined range.

"scale": {
    "zero": false,
    "domain": [150,250]
},

If I use an even wider range like [80, 300] the Axis re-appears but will not use the entire range of the Line Chart anymore. Plus it also does not correctly scale the axis.

 

 

Do I have a wrong understanding of the domain attribute? 

I am using Vega-Lite with a Specification- and a Config-File, I can provide both codes in full if needed.

 

Thanks for any help!

 

 

 

  • Hi distra,

     

    The area mark is designed to have a zero baseline, so even if you cut the domain, it is trying to plot the remainder of the shape outside the domain (as the second y-value is zero). It would work with a line mark as there is only a single y-coordinate to worry about.

     

    So, what is happening here is that your y-scale and axis are being correctly set to the desired range, but the size of your chart is condensing the vertical scale into the equivalent amount of space. You can see it "kind of" working if you make the visual container larger, e.g.:

     

    (ignore the 'undefined'; that's a copy/paste issue with your sample data at my end)

     

    One way to resolve this is to instruct Vega-Lite to clip the area mark. You should be able to make the following changes to your spec:

     

    1. Update the scale as follows (removing "zero": false), e.g.:

    {
      ...
      "encoding": {   
        "y": {
          "field": "Avg Rate dynamic Currency",
          "type": "quantitative",
          "scale": {
            "domain": [80, 235]  // or whatever you need min/max to be
          },
          ...
        },
        ...
      }
      ...
    }

     

    2. Update the area mark definition to include a clip as follows:

     

    {
      ...
      "layer": [
        {
          "description": "sparkline definition",
          "mark": {
            "type":"area",
            "clip": true
          },
          ...
        },
        ...
      }
      ...
    }

     

    This will now cut the baseline of the plotted area, e.g.:

     

    Hopefully this should be all you need to resolve. Good luck!

     

    (and thanks for using Deneb!)

     

    Daniel

17 Replies

  • v-menakakota's avatar
    v-menakakota
    Community Support

    Hi distra ,
    Thanks for reaching out to the Microsoft fabric community forum. 

    Make sure the values you're plotting actually fall within the domain [150, 250] in the layer where you're applying the scale.

    In layered visuals like KPI cards, ensure the scale override is set on the correct layer, ideally the one rendering the line/chart.

    Make sure that axis is not set to null or being turned off in your encoding.

     

    If I misunderstand your needs or you still have problems on it, please feel free to let us know.  

    Best Regards, 
    Community Support Team 

    • distra's avatar
      distra
      Frequent Visitor

      Thank you v-menakakota,

       

      all values are well within that range, these are my current values:

       

      Maybe I did not understand the concept of layers well enough to define the axis correctly. THis is my code that defines the encoding:

       

      "encoding": {   
          "y": {
            "field": "Avg Rate dynamic Currency",
            "type": "quantitative",     
            "scale": {
              "zero": false,
              "domain": [80,300]
            },
            "axis": {
              "title": null,
              "labels": true,
              "ticks": true,
              "grid": true,
              "domain": true,
              "labelColor": "#605E5C",
              "labelFont": "Segoe UI Semibold",
              "labelFontSize": 10,
              "tickColor": "#E1DFDD",
              "gridColor": "#F3F2F1",
              "domainColor": "#E1DFDD"
            }
          },
          "x": {
            "field": "Month",
            "type": "temporal"
          }
        },

      But actually, this is not defined inside the layer definitions, maybe that's the mistake?

  • dm-p's avatar
    dm-p
    Super User

    Hi distra,

     

    The area mark is designed to have a zero baseline, so even if you cut the domain, it is trying to plot the remainder of the shape outside the domain (as the second y-value is zero). It would work with a line mark as there is only a single y-coordinate to worry about.

     

    So, what is happening here is that your y-scale and axis are being correctly set to the desired range, but the size of your chart is condensing the vertical scale into the equivalent amount of space. You can see it "kind of" working if you make the visual container larger, e.g.:

     

    (ignore the 'undefined'; that's a copy/paste issue with your sample data at my end)

     

    One way to resolve this is to instruct Vega-Lite to clip the area mark. You should be able to make the following changes to your spec:

     

    1. Update the scale as follows (removing "zero": false), e.g.:

    {
      ...
      "encoding": {   
        "y": {
          "field": "Avg Rate dynamic Currency",
          "type": "quantitative",
          "scale": {
            "domain": [80, 235]  // or whatever you need min/max to be
          },
          ...
        },
        ...
      }
      ...
    }

     

    2. Update the area mark definition to include a clip as follows:

     

    {
      ...
      "layer": [
        {
          "description": "sparkline definition",
          "mark": {
            "type":"area",
            "clip": true
          },
          ...
        },
        ...
      }
      ...
    }

     

    This will now cut the baseline of the plotted area, e.g.:

     

    Hopefully this should be all you need to resolve. Good luck!

     

    (and thanks for using Deneb!)

     

    Daniel

    • distra's avatar
      distra
      Frequent Visitor

      Hi Daniel,

       

      thank you for reaching out, this works like a charm! I used it with the same boundaries I had provided for the example chart with native Power BI visuals and it looks great!


      I was expecting it to be a simple task to replace the static values by my pre-calculated values, but it seems like Vega-Lite won't let me use the calculations in the domain directly.

      I have made the following update to my transform section. Is there a way to directly reference my calculated upper and lower boundaries in the domain?

      "transform": [
          {
            "joinaggregate": [
              {
                "op": "max",
                "field": "Month",
                "as": "max_period"
              },
              {
                "op": "max",
                "field": "Avg Rate dynamic Currency",
                "as": "max_avg_rate"
              },
              {
                "op": "min",
                "field": "Avg Rate dynamic Currency",
                "as": "min_avg_rate"
              }
            ]
          },
          {
            "calculate": "datum.max_avg_rate - datum.min_avg_rate",
            "as": "delta_max_min"
          },
          {
            "calculate": "datum.max_avg_rate + datum.delta_max_min",
            "as": "upper_limit_yAxis"
          },
          {
            "calculate": "datum.min_avg_rate - datum.delta_max_min",
            "as": "lower_limit_yAxis"
          }
        ],

       

      Thanks a lot!

      Dirk

      • dm-p's avatar
        dm-p
        Super User

        I'm a bit short on time as it's later here, but you can do this with the extent transform, which will let you hoist your min/max to a top-level signal (called params in Vega-Lite). These are easier to access in expressions, which you can use in the scale domain.

         

        Here's a quick, minimal example that you can use as a reference. This clips the area to the extents of the values (in conjunction with the clip approach discussed previously):

         

         

        {
          "$schema": "https://vega.github.io/schema/vega-lite/v6.json",
          "data": {
            "url": "data/stocks.csv"
          },
          "transform": [
            {
              "filter": "datum.symbol==='GOOG'"
            },
            {
              "extent": "price",
              "param": "y_extents"
            }
          ],
          "mark": {
            "type": "area",
            "clip": true
          },
          "encoding": {
            "x": {
              "field": "date",
              "type": "temporal"
            },
            "y": {
              "field": "price",
              "type": "quantitative",
              "scale": {
                "domain": [{"expr": "y_extents[0]"}, {"expr": "y_extents[1]"}]
              }
            }
          }
        }

         

        You can explore this in the Vega Editor here.

         

        The pertinent parts are as follows:

         

        {
          ...
          "transform": [
            ...
            {
              "extent": "price",
              "param": "y_extents"
            }
          ],
          ...
        }

         

        This is a calculation that will take the min/max of a specified field and store them as a variable. You can see this in the Signal pane (in either Deneb or the Vega Editor), e.g.:

         

         

        This is an array that can then be referenced in the domain evaluation, e.g.:

        {
          "encoding": {
            ...
            "y": {
              ...
              "scale": {
                "domain": [
                  {"expr": "y_extents[0]"},
                  {"expr": "y_extents[1]"}
                ]
              }
            }
          }
        }

         

        This is an example of extraction of the direction values, but you could also use downstream params to manipulate these values (such as adding a padding factor). this is similar to a transform in a data stream.

         

        Anyway, I hope this is clear and provides you with some ideas on how to apply it to your design. If you need me to implement your specification exactly, it would be helpful if you could supply an example .pbix file, and I can review it when I'm next back online.

         

        Good luck!

         

        Daniel

  • distra's avatar
    distra
    Frequent Visitor

    Vega Lite Specification

    {
      "description": "Area chart with text updated when hovering over a point. Author: Joanna Sekiewicz",
      "title": "Average Rate by Month",
      "data": {"name": "dataset"}, 
      "transform": [
        {
          "joinaggregate": [
            {
              "op": "max",
              "field": "Month",
              "as": "max_period"
            }
          ]
        },
        {
          "calculate": "datum.max_avg_rate + datum.min_avg_rate * 0.4",
          "as": "upper_limit_yAxis"
        },
        {
          "calculate": "datum.min_avg_rate + datum.min_avg_rate * 0.4",
          "as": "lower_limit_yAxis"
        }
      ],
      "encoding": {   
        "y": {
          "field": "Avg Rate dynamic Currency",
          "type": "quantitative",     
          "scale": {
            "zero": false,
            "domain": [80,300]
          },
          "axis": {
            "title": null,
            "labels": true,
            "ticks": true,
            "grid": true,
            "domain": true,
            "labelColor": "#605E5C",
            "labelFont": "Segoe UI Semibold",
            "labelFontSize": 10,
            "tickColor": "#E1DFDD",
            "gridColor": "#F3F2F1",
            "domainColor": "#E1DFDD"
          }
        },
        "x": {
          "field": "Month",
          "type": "temporal"
        }
      },
      "layer": [
        {
          "description": "sparkline definition",
          "mark": "area",
          "encoding": {
            "y": {"field": "Avg Rate dynamic Currency"}
          }
        },
        {
          "description": "points activated by hovering over",
          "params": [
            {
              "name": "hoverPoint",
              "select": {
                "type": "point",
                "on": "pointermove",
                "encodings": ["x"],
                "clear": "pointerout",
                "nearest": true
              }
            }
          ],
          "encoding": {
            "y": {"field": "Avg Rate dynamic Currency"},
            "opacity": {"value": 0}
          },
          "mark": {"type": "point"}
        },
        {
          "description": "details for hovered point (or the last one)",
          "transform": [
            {
              "calculate": "isDefined(hoverPoint.Month) ? hoverPoint.Month[0] == datum.Month : datum.Month==datum.max_period",
              "as": "displayDetails"
            },
            {
              "filter": "datum.displayDetails"
            }
          ],
          "layer": [
            {
              "description": "point referring to displayed details",
              "mark": "point",
              "encoding": {
                "opacity": {"value": 1},
                "y": {"field": "Avg Rate dynamic Currency"}
              }
            },
            {
              "mark": "rule",
              "encoding": {
                "opacity": {
                  "condition": {
                    "param": "hoverPoint",
                    "empty": false,
                    "value": 1
                  },
                  "value": 0
                }
              }
            },
            {
              "mark": {
                "type": "text",
                "size": 28,
                "fontWeight": "normal",
                "color": "#2D7A89"
              },
              "encoding": {
                "text": {
                  "field": "Avg Rate formatted"
                },
                "x": {"value": -10},
                "y": {"value": -50}
              }
            },
            {
              "mark": {
                "type": "text",
                "size": 13
              },
              "encoding": {
                "text": {
                  "field": "Month",
                  "format": "%b-%Y",
                  "formatType": "time"
                },
                "x": {"value": -10},
                "y": {"value": -25}
              }
            }
          ]
        }
      ]
    }

     

    Config

    {
      "view": {
        "stroke": "transparent",
        "fill": "transparent"
      },
      "font": "Segoe UI semibold",
      "area": {
        "line": true,
        "interpolate": "monotone",
        "color": {
          "gradient": "linear",
          "x1": 1,
          "y1": 1,
          "x2": 1,
          "y2": 0,
          "stops": [
            {"offset": 0, "color": "#ffffff00"},
            {
              "offset": 1,
              "color": "#2D7A89A0"
            }
          ]
        }
      },
      "rule": {
        "strokeWidth": 0.3,
        "color": "grey"
      },
      "line": {
        "interpolate": "monotone",
        "color": "#2D7A89",
        "strokeWidth": 3,
        "strokeCap": "round",
        "strokeJoin": "round"
      },
      "point": {
        "filled": true,
        "size": 85,
        "color": "#2D7A89"
      },
      "text": {
        "align": "left",
        "fill": "#605E5C"
      },
      "axis": {
        "title": null,
        "labels": false,
        "ticks": false,
        "grid": false,
        "domain": false
      },
      "title": {
        "anchor": "start",
        "dx": 0,
        "dy": -8,
        "fontSize": 18.5,
        "fontWeight": "bold",
        "color": "#252423"
      }
    }