Tag: aether

  • Add Animated Icons with Conditional Formatting

    Add Animated Icons with Conditional Formatting

    I use icons quite regularly as part of conditional formatting whether it’s to show change or highlight certain values. Working with Prism, some of those icons were custom-made. But what I didn’t realise until recently is that they don’t have to be static images.

    You can apply the same SVG animation techniques I’ve been experimenting with elsewhere, convert them to base64, and embed them directly into a Power BI theme!

    So, keep reading to learn how to add animated icons using Power BI’s conditional formatting.

    I began with a couple of static icons I’d previously built, converted them to SVG, and added a simple transform animation.

    Here’s an example SVG: a chevron that switches colours and moves left to right in a loop.

    <svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <polyline points="40,30 60,50 40,70" fill="none" stroke="#ffffff" stroke-width="8" stroke-linecap="round" stroke-linejoin="round">
        <animateTransform 
          attributeName="transform" 
          type="translate" 
          values="-8 0;8 0;-8 0" 
          dur="2s" 
          repeatCount="indefinite" />
        <animate 
          attributeName="stroke" 
          values="#ffffff;#6bfad8;#ffffff" 
          dur="2s" 
          repeatCount="indefinite" />
      </polyline>
    </svg>
    


    I recommend using svgviewer.dev a brilliant tool I’ve mentioned in previous posts.

    • Paste your SVG code into the viewer to preview the animation.
    • Switch to the Data URI tab.
    • Copy the base64 output and it’s ready to use!

    Here’s the base64 version of the chevron above (copied here for convenience):

    data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBvbHlsaW5lIHBvaW50cz0iNDAsMzAgNjAsNTAgNDAsNzAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSI4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPGFuaW1hdGVUcmFuc2Zvcm0gCiAgICAgIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgCiAgICAgIHR5cGU9InRyYW5zbGF0ZSIgCiAgICAgIHZhbHVlcz0iLTggMDs4IDA7LTggMCIgCiAgICAgIGR1cj0iMnMiIAogICAgICByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICAgIDxhbmltYXRlIAogICAgICBhdHRyaWJ1dGVOYW1lPSJzdHJva2UiIAogICAgICB2YWx1ZXM9IiNmZmZmZmY7IzZiZmFkODsjZmZmZmZmIiAKICAgICAgZHVyPSIycyIgCiAgICAgIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvcG9seWxpbmU+Cjwvc3ZnPgo=

    Now let’s embed this into a Power BI theme. If you’re already using a custom theme, you can add to it, or create a new one. Here’s a basic example with several icons:

    {
      "name": "SVGTHEME",
      "icons": {
        "GreenChev": {
          "description": "GreenChev",
          "url": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBvbHlsaW5lIHBvaW50cz0iNDAsMzAgNjAsNTAgNDAsNzAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSI4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPGFuaW1hdGVUcmFuc2Zvcm0gYXR0cmlidXRlTmFtZT0idHJhbnNmb3JtIiB0eXBlPSJ0cmFuc2xhdGUiIHZhbHVlcz0iLTggMDs4IDA7LTggMCIgZHVyPSIycyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJzdHJva2UiIHZhbHVlcz0iI2ZmZmZmZjsjNmJmYWQ4OyNmZmZmZmYiIGR1cj0iMnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvcG9seWxpbmU+Cjwvc3ZnPg=="
        },
        "PurpChev": {
          "description": "PurpChev",
          "url": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBvbHlsaW5lIHBvaW50cz0iNDAsMzAgNjAsNTAgNDAsNzAiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSI4IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPGFuaW1hdGVUcmFuc2Zvcm0gYXR0cmlidXRlTmFtZT0idHJhbnNmb3JtIiB0eXBlPSJ0cmFuc2xhdGUiIHZhbHVlcz0iLTggMDs4IDA7LTggMCIgZHVyPSIycyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+CiAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJzdHJva2UiIHZhbHVlcz0iI2ZmZmZmZjsjOWI1OWI2OyNmZmZmZmYiIGR1cj0iMnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvcG9seWxpbmU+Cjwvc3ZnPg=="
        },
        "Warn": {
          "description": "Warn",
          "url": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxzdHlsZT4KICAgICAgICAvKiBEZWZpbmUgdGhlIGFuaW1hdGlvbiBmb3IgdGhlIGV4Y2xhbWF0aW9uIG1hcmsgd2l0aGluIFNWRyBzdHlsZSAqLwogICAgICAgIC8qIE1vZGlmaWVkIGZvciBhIHNtb290aGVyIHB1bHNlIGVmZmVjdCAqLwogICAgICAgIEBrZXlmcmFtZXMgc3ZnUHVsc2UgewogICAgICAgICAgICAwJSB7CiAgICAgICAgICAgICAgICB0cmFuc2Zvcm06IHNjYWxlKDEpOwogICAgICAgICAgICAgICAgb3BhY2l0eTogMTsKICAgICAgICAgICAgfQogICAgICAgICAgICA1MCUgewogICAgICAgICAgICAgICAgdHJhbnNmb3JtOiBzY2FsZSgwLjkpOyAvKiBTbGlnaHRseSBsYXJnZXIgZm9yIGEgc3VidGxlIHB1bHNlICovCiAgICAgICAgICAgICAgICBvcGFjaXR5OiAwLjk7IC8qIFNsaWdodCBmYWRlIGZvciBhIHNvZnRlciBwdWxzZSAqLwogICAgICAgICAgICB9CiAgICAgICAgICAgIDEwMCUgewogICAgICAgICAgICAgICAgdHJhbnNmb3JtOiBzY2FsZSgxKTsKICAgICAgICAgICAgICAgIG9wYWNpdHk6IDE7CiAgICAgICAgICAgIH0KICAgICAgICB9CgogICAgICAgIC5zdmctZXhjbGFtYXRpb24tcGF0aCB7CiAgICAgICAgICAgIGZpbGw6ICNGQUYwQ0E7IC8qIFJlZCBjb2xvciBmb3IgdGhlIGV4Y2xhbWF0aW9uIG1hcmsgKi8KICAgICAgICAgICAgdHJhbnNmb3JtLW9yaWdpbjogNTBweCA1MHB4OyAvKiBDZW50ZXIgb2YgdGhlIHZpZXdCb3ggZm9yIHNjYWxpbmcgKi8KICAgICAgICAgICAgYW5pbWF0aW9uOiBzdmdQdWxzZSAxLjVzIGVhc2UtaW4tb3V0IGluZmluaXRlOyAvKiBBcHBseSB0aGUgbmV3IHB1bHNlIGFuaW1hdGlvbiAqLwogICAgICAgIH0KICAgIDwvc3R5bGU+CiAgICA8IS0tIEV4Y2xhbWF0aW9uIG1hcmsgYm9keSAtLT4KICAgIDxyZWN0IHg9IjQ1IiB5PSIxMCIgd2lkdGg9IjEwIiBoZWlnaHQ9IjYwIiByeD0iNSIgcnk9IjUiIGNsYXNzPSJzdmctZXhjbGFtYXRpb24tcGF0aCIgLz4KICAgIDwhLS0gRXhjbGFtYXRpb24gbWFyayBkb3QgLS0+CiAgICA8Y2lyY2xlIGN4PSI1MCIgY3k9Ijg1IiByPSI3IiBjbGFzcz0ic3ZnLWV4Y2xhbWF0aW9uLXBhdGgiIC8+Cjwvc3ZnPgo="
        },
        "Error": {
          "description": "Error",
          "url": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxzdHlsZT4KICAgICAgICAvKiBEZWZpbmUgdGhlIGFuaW1hdGlvbiBmb3IgdGhlIGNyb3NzIG1hcmsgKi8KICAgICAgICBAa2V5ZnJhbWVzIHN2Z1NoYWtlIHsKICAgICAgICAgICAgMCUgewogICAgICAgICAgICAgICAgdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgMTAlIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKC01ZGVnKTsgLyogUXVpY2sgcm90YXRpb24gdG8gdGhlIGxlZnQgKi8KICAgICAgICAgICAgfQogICAgICAgICAgICAyMCUgewogICAgICAgICAgICAgICAgdHJhbnNmb3JtOiByb3RhdGUoNWRlZyk7IC8qIFF1aWNrIHJvdGF0aW9uIHRvIHRoZSByaWdodCAqLwogICAgICAgICAgICB9CiAgICAgICAgICAgIDMwJSB7CiAgICAgICAgICAgICAgICB0cmFuc2Zvcm06IHJvdGF0ZSgwZGVnKTsgLyogUXVpY2sgcmV0dXJuIHRvIGNlbnRlciAqLwogICAgICAgICAgICB9CiAgICAgICAgICAgIC8qIEZyb20gMzAlIHRvIDEwMCUsIHRoZSBjcm9zcyByZW1haW5zIGF0IDAgZGVncmVlcywgY3JlYXRpbmcgYSBwYXVzZSAqLwogICAgICAgICAgICAxMDAlIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogcm90YXRlKDBkZWcpOwogICAgICAgICAgICB9CiAgICAgICAgfQoKICAgICAgICAuc3ZnLWNyb3NzLXBhdGggewogICAgICAgICAgICBmaWxsOiAjZGMyNjI2OyAvKiBSZWQgY29sb3IgZm9yIHRoZSBjcm9zcyBtYXJrICovCiAgICAgICAgICAgIHRyYW5zZm9ybS1vcmlnaW46IDUwcHggNTBweDsgLyogQ2VudGVyIG9mIHRoZSB2aWV3Qm94IGZvciByb3RhdGlvbiAqLwogICAgICAgICAgICBhbmltYXRpb246IHN2Z1NoYWtlIDJzIGVhc2UtaW4tb3V0IGluZmluaXRlOyAvKiBBcHBseSB0aGUgc2hha2UgYW5pbWF0aW9uLCB3aXRoIGEgbG9uZ2VyIGR1cmF0aW9uIHRvIGFjY29tbW9kYXRlIHRoZSBwYXVzZSAqLwogICAgICAgIH0KICAgIDwvc3R5bGU+CiAgICA8IS0tIEdyb3VwIHRvIGFwcGx5IG92ZXJhbGwgcm90YXRpb24gdG8gdGhlIGNyb3NzIC0tPgogICAgPGcgdHJhbnNmb3JtPSJyb3RhdGUoNDUgNTAgNTApIj4KICAgICAgICA8IS0tIEZpcnN0IGxpbmUgb2YgdGhlIGNyb3NzIChpbml0aWFsbHkgdmVydGljYWwsIHJvdGF0ZWQgKzQ1IGRlZ3JlZXMpIC0tPgogICAgICAgIDxyZWN0IHg9IjQ1IiB5PSIxNSIgd2lkdGg9IjEwIiBoZWlnaHQ9IjcwIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDQ1IDUwIDUwKSIgY2xhc3M9InN2Zy1jcm9zcy1wYXRoIiAvPgogICAgICAgIDwhLS0gU2Vjb25kIGxpbmUgb2YgdGhlIGNyb3NzIChpbml0aWFsbHkgaG9yaXpvbnRhbCwgcm90YXRlZCAtNDUgZGVncmVlcykgLS0+CiAgICAgICAgPHJlY3QgeD0iMTUiIHk9IjQ1IiB3aWR0aD0iNzAiIGhlaWdodD0iMTAiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTQ1IDUwIDUwKSIgY2xhc3M9InN2Zy1jcm9zcy1wYXRoIiAvPgogICAgPC9nPgo8L3N2Zz4K"
        }
      }
    }
    

    Save this as a .json file, for example: svgicons.json.

    Each icon entry includes the below:

    • Name (What you want to refer to the icon as)
    • Description (A brief description of the Icon)
    • URL (The base64 of the icon)

    If you want to add additional icons, just load them in with the same format.

    Once you have the theme ready, we need to load it into PowerBI

    • Open Power BI Desktop.
    • Go to the View tab → click the Themes dropdown → select Browse for themes.
    • Upload your .json file.

    Once the file is loaded in we have the icons ready to be used! So lets try it!

    • Select a column in your table or matrix.
    • Go to Conditional formatting → Icons.
    • Add your desired rules.
    • Scroll to the bottom of the icon list you’ll see your custom ones added from the theme.
      • Note: Previews won’t animate or show properly in the selection list so make sure your naming convention is clear!

    Once added in you should end up with something like the below:

    Here we have

    • Chevrons > (green and purple)
    • Warning ! with a pulse effect
    • Error X with a slight wobble effect

    I love finding new ways to enhance what Power BI can do natively especially when it unlocks more meaningful visualisations.

    Got questions or want to explore more ideas like this? Feel free to get in touch I’m always happy to help!

    Thanks for reading!!

  • Use SVG, HTML & CSS to enhance PowerBI reports

    Use SVG, HTML & CSS to enhance PowerBI reports

    HTML, CSS and SVGs are some of the things I know relatively well, Ive always messed around making games, animating or building apps out of various methods. In this case I wanted to understand how I can use SVG, HTML & CSS to enhance PowerBI reports.

    PowerBI whilst it has a lot of options in terms of its custom visuals, that same flexibility isnt always there. Difference is now you can use SVGs & HTML in tables, cards and all sorts just the same as if you were coding in a webpage. A lot of amazing content creators have already covered a lot of use cases for using HTML & SVG in things like tables. I wanted to do something a bit different, taking a look at those more traditional HTML methods I know for adding things like animation.

    What I have built out as part of this blog post can then be used as a base for building out pretty much anything when it comes to using SVGs and HTML. Throw in animation, changing colors anything you can think of.

    I love that these things also feed into my role as a Product Manager. I can dabble, play and find new ways to make UI / UX more exciting and engaging for my customers.

    Setting up the SVG / HTML Measure

    Firstly, for these initial cases I am just using the built in PowerBI visuals. To make this work we have to load the SVG / HTML in via a measure.

    I have created two base measures to cover common scenarios, depending on your needs (note we can not use JavaScript in either with PowerBI)

    • Using <foreignObject> to embed extended HTML and CSS inside SVG
    • Using pure SVG native elements

    FeatureSVG Native<foreignObject> (Extended HTML inside SVG)
    CSS SupportLimited to SVG and subset of CSSFull CSS including advanced layout, pseudo-elements
    Browser & tool supportVery broad, stableGood in modern browsers, less consistent in tools like Power BI
    Use casesSimple vector graphics, text, shapesComplex layouts, rich text formatting inside SVG
    PerformanceLightweight, fastSlightly heavier, potentially slower
    InteractivitySVG events and styling onlyCan use HTML events and richer interaction

    <foreignObject> Base

    HTMLBASE_FO = 
    
    -- Get the percentage value from a slicer or parameter table (assumes 0 to 1 scale)
    VAR pct = Parameter1[Parameter Value]
    
    -- Format the percentage as a whole number string with a "%" sign (e.g., "85%")
    VAR pctText = FORMAT(pct * 100, "0") & "%"
    
    -- Define the size of the SVG image
    VAR sizeWidth = 320
    VAR sizeHeight = 180
    
    -- Define dynamic colours
    VAR baseColor = "#292929"
    VAR textColor = "#f5f5f5"
    VAR subtitleColor = "#bbb"
    
    -- Build the HTML block for the foreignObject
    VAR svghtml = "
    <foreignObject x='0' y='0' width='" & sizeWidth & "' height='" & sizeHeight & "'>
      <div xmlns='http://www.w3.org/1999/xhtml'>
        <style>
          .card {
            width: 100%;
            height: 100%;
            background-color: " & baseColor & ";
            border-radius: 20px;
            padding: 24px;
            box-sizing: border-box;
            font-family: Segoe UI, sans-serif;
            display: flex;
            flex-direction: column;
            justify-content: center;
          }
          .title {
            font-size: 22px;
            font-weight: 600;
            color: " & textColor & ";
          }
          .value {
            font-size: 64px;
            font-weight: 900;
            color: " & textColor & ";
            margin-top: 4px;
          }
          .subtitle {
            font-size: 14px;
            color: " & subtitleColor & ";
            margin-top: auto;
          }
        </style>
        <div class='card'>
          <div class='title'>Title</div>
          <div class='value'>" & pctText & "</div>
          <div class='subtitle'>SubTitle</div>
        </div>
      </div>
    </foreignObject>
    "
    
    -- Wrap the foreignObject in an SVG
    VAR svg = "<svg xmlns='http://www.w3.org/2000/svg' width='" & sizeWidth & "' height='" & sizeHeight & "'>" & svghtml & "</svg>"
    
    -- Return the inline image
    RETURN "data:image/svg+xml;utf8," & svg

    SVG Native Base

    HTMLBASE_SVG = 
    VAR pct = Parameter1[Parameter Value]
    VAR pctText = FORMAT(pct * 100, "0") & "%"
    VAR sizeWidth = 320
    VAR sizeHeight = 180
    
    VAR baseColor = "#292929"
    VAR textColor = "#f5f5f5"
    VAR subtitleColor = "#bbb"
    
    VAR fillColor =
        SWITCH(
            TRUE(),
            pct < 0.2, "#E59EFB",
            pct > 0.8, "#F8FAB7",
            "#6bfad8"
        )
    
    
    -- SVG generation
    VAR svg = "
    <svg xmlns='http://www.w3.org/2000/svg' width='" & sizeWidth & "' height='" & sizeHeight & "' viewBox='0 0 " & sizeWidth & " " & sizeHeight & "'>
      <style>
        .card-base {
          fill: " & baseColor & ";
        }
        .title {
          font-family: Segoe UI, sans-serif;
          font-size: 22px;
          font-weight: 600;
          fill: " & textColor & ";
        }
        .value {
          font-family: Segoe UI, sans-serif;
          font-size: 64px;
          font-weight: 900;
          fill: " & textColor & ";
        }
        .subtitle {
          font-family: Segoe UI, sans-serif;
          font-size: 14px;
          fill: " & subtitleColor & ";
        }
      </style>
    
      <!-- Background -->
      <rect class='card-base' width='" & sizeWidth & "' height='" & sizeHeight & "' rx='20' ry='20'/>
    
      <!-- Text -->
      <text x='30' y='60' class='title'>Title</text>
      <text x='30' y='120' class='value'>" & pctText & "</text>
      <text x='30' y='160' class='subtitle'>SubTitle</text>
    
    </svg>
    "
    
    RETURN "data:image/svg+xml;utf8," & svg
    

    Adding the SVG / HTML Measure into a Card

    Next, to use this measure I have been using it in the new card visual. Drop a card onto your dashboard – for the case of this code you will need to put any value into the Data field and then disable the callout value / label.

    Once added open the “Images” section on the card settings. Change the Image type to “Image URL” and select your new measure.

    Use SVG, HTML & CSS to enhance PowerBI reports

    Modifying for your own use case

    Once you have this in place you should end up with something like this

    Now you change them around, build anything as long you keep the main building blocks of the code for SVG rendering.

    Eventually you can end up with all sorts of cool things such as the below. These are then all animated with various methods.

    Use SVG, HTML & CSS to enhance PowerBI reports

    A further example

    This code for example animated the bar in the top right with a wave effect

    HTML_Wave_Bar = 
    
    VAR pct = Parameter1[Parameter Value]
    VAR pctText = FORMAT(pct * 100, "0") & "%"
    
    VAR numBars = 40                        -- Added: number of bars for progress
    VAR barsToFill = ROUND(pct * numBars, 0)   -- Added: how many bars to fill based on pct
    
    
    VAR fillColor =                          -- Added: conditional bar color based on pct
        SWITCH(
            TRUE(),
            pct < 0.2, "#E59EFB",
            pct > 0.8, "#F8FAB7",
            "#6bfad8"
        )
    
    -- Added: Generate multiple bar divs with animation and staggered delay
    VAR barHTML = 
    CONCATENATEX (
        GENERATESERIES(1, numBars, 1),
        VAR i = [Value]
        VAR isFilled = i <= barsToFill
        VAR barColor = IF(isFilled, fillColor, "#333")
        VAR height = IF(isFilled, "100%", "40%")
        VAR animation = IF(isFilled, "waveBar 2.5s ease-in-out infinite", "none")
        VAR delay = FORMAT((i - 1) * 0.10, "0.00") & "s"
        VAR animationDelay = IF(isFilled, delay, "0s")
        RETURN "
            <div class='bar' style='
                background: " & barColor & ";
                height: " & height & ";
                animation: " & animation & ";
                animation-delay: " & animationDelay & ";
            '></div>"
    )
    
    -- Dimensions increased for full bar display (added larger SVG size)
    VAR sizeWidth = 1280
    VAR sizeHeight = 200
    
    -- Added: Style definitions including animation keyframes for bars and fade-in for label
    VAR svghtml = "
    <foreignObject x='0' y='0' width='" & sizeWidth & "' height='" & sizeHeight & "'>
    <div xmlns='http://www.w3.org/1999/xhtml'>
      <style>
        .container {
          font-family: Segoe UI, sans-serif;
          width: 90%;
          height: 150px;
          margin: auto;
          display: flex;                     
          align-items: flex-end;
          justify-content: flex-start;
          gap: 4px;                          
        }
        .bar {
          width: 20px;
          border-radius: 4px;
          transition: height 0.3s ease;      
        }
        .percentage-label {
          font-size: 48px;
          font-weight: bold;
          color: " & fillColor & ";          
          text-shadow: 0 0 6px rgba(0,0,0,0.3);
          margin-left: 30px;
          align-self: flex-end;
          animation: fadeIn 0.5s ease-in;   
        }
        @keyframes waveBar {               
          0%, 100% { height: 60%; }
          50% { height: 100%; }
        }
        @keyframes fadeIn {                 
          from { opacity: 0; transform: translateX(-10px); }
          to { opacity: 1; transform: translateX(0); }
        }
      </style>
      <div class='container'>
        " & barHTML & "                   
        <div class='percentage-label'>" & pctText & "</div>  
      </div>
    </div>
    </foreignObject>
    "
    
    -- Wrap in SVG and return as image (same concept, but bigger canvas for wave bars)
    VAR svg = "<svg xmlns='http://www.w3.org/2000/svg' width='" & sizeWidth & "' height='" & sizeHeight & "'>" & svghtml & "</svg>"
    
    RETURN "data:image/svg+xml;utf8," & svg

    Aether Repository – Use SVG, HTML & CSS to enhance PowerBI reports

    I have added these base sets of code to my Aether repo. So feel free to grab, comment and play. As I create some new animations and visuals, I will look to upload those as well

    https://github.com/AetherAdv/dax_htmlbase_FO

    https://github.com/AetherAdv/dax_htmlbase_SVG

  • Enhancing Your DevOps Reporting in Power BI

    Enhancing Your DevOps Reporting in Power BI

    My role in Product Management requires me to use DevOps a fair amount, lets just say I always have it open in one way or another. And whilst there are a number of tools out there these days to manage backlogs, roadmaps and everything else in between, it turns out DevOps and PowerBI were all I needed. This blog post focuses on enhancing your DevOps reporting in Power BI.

    The challenge

    The main connector in PowerBI to pull data from DevOps is perfect for getting started but it only pulls a basic set of data.

    The default connector for example if we take the Work Items – All history endpoint gives you the following:

    • Area Path
    • Assigned To
    • Iteration Path
    • Work Item Id
    • Title
    • State
    • Work Item Type
    • Data
    • is Current

    And this is generally fine if you want a quick view. But if you need to review priorities, further information under those user stories. All those useful data points that can make a much more effective dashboard, we need to go further.

    Luckily we can expand this data out using APIs by simply using this first initial connection as a base.

    The function

    In PowerBI if we add the following as a function thats queries the DevOps API directly, meaning we can then use the Work Item Id to get those extra details

    In the below Ive set DEVOPSORG and DEVOPSPROJECT as parameters matching up with my Organisation name and Project Name.

    https://github.com/AetherAdv/powerbi_product_devopsfunction

    let
        FetchWorkItemSummary = (workItemId as text) as table =>
        let
            // Compose the request URL with API version
            EndpointUrl = "https://dev.azure.com/DEVOPSORG/DEVOPSPROJECT/_apis/wit/workitems/" & workItemId & "?api-version=7.1-preview.3",
            
            // Attempt to retrieve JSON data from Azure DevOps
            RawResponse = try Json.Document(VSTS.Contents(EndpointUrl)) otherwise error "No details for ID " & workItemId,
            
            // Verify expected structure
            HasFieldsSection = if Record.HasFields(RawResponse, "fields") then RawResponse[fields] else error "Missing 'fields' data for item " & workItemId,
            
            // Pull out specific fields
            TaskPriority = Record.FieldOrDefault(HasFieldsSection, "Microsoft.VSTS.Common.Priority", null),
            TaskDescription = Record.FieldOrDefault(HasFieldsSection, "System.Description", null),
    
            // Convert to a table format
            Output = Table.FromRecords({
                [Priority = TaskPriority, Description = TaskDescription]
            })
        in
            Output
    in
        FetchWorkItemSummary

    As part of this code Ive then collected the Priority and the Description, so instantly gaining more useful information for reporting.

    There are of course more fields that we can then collect if needed. Use this table below to identify the field reference and drop into the function.

    Field NameField ReferenceNotes
    TitleSystem.TitleShort name of the work item
    StateSystem.StateCurrent workflow state (e.g. New, Active, Closed)
    Work Item TypeSystem.WorkItemTypeType (e.g., Bug, Task, Epic)
    Created BySystem.CreatedByUser who created the work item
    Created DateSystem.CreatedDateWhen the item was created
    Changed BySystem.ChangedByLast user who changed it
    Changed DateSystem.ChangedDateWhen the item was last updated
    Assigned ToSystem.AssignedToCurrent assignee
    TagsSystem.TagsComma-separated string of tags
    Area PathSystem.AreaPathHierarchical area assignment
    Iteration PathSystem.IterationPathSprint or iteration it belongs to
    Effort / Story PointsMicrosoft.VSTS.Scheduling.StoryPointsOften used for estimation
    Remaining WorkMicrosoft.VSTS.Scheduling.RemainingWorkTypically used in Tasks
    PriorityMicrosoft.VSTS.Common.PriorityUsed for sorting/backlog
    SeverityMicrosoft.VSTS.Common.SeverityOften used in bugs
    DescriptionSystem.DescriptionFull rich-text description

    Why?

    This data now forms the basis of dashboards I and other stakeholders rely on when reviewing the backlog, keeping up to date on the road map and much more. Enhancing your DevOps reporting in Power BI means we can get the extra fields we need for stronger reporting capability.

    Coming Next

    Next up I will show some of this data in some of the dashboards I use today, how I’m continuously improving them to support better decision making and how you can also looking to use them in your own environment.