Power BI高级交互:比较两元素的差异

这看上去是一个普通的Power BI条形图:

但是,当选中任意两个条形时,可以弹出二者之间的差异对话框:

点选的顺序决定了对话框的计算结果,比如换成先点丽水店:

动画演示:

实现原理为SVG图表结合JS交互,一个度量值生成:

代码语言:javascript代码运行次数:0运行复制
HTML Bar Chart 1 = 
VAR SelectedStores = SELECTCOLUMNS('店铺资料', "店铺名称", '店铺资料'[店铺名称], "销售业绩", [M.销售业绩])
VAR MaxValue = MAXX(SelectedStores, [销售业绩])
VAR StoreCount = COUNTROWS(SelectedStores)
VAR ChartHeight = StoreCount * 45 
VAR BarHeight = 30
VAR Margin = 15
VAR Width = 1000
VAR LeftMargin = 150
VAR RightMargin = 100
RETURN
"
<div id='chartContainer' style='width:100%; overflow:auto;'>
<svg id='barChartSVG' width='" & Width & "' height='" & ChartHeight & "' xmlns=''>
    " & 
    CONCATENATEX(
        SelectedStores,
        "
        <g class='bar-group' transform='translate(0, " & (RANKX(SelectedStores, [销售业绩], , DESC) - 1) * (BarHeight + Margin) & ")'
           data-store='" & [店铺名称] & "' 
           data-value='" & [销售业绩] & "'
           data-bar-height='" & BarHeight & "'
           data-margin='" & Margin & "'>
            <!-- 透明点击区域 -->
            <rect 
                class='click-area'
                x='0'
                y='0'
                width='" & Width & "'
                height='" & BarHeight + Margin & "'
                fill='transparent'
            />
            
            <!-- 实际条形 -->
            <rect 
                class='bar' 
                x='" & LeftMargin & "' 
                y='" & (Margin/2) & "' 
                width='" & DIVIDE([销售业绩], MaxValue, 0) * (Width - LeftMargin - RightMargin) & "' 
                height='" & BarHeight & "' 
                fill='deepskyblue'
            />
            
            <!-- 店铺名称标签 -->
            <text 
                x='" & LeftMargin - 10 & "' 
                y='" & (Margin/2) + BarHeight/2 & "' 
                text-anchor='end' 
                dominant-baseline='middle'
                font-size='14px'
                font-family='Arial'>
                " & [店铺名称] & "
            </text>
            
            <!-- 数值标签 -->
            <text 
                x='" & LeftMargin + DIVIDE([销售业绩], MaxValue, 0) * (Width - LeftMargin - RightMargin) + 10 & "' 
                y='" & (Margin/2) + BarHeight/2 & "' 
                text-anchor='start' 
                dominant-baseline='middle'
                font-size='12px'
                font-family='Arial'>
                " & FORMAT([销售业绩], "#,##0") & "
            </text>
        </g>
        ",
        UNICHAR(10)
    ) & "
</svg>
</div>
<script>
(function() {
    let selectedBars = [];
    
    function initBarChart() {
        const svg = document.getElementById('barChartSVG');
        if (!svg) {
            setTimeout(initBarChart, 100);
            return;
        }
        
        // 清除旧的事件监听器
        svg.replaceWith(svg.cloneNode(true));
        const newSvg = document.getElementById('barChartSVG');
        
        newSvg.addEventListener('click', function(event) {
            event.preventDefault();
            
            let target = event.target;
            while (target && !target.classList.contains('bar-group')) {
                target = target.parentElement;
            }
            
            if (!target) return;
            
            const store = target.getAttribute('data-store');
            const value = parseFloat(target.getAttribute('data-value'));
            const barHeight = parseFloat(target.getAttribute('data-bar-height'));
            const margin = parseFloat(target.getAttribute('data-margin'));
            const bar = target.querySelector('.bar');
            
            const index = selectedBars.findIndex(b => b.store === store);
            
            if (index === -1) {
                if (selectedBars.length >= 2) {
                    const firstBar = selectedBars[0].element;
                    firstBar.setAttribute('fill', 'deepskyblue');
                    selectedBars.shift();
                    clearComparison(newSvg);
                }
                
                selectedBars.push({ 
                    store, 
                    value, 
                    element: bar, 
                    group: target,
                    barHeight,
                    margin
                });
                bar.setAttribute('fill', '#FF5722');
                
                if (selectedBars.length === 2) {
                    drawComparison(newSvg, selectedBars[0], selectedBars[1]);
                }
            } else {
                selectedBars.splice(index, 1);
                bar.setAttribute('fill', 'deepskyblue');
                clearComparison(newSvg);
            }
        });
    }
    
    function clearComparison(svg) {
        const existingComparison = svg.querySelector('parison-bubble');
        if (existingComparison) {
            existingComparison.remove();
        }
    }
    
    function drawComparison(svg, bar1, bar2) {
        clearComparison(svg);
        
        const diff = bar1.value - bar2.value;
        const diffPercent = (diff / bar2.value) * 100;
        
        // 计算画布中心位置
        const svgWidth = parseFloat(svg.getAttribute('width'));
        const svgHeight = parseFloat(svg.getAttribute('height'));
        const centerX = svgWidth / 2;
        const centerY = svgHeight / 2;
        
        const bubbleGroup = document.createElementNS('', 'g');
        bubbleGroup.setAttribute('class', 'comparison-bubble');
        
        const bubble = document.createElementNS('', 'rect');
        const bubbleWidth = 200; 
        const bubbleHeight = 100;
        const cornerRadius = 5;
        
        bubble.setAttribute('x', centerX - bubbleWidth/2);
        bubble.setAttribute('y', centerY - bubbleHeight/2);
        bubble.setAttribute('width', bubbleWidth);
        bubble.setAttribute('height', bubbleHeight);
        bubble.setAttribute('rx', cornerRadius);
        bubble.setAttribute('ry', cornerRadius);
        bubble.setAttribute('fill', 'brown');
        bubble.setAttribute('stroke', '#3F51B5');
        bubble.setAttribute('stroke-width', '1');
        
        const foreignObject = document.createElementNS('', 'foreignObject');
        foreignObject.setAttribute('x', centerX - bubbleWidth/2 + 10);
        foreignObject.setAttribute('y', centerY - bubbleHeight/2 + 10);
        foreignObject.setAttribute('width', bubbleWidth - 20);
        foreignObject.setAttribute('height', bubbleHeight - 20);
        
        const div = document.createElementNS('', 'div');
        div.style.color = 'white';
        div.style.fontFamily = 'Arial';
        div.style.fontSize = '18px';
        div.style.lineHeight = '1.4';
        div.style.wordWrap = 'break-word';
        div.style.whiteSpace = 'normal';
        div.style.textAlign = 'center';
        
        div.textContent = `${bar1.store}比${bar2.store} ${diff >= 0 ? '多' : '少'}${Math.abs(Math.round(diff)).toLocaleString()} (${diffPercent.toFixed(1)}%)`;
        
        foreignObject.appendChild(div);
        bubbleGroup.appendChild(bubble);
        bubbleGroup.appendChild(foreignObject);
        
        svg.appendChild(bubbleGroup);
    }
    
    initBarChart();
})();
</script>
"

把度量值中的维度和指标替换为你模型中的数据,放入HTML Content视觉对象使用。

类似的原理可以扩展应用到其他图表类型,比如柱形图:

折线图:

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-04-03,如有侵权请联系 cloudcommunity@tencent 删除svgbitarget图表原理