Hills and Valleys Lables - yOffset not working

Hi all,

I’m trying to extend the “hills and valleys” visual by Daniel Marsh-Patrick posted here:
https://coacervo.co/deneb_hill_valley

It is already looking pretty promising but I would like to add the data labels to the chart, depending on an calculated offset value.

I’m able to display the values but I’m it looks like the offset value gets incorrectly interpreted and I’m stuck on how to fix it.

If I replace:

        "yOffset": {
          "expr": "datum['act_label_offset']"
        }

with a fixed value, it is working, but I need this to be dynamic.

My file can be found here:
Custom Hills and Valley.pbix (3.5 MB)

My code is:

{
  "data": {"name": "dataset"},
  "encoding": {
    "x": {
      "field": "Month",
      "type": "temporal"
    },
    "y": {"type": "quantitative"}
  },
  "layer": [
    {
      "description": "Target area - background",
      "mark": {
        "type": "area",
        "style": "delta_negative"
      },
      "encoding": {
        "y": {"field": "Sales PY"}
      }
    },
    {
      "description": "Actual area - masks out target where necessary",
      "mark": {
        "type": "area",
        "style": "delta_positive"
      },
      "encoding": {
        "y": {"field": "Sales"}
      }
    },
    {
      "description": "Masking layer (with interpolated points)",
      "transform": [
        {
          "calculate": "min(datum['Sales PY'] , datum['Sales'])",
          "as": "low_value"
        },
        {
          "window": [
            {
              "op": "lead",
              "field": "Month",
              "as": "month_following"
            },
            {
              "op": "lead",
              "field": "Sales",
              "as": "actual_following"
            },
            {
              "op": "lead",
              "field": "Sales PY",
              "as": "target_following"
            }
          ]
        },
        {
          "calculate": "(datum['actual_following'] - datum['Sales']) / (datum['month_following'] - datum['Month'])",
          "as": "actual_slope"
        },
        {
          "calculate": "(datum['target_following'] - datum['Sales PY']) / (datum['month_following'] - datum['Month'])",
          "as": "target_slope"
        },
        {
          "calculate": "datum['Sales'] - (datum['actual_slope'] * datum['Month'])",
          "as": "actual_y_intercept"
        },
        {
          "calculate": "datum['Sales PY'] - (datum['target_slope'] * datum['Month'])",
          "as": "target_y_intercept"
        },
        {
          "calculate": "(datum['target_y_intercept'] - datum['actual_y_intercept']) / (datum['actual_slope'] - datum['target_slope'])",
          "as": "intersect_base"
        },
        {
          "calculate": "datum['intersect_base'] > datum['Month'] && datum['intersect_base'] < datum['month_following']",
          "as": "intersect_before_following"
        },
        {
          "calculate": "datum['intersect_before_following'] ? datetime(datum['intersect_base']) : null",
          "as": "intersect_x"
        },
        {
          "calculate": "datum['intersect_before_following'] ? (datum['actual_slope'] * datum['intersect_base']) + datum['actual_y_intercept'] : null",
          "as": "intersect_y"
        },
        {
          "calculate": "datum['Sales'] !== 0 ? datum['Sales'] >= datum['Sales PY'] : null",
          "as": "delta_positive"
        },
        {
          "calculate": "datum['Sales'] !== 0 ? (!datum['delta_positive'] ? 'bottom' : 'top') : null",
          "as": "act_label_baseline"
        },
        {
          "calculate": "datum['Sales'] !== 0 ? (!datum['delta_positive'] ? -15 : 15) : null",
          "as": "act_label_offset"
        },
        {
          "fold": [
            "Month",
            "intersect_x"
          ]
        },
        {
          "filter": "datum['value'] !== null"
        },
        {
          "calculate": "datum['key'] === 'Month' ? datum['Month'] : datum['intersect_x']",
          "as": "x"
        },
        {
          "calculate": "datum['key'] === 'Month' ? datum['low_value'] : datum['intersect_y']",
          "as": "y"
        }
      ],
      "mark": {
        "type": "area",
        "style": "mask_foreground"
      },
      "encoding": {
        "x": {"field": "x"},
        "y": {"field": "y"}
      }
    },
    {
      "description": "PY Line",
      "mark": {
        "type": "line",
        "style": "py_line"
      },
      "encoding": {
        "y": {"field": "Sales PY"}
      }
    },
    {
      "description": "Actual Line",
      "mark": {
        "type": "line",
        "style": "actual_line"
      },
      "encoding": {
        "y": {"field": "Sales"}
      }
    },
    {
      "description": "Actual Dots",
      "mark": {
        "type": "point",
        "style": "actual_dots"
      },
      "encoding": {
        "y": {"field": "Sales"}
      }
    },
    {
      "description": "Actual Label",
      "mark": {
        "type": "text",
        "baseline": {
          "expr": "datum['act_label_baseline']"
        },
        "yOffset": {
          "expr": "datum['act_label_offset']"
        }
      },
      "encoding": {
        "y": {"field": "Sales"},
        "text": {
          "field": "Sales",
          "format": "#,##0",
          "formatType": "pbiFormat"
        }
      }
    }
  ]
}

Many thanks!

Sebastian

Hi @sjurzyk79

The yOffset calculation for the “text” mark (with description “Actual Label”) is currently referencing the “datum[‘act_label_offset’]” field which is calculated in the transform block for a different mark in the layer block (the “area” mark with description “Masking layer (with interpolated points)”).

To use a calculated field, the mark must be able to “see” it, so it would need to be calculated in the specific mark block or included in a shared encoding block outside the layer block.

I’ve added a transform block to the “text” mark (with description “Actual Label”) and recalculated the positive, baseline, and offset values (using a “temp” prefix) and adjusted the mark property expressions accordingly; the labels are now visible and positioned above for positives and below for negatives.

(I also adjusted the colour of the actuals line to be lighter (using the style\actual_line in the config) to increase the legibility of the labels.)

Hope it helps.
Greg
eDNA Forum - Deneb yOffset.pbix (3.5 MB)

2 Likes

Thank you, Greg, for your swift response and support. Encouraged by your and Daniel’s example, I’ve delved into exploring Deneb and have gained extensive knowledge from scratch by actively participating in this forum and watching YouTube tutorials.

Currently, I’m focused on recreating several ICBS charts from the web to deepen my understanding of Deneb and SVG visuals. As Power BI continues to improve, the hope is that reliance on custom visuals like these will diminish over time, given the typically smoother experience with native visuals (even though replicating features often demands a significant number of measures :smiley:).

A heartfelt appreciation to you and the entire community!

1 Like