mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-21 19:05:13 +00:00
Add Power BI resources (#298)
* Add Power BI resources: 4 chat modes, 6 instructions, 4 prompts, and resources README * Remove power-bi-resources-README.md - not needed for PR * Add Power BI Development collection * Fix PR review feedback: Add collection YAML file and remove double fenced code blocks - Add power-bi-development.collection.yml with proper metadata - Remove outer 4-backtick fences from all Power BI files (chatmodes, instructions, prompts) - Files now have only the standard 3-backtick fences for proper GitHub Copilot compatibility * Remove outer code fences from Power BI chatmode files
This commit is contained in:
committed by
GitHub
parent
7786c82cad
commit
38969f7cc2
810
instructions/power-bi-custom-visuals-development.instructions.md
Normal file
810
instructions/power-bi-custom-visuals-development.instructions.md
Normal file
@@ -0,0 +1,810 @@
|
||||
---
|
||||
description: 'Comprehensive Power BI custom visuals development guide covering React, D3.js integration, TypeScript patterns, testing frameworks, and advanced visualization techniques.'
|
||||
applyTo: '**/*.{ts,tsx,js,jsx,json,less,css}'
|
||||
---
|
||||
|
||||
# Power BI Custom Visuals Development Best Practices
|
||||
|
||||
## Overview
|
||||
This document provides comprehensive instructions for developing custom Power BI visuals using modern web technologies including React, D3.js, TypeScript, and advanced testing frameworks, based on Microsoft's official guidance and community best practices.
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
### 1. Project Initialization
|
||||
```typescript
|
||||
// Install Power BI visuals tools globally
|
||||
npm install -g powerbi-visuals-tools
|
||||
|
||||
// Create new visual project
|
||||
pbiviz new MyCustomVisual
|
||||
cd MyCustomVisual
|
||||
|
||||
// Start development server
|
||||
pbiviz start
|
||||
```
|
||||
|
||||
### 2. TypeScript Configuration
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"types": ["react", "react-dom"],
|
||||
"allowJs": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"outDir": "./.tmp/build/",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"lib": [
|
||||
"es2015",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"./src/visual.ts"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Core Visual Development Patterns
|
||||
|
||||
### 1. Basic Visual Structure
|
||||
```typescript
|
||||
"use strict";
|
||||
import powerbi from "powerbi-visuals-api";
|
||||
|
||||
import DataView = powerbi.DataView;
|
||||
import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
|
||||
import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
|
||||
import IVisual = powerbi.extensibility.visual.IVisual;
|
||||
import IVisualHost = powerbi.extensibility.IVisualHost;
|
||||
|
||||
import "./../style/visual.less";
|
||||
|
||||
export class Visual implements IVisual {
|
||||
private target: HTMLElement;
|
||||
private host: IVisualHost;
|
||||
|
||||
constructor(options: VisualConstructorOptions) {
|
||||
this.target = options.element;
|
||||
this.host = options.host;
|
||||
}
|
||||
|
||||
public update(options: VisualUpdateOptions) {
|
||||
const dataView: DataView = options.dataViews[0];
|
||||
|
||||
if (!dataView) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Visual update logic here
|
||||
}
|
||||
|
||||
public getFormattingModel(): powerbi.visuals.FormattingModel {
|
||||
return this.formattingSettingsService.buildFormattingModel(this.formattingSettings);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Data View Processing
|
||||
```typescript
|
||||
// Single data mapping example
|
||||
export class Visual implements IVisual {
|
||||
private valueText: HTMLParagraphElement;
|
||||
|
||||
constructor(options: VisualConstructorOptions) {
|
||||
this.target = options.element;
|
||||
this.host = options.host;
|
||||
this.valueText = document.createElement("p");
|
||||
this.target.appendChild(this.valueText);
|
||||
}
|
||||
|
||||
public update(options: VisualUpdateOptions) {
|
||||
const dataView: DataView = options.dataViews[0];
|
||||
const singleDataView: DataViewSingle = dataView.single;
|
||||
|
||||
if (!singleDataView || !singleDataView.value ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.valueText.innerText = singleDataView.value.toString();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## React Integration
|
||||
|
||||
### 1. React Visual Setup
|
||||
```typescript
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import ReactCircleCard from "./component";
|
||||
|
||||
export class Visual implements IVisual {
|
||||
private target: HTMLElement;
|
||||
private reactRoot: React.ComponentElement<any, any>;
|
||||
|
||||
constructor(options: VisualConstructorOptions) {
|
||||
this.reactRoot = React.createElement(ReactCircleCard, {});
|
||||
this.target = options.element;
|
||||
|
||||
ReactDOM.render(this.reactRoot, this.target);
|
||||
}
|
||||
|
||||
public update(options: VisualUpdateOptions) {
|
||||
const dataView: DataView = options.dataViews[0];
|
||||
|
||||
if (dataView) {
|
||||
const reactProps = this.parseDataView(dataView);
|
||||
this.reactRoot = React.createElement(ReactCircleCard, reactProps);
|
||||
ReactDOM.render(this.reactRoot, this.target);
|
||||
}
|
||||
}
|
||||
|
||||
private parseDataView(dataView: DataView): any {
|
||||
// Transform Power BI data for React component
|
||||
return {
|
||||
data: dataView.categorical?.values?.[0]?.values || [],
|
||||
categories: dataView.categorical?.categories?.[0]?.values || []
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. React Component with Props
|
||||
```typescript
|
||||
// React component for Power BI visual
|
||||
import * as React from "react";
|
||||
|
||||
export interface ReactCircleCardProps {
|
||||
data: number[];
|
||||
categories: string[];
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export const ReactCircleCard: React.FC<ReactCircleCardProps> = (props) => {
|
||||
const { data, categories, size = 200, color = "#3498db" } = props;
|
||||
|
||||
const maxValue = Math.max(...data);
|
||||
const minValue = Math.min(...data);
|
||||
|
||||
return (
|
||||
<div className="react-circle-card">
|
||||
{data.map((value, index) => {
|
||||
const radius = ((value - minValue) / (maxValue - minValue)) * size / 2;
|
||||
return (
|
||||
<div key={index} className="data-point">
|
||||
<div
|
||||
className="circle"
|
||||
style={{
|
||||
width: radius * 2,
|
||||
height: radius * 2,
|
||||
backgroundColor: color,
|
||||
borderRadius: '50%'
|
||||
}}
|
||||
/>
|
||||
<span className="label">{categories[index]}: {value}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactCircleCard;
|
||||
```
|
||||
|
||||
## D3.js Integration
|
||||
|
||||
### 1. D3 with TypeScript
|
||||
```typescript
|
||||
import * as d3 from "d3";
|
||||
type Selection<T extends d3.BaseType> = d3.Selection<T, any, any, any>;
|
||||
|
||||
export class Visual implements IVisual {
|
||||
private svg: Selection<SVGElement>;
|
||||
private container: Selection<SVGElement>;
|
||||
private host: IVisualHost;
|
||||
|
||||
constructor(options: VisualConstructorOptions) {
|
||||
this.host = options.host;
|
||||
this.svg = d3.select(options.element)
|
||||
.append('svg')
|
||||
.classed('visual-svg', true);
|
||||
|
||||
this.container = this.svg
|
||||
.append('g')
|
||||
.classed('visual-container', true);
|
||||
}
|
||||
|
||||
public update(options: VisualUpdateOptions) {
|
||||
const dataView = options.dataViews[0];
|
||||
|
||||
if (!dataView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = options.viewport.width;
|
||||
const height = options.viewport.height;
|
||||
|
||||
this.svg
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
// D3 data binding and visualization logic
|
||||
this.renderChart(dataView, width, height);
|
||||
}
|
||||
|
||||
private renderChart(dataView: DataView, width: number, height: number): void {
|
||||
const data = this.transformData(dataView);
|
||||
|
||||
// Create scales
|
||||
const xScale = d3.scaleBand()
|
||||
.domain(data.map(d => d.category))
|
||||
.range([0, width])
|
||||
.padding(0.1);
|
||||
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain([0, d3.max(data, d => d.value)])
|
||||
.range([height, 0]);
|
||||
|
||||
// Bind data and create bars
|
||||
const bars = this.container.selectAll('.bar')
|
||||
.data(data);
|
||||
|
||||
bars.enter()
|
||||
.append('rect')
|
||||
.classed('bar', true)
|
||||
.merge(bars)
|
||||
.attr('x', d => xScale(d.category))
|
||||
.attr('y', d => yScale(d.value))
|
||||
.attr('width', xScale.bandwidth())
|
||||
.attr('height', d => height - yScale(d.value))
|
||||
.style('fill', '#3498db');
|
||||
|
||||
bars.exit().remove();
|
||||
}
|
||||
|
||||
private transformData(dataView: DataView): any[] {
|
||||
// Transform Power BI DataView to D3-friendly format
|
||||
const categorical = dataView.categorical;
|
||||
const categories = categorical.categories[0];
|
||||
const values = categorical.values[0];
|
||||
|
||||
return categories.values.map((category, index) => ({
|
||||
category: category.toString(),
|
||||
value: values.values[index] as number
|
||||
}));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Advanced D3 Patterns
|
||||
```typescript
|
||||
// Complex D3 visualization with interactions
|
||||
export class AdvancedD3Visual implements IVisual {
|
||||
private svg: Selection<SVGElement>;
|
||||
private tooltip: Selection<HTMLDivElement>;
|
||||
private selectionManager: ISelectionManager;
|
||||
|
||||
constructor(options: VisualConstructorOptions) {
|
||||
this.host = options.host;
|
||||
this.selectionManager = this.host.createSelectionManager();
|
||||
|
||||
// Create main SVG
|
||||
this.svg = d3.select(options.element)
|
||||
.append('svg');
|
||||
|
||||
// Create tooltip
|
||||
this.tooltip = d3.select(options.element)
|
||||
.append('div')
|
||||
.classed('tooltip', true)
|
||||
.style('opacity', 0);
|
||||
}
|
||||
|
||||
private createInteractiveElements(data: VisualDataPoint[]): void {
|
||||
const circles = this.svg.selectAll('.data-circle')
|
||||
.data(data);
|
||||
|
||||
const circlesEnter = circles.enter()
|
||||
.append('circle')
|
||||
.classed('data-circle', true);
|
||||
|
||||
circlesEnter.merge(circles)
|
||||
.attr('cx', d => d.x)
|
||||
.attr('cy', d => d.y)
|
||||
.attr('r', d => d.radius)
|
||||
.style('fill', d => d.color)
|
||||
.style('stroke', d => d.strokeColor)
|
||||
.style('stroke-width', d => `${d.strokeWidth}px`)
|
||||
.on('click', (event, d) => {
|
||||
// Handle selection
|
||||
this.selectionManager.select(d.selectionId, event.ctrlKey);
|
||||
})
|
||||
.on('mouseover', (event, d) => {
|
||||
// Show tooltip
|
||||
this.tooltip
|
||||
.style('opacity', 1)
|
||||
.style('left', (event.pageX + 10) + 'px')
|
||||
.style('top', (event.pageY - 10) + 'px')
|
||||
.html(`${d.category}: ${d.value}`);
|
||||
})
|
||||
.on('mouseout', () => {
|
||||
// Hide tooltip
|
||||
this.tooltip.style('opacity', 0);
|
||||
});
|
||||
|
||||
circles.exit().remove();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Visual Features
|
||||
|
||||
### 1. Custom Formatting Model
|
||||
```typescript
|
||||
import { formattingSettings } from "powerbi-visuals-utils-formattingmodel";
|
||||
|
||||
export class VisualFormattingSettingsModel extends formattingSettings.CompositeFormattingSettingsModel {
|
||||
// Color settings card
|
||||
public colorCard: ColorCardSettings = new ColorCardSettings();
|
||||
|
||||
// Data point settings card
|
||||
public dataPointCard: DataPointCardSettings = new DataPointCardSettings();
|
||||
|
||||
// General settings card
|
||||
public generalCard: GeneralCardSettings = new GeneralCardSettings();
|
||||
|
||||
public cards: formattingSettings.SimpleCard[] = [this.colorCard, this.dataPointCard, this.generalCard];
|
||||
}
|
||||
|
||||
export class ColorCardSettings extends formattingSettings.SimpleCard {
|
||||
name: string = "colorCard";
|
||||
displayName: string = "Color";
|
||||
|
||||
public defaultColor: formattingSettings.ColorPicker = new formattingSettings.ColorPicker({
|
||||
name: "defaultColor",
|
||||
displayName: "Default color",
|
||||
value: { value: "#3498db" }
|
||||
});
|
||||
|
||||
public showAllDataPoints: formattingSettings.ToggleSwitch = new formattingSettings.ToggleSwitch({
|
||||
name: "showAllDataPoints",
|
||||
displayName: "Show all",
|
||||
value: false
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Interactivity and Selections
|
||||
```typescript
|
||||
import { interactivitySelectionService, baseBehavior } from "powerbi-visuals-utils-interactivityutils";
|
||||
|
||||
export interface VisualDataPoint extends interactivitySelectionService.SelectableDataPoint {
|
||||
value: powerbi.PrimitiveValue;
|
||||
category: string;
|
||||
color: string;
|
||||
selectionId: ISelectionId;
|
||||
}
|
||||
|
||||
export class VisualBehavior extends baseBehavior.BaseBehavior<VisualDataPoint> {
|
||||
protected bindClick() {
|
||||
// Implement click behavior for data point selection
|
||||
this.behaviorOptions.clearCatcher.on('click', () => {
|
||||
this.selectionHandler.handleClearSelection();
|
||||
});
|
||||
|
||||
this.behaviorOptions.elementsSelection.on('click', (event, dataPoint) => {
|
||||
event.stopPropagation();
|
||||
this.selectionHandler.handleSelection(dataPoint, event.ctrlKey);
|
||||
});
|
||||
}
|
||||
|
||||
protected bindContextMenu() {
|
||||
// Implement context menu behavior
|
||||
this.behaviorOptions.elementsSelection.on('contextmenu', (event, dataPoint) => {
|
||||
this.selectionHandler.handleContextMenu(
|
||||
dataPoint ? dataPoint.selectionId : null,
|
||||
{
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
}
|
||||
);
|
||||
event.preventDefault();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Landing Page Implementation
|
||||
```typescript
|
||||
export class Visual implements IVisual {
|
||||
private element: HTMLElement;
|
||||
private isLandingPageOn: boolean;
|
||||
private LandingPageRemoved: boolean;
|
||||
private LandingPage: d3.Selection<any>;
|
||||
|
||||
constructor(options: VisualConstructorOptions) {
|
||||
this.element = options.element;
|
||||
}
|
||||
|
||||
public update(options: VisualUpdateOptions) {
|
||||
this.HandleLandingPage(options);
|
||||
}
|
||||
|
||||
private HandleLandingPage(options: VisualUpdateOptions) {
|
||||
if(!options.dataViews || !options.dataViews[0]?.metadata?.columns?.length){
|
||||
if(!this.isLandingPageOn) {
|
||||
this.isLandingPageOn = true;
|
||||
const SampleLandingPage: Element = this.createSampleLandingPage();
|
||||
this.element.appendChild(SampleLandingPage);
|
||||
this.LandingPage = d3.select(SampleLandingPage);
|
||||
}
|
||||
} else {
|
||||
if(this.isLandingPageOn && !this.LandingPageRemoved){
|
||||
this.LandingPageRemoved = true;
|
||||
this.LandingPage.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createSampleLandingPage(): Element {
|
||||
const landingPage = document.createElement("div");
|
||||
landingPage.className = "landing-page";
|
||||
landingPage.innerHTML = `
|
||||
<div class="landing-page-content">
|
||||
<h2>Custom Visual</h2>
|
||||
<p>Add data to get started</p>
|
||||
<div class="landing-page-icon">📊</div>
|
||||
</div>
|
||||
`;
|
||||
return landingPage;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Framework
|
||||
|
||||
### 1. Unit Testing Setup
|
||||
```typescript
|
||||
// Webpack configuration for testing
|
||||
const path = require('path');
|
||||
const webpack = require("webpack");
|
||||
|
||||
module.exports = {
|
||||
devtool: 'source-map',
|
||||
mode: 'development',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader'
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/i,
|
||||
enforce: 'post',
|
||||
include: path.resolve(__dirname, 'src'),
|
||||
exclude: /(node_modules|resources\/js\/vendor)/,
|
||||
loader: 'coverage-istanbul-loader',
|
||||
options: { esModules: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
externals: {
|
||||
"powerbi-visuals-api": '{}'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js', '.css']
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, ".tmp/test")
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
'powerbi-visuals-api': null
|
||||
})
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Visual Testing Utilities
|
||||
```typescript
|
||||
// Test utilities for Power BI visuals
|
||||
export class VisualTestUtils {
|
||||
public static d3Click(element: JQuery, x: number, y: number): void {
|
||||
const event = new MouseEvent('click', {
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
button: 0
|
||||
});
|
||||
element[0].dispatchEvent(event);
|
||||
}
|
||||
|
||||
public static d3KeyEvent(element: JQuery, typeArg: string, keyArg: string, keyCode: number): void {
|
||||
const event = new KeyboardEvent(typeArg, {
|
||||
key: keyArg,
|
||||
code: keyArg,
|
||||
keyCode: keyCode
|
||||
});
|
||||
element[0].dispatchEvent(event);
|
||||
}
|
||||
|
||||
public static createVisualHost(): IVisualHost {
|
||||
return {
|
||||
createSelectionIdBuilder: () => new SelectionIdBuilder(),
|
||||
createSelectionManager: () => new SelectionManager(),
|
||||
colorPalette: new ColorPalette(),
|
||||
eventService: new EventService(),
|
||||
tooltipService: new TooltipService()
|
||||
} as IVisualHost;
|
||||
}
|
||||
|
||||
public static createUpdateOptions(dataView: DataView, viewport?: IViewport): VisualUpdateOptions {
|
||||
return {
|
||||
dataViews: [dataView],
|
||||
viewport: viewport || { width: 500, height: 500 },
|
||||
operationKind: VisualDataChangeOperationKind.Create,
|
||||
type: VisualUpdateType.Data
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Component Testing
|
||||
```typescript
|
||||
// Jest test for React component
|
||||
import * as React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ReactCircleCard from '../src/component';
|
||||
|
||||
describe('ReactCircleCard', () => {
|
||||
const mockProps = {
|
||||
data: [10, 20, 30],
|
||||
categories: ['A', 'B', 'C'],
|
||||
size: 200,
|
||||
color: '#3498db'
|
||||
};
|
||||
|
||||
test('renders with correct data points', () => {
|
||||
render(<ReactCircleCard {...mockProps} />);
|
||||
|
||||
expect(screen.getByText('A: 10')).toBeInTheDocument();
|
||||
expect(screen.getByText('B: 20')).toBeInTheDocument();
|
||||
expect(screen.getByText('C: 30')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('applies correct styling', () => {
|
||||
render(<ReactCircleCard {...mockProps} />);
|
||||
|
||||
const circles = document.querySelectorAll('.circle');
|
||||
expect(circles).toHaveLength(3);
|
||||
|
||||
circles.forEach(circle => {
|
||||
expect(circle).toHaveStyle('backgroundColor: #3498db');
|
||||
expect(circle).toHaveStyle('borderRadius: 50%');
|
||||
});
|
||||
});
|
||||
|
||||
test('handles empty data gracefully', () => {
|
||||
const emptyProps = { ...mockProps, data: [], categories: [] };
|
||||
const { container } = render(<ReactCircleCard {...emptyProps} />);
|
||||
|
||||
expect(container.querySelector('.data-point')).toBeNull();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### 1. Dialog Box Implementation
|
||||
```typescript
|
||||
import DialogConstructorOptions = powerbi.extensibility.visual.DialogConstructorOptions;
|
||||
import DialogAction = powerbi.DialogAction;
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import * as React from 'react';
|
||||
|
||||
export class CustomDialog {
|
||||
private dialogContainer: HTMLElement;
|
||||
|
||||
constructor(options: DialogConstructorOptions) {
|
||||
this.dialogContainer = options.element;
|
||||
this.initializeDialog();
|
||||
}
|
||||
|
||||
private initializeDialog(): void {
|
||||
const dialogContent = React.createElement(DialogContent, {
|
||||
onSave: this.handleSave.bind(this),
|
||||
onCancel: this.handleCancel.bind(this)
|
||||
});
|
||||
|
||||
ReactDOM.render(dialogContent, this.dialogContainer);
|
||||
}
|
||||
|
||||
private handleSave(data: any): void {
|
||||
// Process save action
|
||||
this.closeDialog(DialogAction.Save, data);
|
||||
}
|
||||
|
||||
private handleCancel(): void {
|
||||
// Process cancel action
|
||||
this.closeDialog(DialogAction.Cancel);
|
||||
}
|
||||
|
||||
private closeDialog(action: DialogAction, data?: any): void {
|
||||
// Close dialog with action and optional data
|
||||
powerbi.extensibility.visual.DialogUtils.closeDialog(action, data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Conditional Formatting Integration
|
||||
```typescript
|
||||
import powerbiVisualsApi from "powerbi-visuals-api";
|
||||
import { ColorHelper } from "powerbi-visuals-utils-colorutils";
|
||||
|
||||
export class Visual implements IVisual {
|
||||
private colorHelper: ColorHelper;
|
||||
|
||||
constructor(options: VisualConstructorOptions) {
|
||||
this.colorHelper = new ColorHelper(
|
||||
options.host.colorPalette,
|
||||
{ objectName: "dataPoint", propertyName: "fill" },
|
||||
"#3498db" // Default color
|
||||
);
|
||||
}
|
||||
|
||||
private applyConditionalFormatting(dataPoints: VisualDataPoint[]): VisualDataPoint[] {
|
||||
return dataPoints.map(dataPoint => {
|
||||
// Get conditional formatting color
|
||||
const color = this.colorHelper.getColorForDataPoint(dataPoint.dataViewObject);
|
||||
|
||||
return {
|
||||
...dataPoint,
|
||||
color: color,
|
||||
strokeColor: this.darkenColor(color, 0.2),
|
||||
strokeWidth: 2
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private darkenColor(color: string, amount: number): string {
|
||||
// Utility function to darken a color for stroke
|
||||
const colorObj = d3.color(color);
|
||||
return colorObj ? colorObj.darker(amount).toString() : color;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Tooltip Integration
|
||||
```typescript
|
||||
import { createTooltipServiceWrapper, TooltipEventArgs, ITooltipServiceWrapper } from "powerbi-visuals-utils-tooltiputils";
|
||||
|
||||
export class Visual implements IVisual {
|
||||
private tooltipServiceWrapper: ITooltipServiceWrapper;
|
||||
|
||||
constructor(options: VisualConstructorOptions) {
|
||||
this.tooltipServiceWrapper = createTooltipServiceWrapper(
|
||||
options.host.tooltipService,
|
||||
options.element
|
||||
);
|
||||
}
|
||||
|
||||
private addTooltips(selection: d3.Selection<any, VisualDataPoint, any, any>): void {
|
||||
this.tooltipServiceWrapper.addTooltip(
|
||||
selection,
|
||||
(tooltipEvent: TooltipEventArgs<VisualDataPoint>) => {
|
||||
const dataPoint = tooltipEvent.data;
|
||||
return [
|
||||
{
|
||||
displayName: "Category",
|
||||
value: dataPoint.category
|
||||
},
|
||||
{
|
||||
displayName: "Value",
|
||||
value: dataPoint.value.toString()
|
||||
},
|
||||
{
|
||||
displayName: "Percentage",
|
||||
value: `${((dataPoint.value / this.totalValue) * 100).toFixed(1)}%`
|
||||
}
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### 1. Data Reduction Strategies
|
||||
```json
|
||||
// Visual capabilities with data reduction
|
||||
"dataViewMappings": {
|
||||
"categorical": {
|
||||
"categories": {
|
||||
"for": { "in": "category" },
|
||||
"dataReductionAlgorithm": {
|
||||
"window": {
|
||||
"count": 300
|
||||
}
|
||||
}
|
||||
},
|
||||
"values": {
|
||||
"group": {
|
||||
"by": "series",
|
||||
"select": [{
|
||||
"for": {
|
||||
"in": "measure"
|
||||
}
|
||||
}],
|
||||
"dataReductionAlgorithm": {
|
||||
"top": {
|
||||
"count": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Efficient Rendering Patterns
|
||||
```typescript
|
||||
export class OptimizedVisual implements IVisual {
|
||||
private animationFrameId: number;
|
||||
private renderQueue: (() => void)[] = [];
|
||||
|
||||
public update(options: VisualUpdateOptions) {
|
||||
// Queue render operation instead of immediate execution
|
||||
this.queueRender(() => this.performUpdate(options));
|
||||
}
|
||||
|
||||
private queueRender(renderFunction: () => void): void {
|
||||
this.renderQueue.push(renderFunction);
|
||||
|
||||
if (!this.animationFrameId) {
|
||||
this.animationFrameId = requestAnimationFrame(() => {
|
||||
this.processRenderQueue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private processRenderQueue(): void {
|
||||
// Process all queued render operations
|
||||
while (this.renderQueue.length > 0) {
|
||||
const renderFunction = this.renderQueue.shift();
|
||||
if (renderFunction) {
|
||||
renderFunction();
|
||||
}
|
||||
}
|
||||
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
|
||||
private performUpdate(options: VisualUpdateOptions): void {
|
||||
// Use virtual DOM or efficient diffing strategies
|
||||
const currentData = this.transformData(options.dataViews[0]);
|
||||
|
||||
if (this.hasDataChanged(currentData)) {
|
||||
this.renderVisualization(currentData);
|
||||
this.previousData = currentData;
|
||||
}
|
||||
}
|
||||
|
||||
private hasDataChanged(newData: any[]): boolean {
|
||||
// Efficient data comparison
|
||||
return JSON.stringify(newData) !== JSON.stringify(this.previousData);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Remember: Custom visual development requires understanding both Power BI's visual framework and modern web development practices. Focus on creating reusable, testable, and performant visualizations that enhance the Power BI ecosystem.
|
||||
@@ -0,0 +1,639 @@
|
||||
---
|
||||
description: 'Comprehensive Power BI data modeling best practices based on Microsoft guidance for creating efficient, scalable, and maintainable semantic models using star schema principles.'
|
||||
applyTo: '**/*.{pbix,md,json,txt}'
|
||||
---
|
||||
|
||||
# Power BI Data Modeling Best Practices
|
||||
|
||||
## Overview
|
||||
This document provides comprehensive instructions for designing efficient, scalable, and maintainable Power BI semantic models following Microsoft's official guidance and dimensional modeling best practices.
|
||||
|
||||
## Star Schema Design Principles
|
||||
|
||||
### 1. Fundamental Table Types
|
||||
**Dimension Tables** - Store descriptive business entities:
|
||||
- Products, customers, geography, time, employees
|
||||
- Contain unique key columns (preferably surrogate keys)
|
||||
- Relatively small number of rows
|
||||
- Used for filtering, grouping, and providing context
|
||||
- Support hierarchical drill-down scenarios
|
||||
|
||||
**Fact Tables** - Store measurable business events:
|
||||
- Sales transactions, website clicks, manufacturing events
|
||||
- Contain foreign keys to dimension tables
|
||||
- Numeric measures for aggregation
|
||||
- Large number of rows (typically growing over time)
|
||||
- Represent specific grain/level of detail
|
||||
|
||||
```
|
||||
Example Star Schema Structure:
|
||||
|
||||
DimProduct (Dimension) FactSales (Fact) DimCustomer (Dimension)
|
||||
├── ProductKey (PK) ├── SalesKey (PK) ├── CustomerKey (PK)
|
||||
├── ProductName ├── ProductKey (FK) ├── CustomerName
|
||||
├── Category ├── CustomerKey (FK) ├── CustomerType
|
||||
├── SubCategory ├── DateKey (FK) ├── Region
|
||||
└── UnitPrice ├── SalesAmount └── RegistrationDate
|
||||
├── Quantity
|
||||
DimDate (Dimension) └── DiscountAmount
|
||||
├── DateKey (PK)
|
||||
├── Date
|
||||
├── Year
|
||||
├── Quarter
|
||||
├── Month
|
||||
└── DayOfWeek
|
||||
```
|
||||
|
||||
### 2. Table Design Best Practices
|
||||
|
||||
#### Dimension Table Design
|
||||
```
|
||||
✅ DO:
|
||||
- Use surrogate keys (auto-incrementing integers) as primary keys
|
||||
- Include business keys for integration purposes
|
||||
- Create hierarchical attributes (Category > SubCategory > Product)
|
||||
- Use descriptive names and proper data types
|
||||
- Include "Unknown" records for missing dimension data
|
||||
- Keep dimension tables relatively narrow (focused attributes)
|
||||
|
||||
❌ DON'T:
|
||||
- Use natural business keys as primary keys in large models
|
||||
- Mix fact and dimension characteristics in same table
|
||||
- Create unnecessarily wide dimension tables
|
||||
- Leave missing values without proper handling
|
||||
```
|
||||
|
||||
#### Fact Table Design
|
||||
```
|
||||
✅ DO:
|
||||
- Store data at the most granular level needed
|
||||
- Use foreign keys that match dimension table keys
|
||||
- Include only numeric, measurable columns
|
||||
- Maintain consistent grain across all fact table rows
|
||||
- Use appropriate data types (decimal for currency, integer for counts)
|
||||
|
||||
❌ DON'T:
|
||||
- Include descriptive text columns (these belong in dimensions)
|
||||
- Mix different grains in the same fact table
|
||||
- Store calculated values that can be computed at query time
|
||||
- Use composite keys when surrogate keys would be simpler
|
||||
```
|
||||
|
||||
## Relationship Design and Management
|
||||
|
||||
### 1. Relationship Types and Best Practices
|
||||
|
||||
#### One-to-Many Relationships (Standard Pattern)
|
||||
```
|
||||
Configuration:
|
||||
- From Dimension (One side) to Fact (Many side)
|
||||
- Single direction filtering (Dimension filters Fact)
|
||||
- Mark as "Assume Referential Integrity" for DirectQuery performance
|
||||
|
||||
Example:
|
||||
DimProduct (1) ← ProductKey → (*) FactSales
|
||||
DimCustomer (1) ← CustomerKey → (*) FactSales
|
||||
DimDate (1) ← DateKey → (*) FactSales
|
||||
```
|
||||
|
||||
#### Many-to-Many Relationships (Use Sparingly)
|
||||
```
|
||||
When to Use:
|
||||
✅ Genuine many-to-many business relationships
|
||||
✅ When bridging table pattern is not feasible
|
||||
✅ For advanced analytical scenarios
|
||||
|
||||
Best Practices:
|
||||
- Create explicit bridging tables when possible
|
||||
- Use low-cardinality relationship columns
|
||||
- Monitor performance impact carefully
|
||||
- Document business rules clearly
|
||||
|
||||
Example with Bridging Table:
|
||||
DimCustomer (1) ← CustomerKey → (*) BridgeCustomerAccount (*) ← AccountKey → (1) DimAccount
|
||||
```
|
||||
|
||||
#### One-to-One Relationships (Rare)
|
||||
```
|
||||
When to Use:
|
||||
- Extending dimension tables with additional attributes
|
||||
- Degenerate dimension scenarios
|
||||
- Separating PII from operational data
|
||||
|
||||
Implementation:
|
||||
- Consider consolidating into single table if possible
|
||||
- Use for security/privacy separation
|
||||
- Maintain referential integrity
|
||||
```
|
||||
|
||||
### 2. Relationship Configuration Guidelines
|
||||
```
|
||||
Filter Direction:
|
||||
✅ Single Direction: Default choice, best performance
|
||||
✅ Both Directions: Only when cross-filtering is required for business logic
|
||||
❌ Avoid: Circular relationship paths
|
||||
|
||||
Cross-Filter Direction:
|
||||
- Dimension to Fact: Always single direction
|
||||
- Fact to Fact: Avoid direct relationships, use shared dimensions
|
||||
- Dimension to Dimension: Only when business logic requires it
|
||||
|
||||
Referential Integrity:
|
||||
✅ Enable for DirectQuery sources when data quality is guaranteed
|
||||
✅ Improves query performance by using INNER JOINs
|
||||
❌ Don't enable if source data has orphaned records
|
||||
```
|
||||
|
||||
## Storage Mode Optimization
|
||||
|
||||
### 1. Import Mode Best Practices
|
||||
```
|
||||
When to Use Import Mode:
|
||||
✅ Data size fits within capacity limits
|
||||
✅ Complex analytical calculations required
|
||||
✅ Historical data analysis with stable datasets
|
||||
✅ Need for optimal query performance
|
||||
|
||||
Optimization Strategies:
|
||||
- Remove unnecessary columns and rows
|
||||
- Use appropriate data types
|
||||
- Pre-aggregate data when possible
|
||||
- Implement incremental refresh for large datasets
|
||||
- Optimize Power Query transformations
|
||||
```
|
||||
|
||||
#### Data Reduction Techniques for Import
|
||||
```
|
||||
Vertical Filtering (Column Reduction):
|
||||
✅ Remove columns not used in reports or relationships
|
||||
✅ Remove calculated columns that can be computed in DAX
|
||||
✅ Remove intermediate columns used only in Power Query
|
||||
✅ Optimize data types (Integer vs. Decimal, Date vs. DateTime)
|
||||
|
||||
Horizontal Filtering (Row Reduction):
|
||||
✅ Filter to relevant time periods (e.g., last 3 years of data)
|
||||
✅ Filter to relevant business entities (active customers, specific regions)
|
||||
✅ Remove test, invalid, or cancelled transactions
|
||||
✅ Implement proper data archiving strategies
|
||||
|
||||
Data Type Optimization:
|
||||
Text → Numeric: Convert codes to integers when possible
|
||||
DateTime → Date: Use Date type when time is not needed
|
||||
Decimal → Integer: Use integers for whole number measures
|
||||
High Precision → Lower Precision: Match business requirements
|
||||
```
|
||||
|
||||
### 2. DirectQuery Mode Best Practices
|
||||
```
|
||||
When to Use DirectQuery Mode:
|
||||
✅ Data exceeds import capacity limits
|
||||
✅ Real-time data requirements
|
||||
✅ Security/compliance requires data to stay at source
|
||||
✅ Integration with operational systems
|
||||
|
||||
Optimization Requirements:
|
||||
- Optimize source database performance
|
||||
- Create appropriate indexes on source tables
|
||||
- Minimize complex DAX calculations
|
||||
- Use simple measures and aggregations
|
||||
- Limit number of visuals per report page
|
||||
- Implement query reduction techniques
|
||||
```
|
||||
|
||||
#### DirectQuery Performance Optimization
|
||||
```
|
||||
Database Optimization:
|
||||
✅ Create indexes on frequently filtered columns
|
||||
✅ Create indexes on relationship key columns
|
||||
✅ Use materialized views for complex joins
|
||||
✅ Implement appropriate database maintenance
|
||||
✅ Consider columnstore indexes for analytical workloads
|
||||
|
||||
Model Design for DirectQuery:
|
||||
✅ Keep DAX measures simple
|
||||
✅ Avoid calculated columns on large tables
|
||||
✅ Use star schema design strictly
|
||||
✅ Minimize cross-table operations
|
||||
✅ Pre-aggregate data in source when possible
|
||||
|
||||
Query Performance:
|
||||
✅ Apply filters early in report design
|
||||
✅ Use appropriate visual types
|
||||
✅ Limit high-cardinality filtering
|
||||
✅ Monitor and optimize slow queries
|
||||
```
|
||||
|
||||
### 3. Composite Model Design
|
||||
```
|
||||
When to Use Composite Models:
|
||||
✅ Combine historical (Import) with real-time (DirectQuery) data
|
||||
✅ Extend existing models with additional data sources
|
||||
✅ Balance performance with data freshness requirements
|
||||
✅ Integrate multiple DirectQuery sources
|
||||
|
||||
Storage Mode Selection:
|
||||
Import: Small dimension tables, historical aggregated facts
|
||||
DirectQuery: Large fact tables, real-time operational data
|
||||
Dual: Dimension tables that need to work with both Import and DirectQuery facts
|
||||
Hybrid: Fact tables combining historical (Import) with recent (DirectQuery) data
|
||||
```
|
||||
|
||||
#### Dual Storage Mode Strategy
|
||||
```
|
||||
Use Dual Mode For:
|
||||
✅ Dimension tables that relate to both Import and DirectQuery facts
|
||||
✅ Small, slowly changing reference tables
|
||||
✅ Lookup tables that need flexible querying
|
||||
|
||||
Configuration:
|
||||
- Set dimension tables to Dual mode
|
||||
- Power BI automatically chooses optimal query path
|
||||
- Maintains single copy of dimension data
|
||||
- Enables efficient cross-source relationships
|
||||
```
|
||||
|
||||
## Advanced Modeling Patterns
|
||||
|
||||
### 1. Date Table Design
|
||||
```
|
||||
Essential Date Table Attributes:
|
||||
✅ Continuous date range (no gaps)
|
||||
✅ Mark as date table in Power BI
|
||||
✅ Include standard hierarchy (Year > Quarter > Month > Day)
|
||||
✅ Add business-specific columns (FiscalYear, WorkingDay, Holiday)
|
||||
✅ Use Date data type for date column
|
||||
|
||||
Date Table Implementation:
|
||||
DateKey (Integer): 20240315 (YYYYMMDD format)
|
||||
Date (Date): 2024-03-15
|
||||
Year (Integer): 2024
|
||||
Quarter (Text): Q1 2024
|
||||
Month (Text): March 2024
|
||||
MonthNumber (Integer): 3
|
||||
DayOfWeek (Text): Friday
|
||||
IsWorkingDay (Boolean): TRUE
|
||||
FiscalYear (Integer): 2024
|
||||
FiscalQuarter (Text): FY2024 Q3
|
||||
```
|
||||
|
||||
### 2. Slowly Changing Dimensions (SCD)
|
||||
```
|
||||
Type 1 SCD (Overwrite):
|
||||
- Update existing records with new values
|
||||
- Lose historical context
|
||||
- Simple to implement and maintain
|
||||
- Use for non-critical attribute changes
|
||||
|
||||
Type 2 SCD (History Preservation):
|
||||
- Create new records for changes
|
||||
- Maintain complete history
|
||||
- Include effective date ranges
|
||||
- Use surrogate keys for unique identification
|
||||
|
||||
Implementation Pattern:
|
||||
CustomerKey (Surrogate): 1, 2, 3, 4
|
||||
CustomerID (Business): 101, 101, 102, 103
|
||||
CustomerName: "John Doe", "John Smith", "Jane Doe", "Bob Johnson"
|
||||
EffectiveDate: 2023-01-01, 2024-01-01, 2023-01-01, 2023-01-01
|
||||
ExpirationDate: 2023-12-31, 9999-12-31, 9999-12-31, 9999-12-31
|
||||
IsCurrent: FALSE, TRUE, TRUE, TRUE
|
||||
```
|
||||
|
||||
### 3. Role-Playing Dimensions
|
||||
```
|
||||
Scenario: Date table used for Order Date, Ship Date, Delivery Date
|
||||
|
||||
Implementation Options:
|
||||
|
||||
Option 1: Multiple Relationships (Recommended)
|
||||
- Single Date table with multiple relationships to Fact
|
||||
- One active relationship (Order Date)
|
||||
- Inactive relationships for Ship Date and Delivery Date
|
||||
- Use USERELATIONSHIP in DAX measures
|
||||
|
||||
Option 2: Multiple Date Tables
|
||||
- Separate tables: OrderDate, ShipDate, DeliveryDate
|
||||
- Each with dedicated relationship
|
||||
- More intuitive for report authors
|
||||
- Larger model size due to duplication
|
||||
|
||||
DAX Implementation:
|
||||
Sales by Order Date = [Total Sales] // Uses active relationship
|
||||
Sales by Ship Date = CALCULATE([Total Sales], USERELATIONSHIP(FactSales[ShipDate], DimDate[Date]))
|
||||
Sales by Delivery Date = CALCULATE([Total Sales], USERELATIONSHIP(FactSales[DeliveryDate], DimDate[Date]))
|
||||
```
|
||||
|
||||
### 4. Bridge Tables for Many-to-Many
|
||||
```
|
||||
Scenario: Students can be in multiple Courses, Courses can have multiple Students
|
||||
|
||||
Bridge Table Design:
|
||||
DimStudent (1) ← StudentKey → (*) BridgeStudentCourse (*) ← CourseKey → (1) DimCourse
|
||||
|
||||
Bridge Table Structure:
|
||||
StudentCourseKey (PK): Surrogate key
|
||||
StudentKey (FK): Reference to DimStudent
|
||||
CourseKey (FK): Reference to DimCourse
|
||||
EnrollmentDate: Additional context
|
||||
Grade: Additional context
|
||||
Status: Active, Completed, Dropped
|
||||
|
||||
Relationship Configuration:
|
||||
- DimStudent to BridgeStudentCourse: One-to-Many
|
||||
- BridgeStudentCourse to DimCourse: Many-to-One
|
||||
- Set one relationship to bi-directional for filter propagation
|
||||
- Hide bridge table from report view
|
||||
```
|
||||
|
||||
## Performance Optimization Strategies
|
||||
|
||||
### 1. Model Size Optimization
|
||||
```
|
||||
Column Optimization:
|
||||
✅ Remove unused columns completely
|
||||
✅ Use smallest appropriate data types
|
||||
✅ Convert high-cardinality text to integers with lookup tables
|
||||
✅ Remove redundant calculated columns
|
||||
|
||||
Row Optimization:
|
||||
✅ Filter to business-relevant time periods
|
||||
✅ Remove invalid, test, or cancelled transactions
|
||||
✅ Archive historical data appropriately
|
||||
✅ Use incremental refresh for growing datasets
|
||||
|
||||
Aggregation Strategies:
|
||||
✅ Pre-calculate common aggregations
|
||||
✅ Use summary tables for high-level reporting
|
||||
✅ Implement automatic aggregations in Premium
|
||||
✅ Consider OLAP cubes for complex analytical requirements
|
||||
```
|
||||
|
||||
### 2. Relationship Performance
|
||||
```
|
||||
Key Selection:
|
||||
✅ Use integer keys over text keys
|
||||
✅ Prefer surrogate keys over natural keys
|
||||
✅ Ensure referential integrity in source data
|
||||
✅ Create appropriate indexes on key columns
|
||||
|
||||
Cardinality Optimization:
|
||||
✅ Set correct relationship cardinality
|
||||
✅ Use "Assume Referential Integrity" when appropriate
|
||||
✅ Minimize bidirectional relationships
|
||||
✅ Avoid many-to-many relationships when possible
|
||||
|
||||
Cross-Filtering Strategy:
|
||||
✅ Use single-direction filtering as default
|
||||
✅ Enable bi-directional only when required
|
||||
✅ Test performance impact of cross-filtering
|
||||
✅ Document business reasons for bi-directional relationships
|
||||
```
|
||||
|
||||
### 3. Query Performance Patterns
|
||||
```
|
||||
Efficient Model Patterns:
|
||||
✅ Proper star schema implementation
|
||||
✅ Normalized dimension tables
|
||||
✅ Denormalized fact tables
|
||||
✅ Consistent grain across related tables
|
||||
✅ Appropriate use of calculated tables and columns
|
||||
|
||||
Query Optimization:
|
||||
✅ Pre-filter large datasets
|
||||
✅ Use appropriate visual types for data
|
||||
✅ Minimize complex DAX in reports
|
||||
✅ Leverage model relationships effectively
|
||||
✅ Consider DirectQuery for large, real-time datasets
|
||||
```
|
||||
|
||||
## Security and Governance
|
||||
|
||||
### 1. Row-Level Security (RLS)
|
||||
```
|
||||
Implementation Patterns:
|
||||
|
||||
User-Based Security:
|
||||
[UserEmail] = USERPRINCIPALNAME()
|
||||
|
||||
Role-Based Security:
|
||||
VAR UserRole =
|
||||
LOOKUPVALUE(
|
||||
UserRoles[Role],
|
||||
UserRoles[Email],
|
||||
USERPRINCIPALNAME()
|
||||
)
|
||||
RETURN
|
||||
Customers[Region] = UserRole
|
||||
|
||||
Dynamic Security:
|
||||
LOOKUPVALUE(
|
||||
UserRegions[Region],
|
||||
UserRegions[Email],
|
||||
USERPRINCIPALNAME()
|
||||
) = Customers[Region]
|
||||
|
||||
Best Practices:
|
||||
✅ Test with different user accounts
|
||||
✅ Keep security logic simple and performant
|
||||
✅ Document security requirements clearly
|
||||
✅ Use security roles, not individual user filters
|
||||
✅ Consider performance impact of complex RLS
|
||||
```
|
||||
|
||||
### 2. Data Governance
|
||||
```
|
||||
Documentation Requirements:
|
||||
✅ Business definitions for all measures
|
||||
✅ Data lineage and source system mapping
|
||||
✅ Refresh schedules and dependencies
|
||||
✅ Security and access control documentation
|
||||
✅ Change management procedures
|
||||
|
||||
Data Quality:
|
||||
✅ Implement data validation rules
|
||||
✅ Monitor for data completeness
|
||||
✅ Handle missing values appropriately
|
||||
✅ Validate business rule implementation
|
||||
✅ Regular data quality assessments
|
||||
|
||||
Version Control:
|
||||
✅ Source control for Power BI files
|
||||
✅ Environment promotion procedures
|
||||
✅ Change tracking and approval processes
|
||||
✅ Backup and recovery procedures
|
||||
```
|
||||
|
||||
## Testing and Validation Framework
|
||||
|
||||
### 1. Model Testing Checklist
|
||||
```
|
||||
Functional Testing:
|
||||
□ All relationships function correctly
|
||||
□ Measures calculate expected values
|
||||
□ Filters propagate appropriately
|
||||
□ Security rules work as designed
|
||||
□ Data refresh completes successfully
|
||||
|
||||
Performance Testing:
|
||||
□ Model loads within acceptable time
|
||||
□ Queries execute within SLA requirements
|
||||
□ Visual interactions are responsive
|
||||
□ Memory usage is within capacity limits
|
||||
□ Concurrent user load testing completed
|
||||
|
||||
Data Quality Testing:
|
||||
□ No missing foreign key relationships
|
||||
□ Measure totals match source system
|
||||
□ Date ranges are complete and continuous
|
||||
□ Security filtering produces correct results
|
||||
□ Business rules are correctly implemented
|
||||
```
|
||||
|
||||
### 2. Validation Procedures
|
||||
```
|
||||
Business Validation:
|
||||
✅ Compare report totals with source systems
|
||||
✅ Validate complex calculations with business users
|
||||
✅ Test edge cases and boundary conditions
|
||||
✅ Confirm business logic implementation
|
||||
✅ Verify report accuracy across different filters
|
||||
|
||||
Technical Validation:
|
||||
✅ Performance testing with realistic data volumes
|
||||
✅ Concurrent user testing
|
||||
✅ Security testing with different user roles
|
||||
✅ Data refresh testing and monitoring
|
||||
✅ Disaster recovery testing
|
||||
```
|
||||
|
||||
## Common Anti-Patterns to Avoid
|
||||
|
||||
### 1. Schema Anti-Patterns
|
||||
```
|
||||
❌ Snowflake Schema (Unless Necessary):
|
||||
- Multiple normalized dimension tables
|
||||
- Complex relationship chains
|
||||
- Reduced query performance
|
||||
- More complex for business users
|
||||
|
||||
❌ Single Large Table:
|
||||
- Mixing facts and dimensions
|
||||
- Denormalized to extreme
|
||||
- Difficult to maintain and extend
|
||||
- Poor performance for analytical queries
|
||||
|
||||
❌ Multiple Fact Tables with Direct Relationships:
|
||||
- Many-to-many between facts
|
||||
- Complex filter propagation
|
||||
- Difficult to maintain consistency
|
||||
- Better to use shared dimensions
|
||||
```
|
||||
|
||||
### 2. Relationship Anti-Patterns
|
||||
```
|
||||
❌ Bidirectional Relationships Everywhere:
|
||||
- Performance impact
|
||||
- Unpredictable filter behavior
|
||||
- Maintenance complexity
|
||||
- Should be exception, not rule
|
||||
|
||||
❌ Many-to-Many Without Business Justification:
|
||||
- Often indicates missing dimension
|
||||
- Can hide data quality issues
|
||||
- Complex debugging and maintenance
|
||||
- Bridge tables usually better solution
|
||||
|
||||
❌ Circular Relationships:
|
||||
- Ambiguous filter paths
|
||||
- Unpredictable results
|
||||
- Difficult debugging
|
||||
- Always avoid through proper design
|
||||
```
|
||||
|
||||
## Advanced Data Modeling Patterns
|
||||
|
||||
### 1. Slowly Changing Dimensions Implementation
|
||||
```powerquery
|
||||
// Type 1 SCD: Power Query implementation for hash-based change detection
|
||||
let
|
||||
Source = Source,
|
||||
|
||||
#"Added custom" = Table.TransformColumnTypes(
|
||||
Table.AddColumn(Source, "Hash", each Binary.ToText(
|
||||
Text.ToBinary(
|
||||
Text.Combine(
|
||||
List.Transform({[FirstName],[LastName],[Region]}, each if _ = null then "" else _),
|
||||
"|")),
|
||||
BinaryEncoding.Hex)
|
||||
),
|
||||
{{"Hash", type text}}
|
||||
),
|
||||
|
||||
#"Marked key columns" = Table.AddKey(#"Added custom", {"Hash"}, false),
|
||||
|
||||
#"Merged queries" = Table.NestedJoin(
|
||||
#"Marked key columns",
|
||||
{"Hash"},
|
||||
ExistingDimRecords,
|
||||
{"Hash"},
|
||||
"ExistingDimRecords",
|
||||
JoinKind.LeftOuter
|
||||
),
|
||||
|
||||
#"Expanded ExistingDimRecords" = Table.ExpandTableColumn(
|
||||
#"Merged queries",
|
||||
"ExistingDimRecords",
|
||||
{"Count"},
|
||||
{"Count"}
|
||||
),
|
||||
|
||||
#"Filtered rows" = Table.SelectRows(#"Expanded ExistingDimRecords", each ([Count] = null)),
|
||||
|
||||
#"Removed columns" = Table.RemoveColumns(#"Filtered rows", {"Count"})
|
||||
in
|
||||
#"Removed columns"
|
||||
```
|
||||
|
||||
### 2. Incremental Refresh with Query Folding
|
||||
```powerquery
|
||||
// Optimized incremental refresh pattern
|
||||
let
|
||||
Source = Sql.Database("server","database"),
|
||||
Data = Source{[Schema="dbo",Item="FactInternetSales"]}[Data],
|
||||
FilteredByStart = Table.SelectRows(Data, each [OrderDateKey] >= Int32.From(DateTime.ToText(RangeStart,[Format="yyyyMMdd"]))),
|
||||
FilteredByEnd = Table.SelectRows(FilteredByStart, each [OrderDateKey] < Int32.From(DateTime.ToText(RangeEnd,[Format="yyyyMMdd"])))
|
||||
in
|
||||
FilteredByEnd
|
||||
```
|
||||
|
||||
### 3. Semantic Link Integration
|
||||
```python
|
||||
# Working with Power BI semantic models in Python
|
||||
import sempy.fabric as fabric
|
||||
from sempy.relationships import plot_relationship_metadata
|
||||
|
||||
relationships = fabric.list_relationships("my_dataset")
|
||||
plot_relationship_metadata(relationships)
|
||||
```
|
||||
|
||||
### 4. Advanced Partition Strategies
|
||||
```json
|
||||
// TMSL partition with time-based filtering
|
||||
"partition": {
|
||||
"name": "Sales2019",
|
||||
"mode": "import",
|
||||
"source": {
|
||||
"type": "m",
|
||||
"expression": [
|
||||
"let",
|
||||
" Source = SqlDatabase,",
|
||||
" dbo_Sales = Source{[Schema=\"dbo\",Item=\"Sales\"]}[Data],",
|
||||
" FilteredRows = Table.SelectRows(dbo_Sales, each [OrderDateKey] >= 20190101 and [OrderDateKey] <= 20191231)",
|
||||
"in",
|
||||
" FilteredRows"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Remember: Always validate your model design with business users and test with realistic data volumes and usage patterns. Use Power BI's built-in tools like Performance Analyzer and DAX Studio for optimization and debugging.
|
||||
795
instructions/power-bi-dax-best-practices.instructions.md
Normal file
795
instructions/power-bi-dax-best-practices.instructions.md
Normal file
@@ -0,0 +1,795 @@
|
||||
---
|
||||
description: 'Comprehensive Power BI DAX best practices and patterns based on Microsoft guidance for creating efficient, maintainable, and performant DAX formulas.'
|
||||
applyTo: '**/*.{pbix,dax,md,txt}'
|
||||
---
|
||||
|
||||
# Power BI DAX Best Practices
|
||||
|
||||
## Overview
|
||||
This document provides comprehensive instructions for writing efficient, maintainable, and performant DAX (Data Analysis Expressions) formulas in Power BI, based on Microsoft's official guidance and best practices.
|
||||
|
||||
## Core DAX Principles
|
||||
|
||||
### 1. Formula Structure and Variables
|
||||
Always use variables to improve performance, readability, and debugging:
|
||||
|
||||
```dax
|
||||
// ✅ PREFERRED: Using variables for clarity and performance
|
||||
Sales YoY Growth % =
|
||||
VAR CurrentSales = [Total Sales]
|
||||
VAR PreviousYearSales =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
SAMEPERIODLASTYEAR('Date'[Date])
|
||||
)
|
||||
RETURN
|
||||
DIVIDE(CurrentSales - PreviousYearSales, PreviousYearSales)
|
||||
|
||||
// ❌ AVOID: Repeated calculations without variables
|
||||
Sales YoY Growth % =
|
||||
DIVIDE(
|
||||
[Total Sales] - CALCULATE([Total Sales], SAMEPERIODLASTYEAR('Date'[Date])),
|
||||
CALCULATE([Total Sales], SAMEPERIODLASTYEAR('Date'[Date]))
|
||||
)
|
||||
```
|
||||
|
||||
**Key Benefits of Variables:**
|
||||
- **Performance**: Calculations are evaluated once and cached
|
||||
- **Readability**: Complex formulas become self-documenting
|
||||
- **Debugging**: Can temporarily return variable values for testing
|
||||
- **Maintainability**: Changes need to be made in only one place
|
||||
|
||||
### 2. Proper Reference Syntax
|
||||
Follow Microsoft's recommended patterns for column and measure references:
|
||||
|
||||
```dax
|
||||
// ✅ ALWAYS fully qualify column references
|
||||
Customer Count =
|
||||
DISTINCTCOUNT(Sales[CustomerID])
|
||||
|
||||
Profit Margin =
|
||||
DIVIDE(
|
||||
SUM(Sales[Profit]),
|
||||
SUM(Sales[Revenue])
|
||||
)
|
||||
|
||||
// ✅ NEVER fully qualify measure references
|
||||
YTD Sales Growth =
|
||||
DIVIDE([YTD Sales] - [YTD Sales PY], [YTD Sales PY])
|
||||
|
||||
// ❌ AVOID: Unqualified column references
|
||||
Customer Count = DISTINCTCOUNT([CustomerID]) // Ambiguous
|
||||
|
||||
// ❌ AVOID: Fully qualified measure references
|
||||
Growth Rate = DIVIDE(Sales[Total Sales] - Sales[Total Sales PY], Sales[Total Sales PY]) // Breaks if measure moves
|
||||
```
|
||||
|
||||
### 3. Error Handling Strategies
|
||||
Implement robust error handling using appropriate patterns:
|
||||
|
||||
```dax
|
||||
// ✅ PREFERRED: Use DIVIDE function for safe division
|
||||
Profit Margin =
|
||||
DIVIDE([Total Profit], [Total Revenue])
|
||||
|
||||
// ✅ PREFERRED: Use defensive strategies in model design
|
||||
Average Order Value =
|
||||
VAR TotalOrders = COUNTROWS(Orders)
|
||||
VAR TotalRevenue = SUM(Orders[Amount])
|
||||
RETURN
|
||||
IF(TotalOrders > 0, DIVIDE(TotalRevenue, TotalOrders))
|
||||
|
||||
// ❌ AVOID: ISERROR and IFERROR functions (performance impact)
|
||||
Profit Margin =
|
||||
IFERROR([Total Profit] / [Total Revenue], BLANK())
|
||||
|
||||
// ❌ AVOID: Complex error handling that could be prevented
|
||||
Unsafe Calculation =
|
||||
IF(
|
||||
OR(
|
||||
ISBLANK([Revenue]),
|
||||
[Revenue] = 0
|
||||
),
|
||||
BLANK(),
|
||||
[Profit] / [Revenue]
|
||||
)
|
||||
```
|
||||
|
||||
## DAX Function Categories and Best Practices
|
||||
|
||||
### Aggregation Functions
|
||||
```dax
|
||||
// Use appropriate aggregation functions for performance
|
||||
Customer Count = DISTINCTCOUNT(Sales[CustomerID]) // ✅ For unique counts
|
||||
Order Count = COUNTROWS(Orders) // ✅ For row counts
|
||||
Average Deal Size = AVERAGE(Sales[DealValue]) // ✅ For averages
|
||||
|
||||
// Avoid COUNT when COUNTROWS is more appropriate
|
||||
// ❌ COUNT(Sales[OrderID]) - slower for counting rows
|
||||
// ✅ COUNTROWS(Sales) - faster and more explicit
|
||||
```
|
||||
|
||||
### Filter and Context Functions
|
||||
```dax
|
||||
// Efficient use of CALCULATE with multiple filters
|
||||
High Value Customers =
|
||||
CALCULATE(
|
||||
DISTINCTCOUNT(Sales[CustomerID]),
|
||||
Sales[OrderValue] > 1000,
|
||||
Sales[OrderDate] >= DATE(2024,1,1)
|
||||
)
|
||||
|
||||
// Proper context modification patterns
|
||||
Same Period Last Year =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
SAMEPERIODLASTYEAR('Date'[Date])
|
||||
)
|
||||
|
||||
// Using FILTER appropriately (avoid as filter argument)
|
||||
// ✅ PREFERRED: Direct filter expression
|
||||
High Value Orders =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
Sales[OrderValue] > 1000
|
||||
)
|
||||
|
||||
// ❌ AVOID: FILTER as filter argument (unless table manipulation needed)
|
||||
High Value Orders =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
FILTER(Sales, Sales[OrderValue] > 1000)
|
||||
)
|
||||
```
|
||||
|
||||
### Time Intelligence Patterns
|
||||
```dax
|
||||
// Standard time intelligence measures
|
||||
YTD Sales =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
DATESYTD('Date'[Date])
|
||||
)
|
||||
|
||||
MTD Sales =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
DATESMTD('Date'[Date])
|
||||
)
|
||||
|
||||
// Moving averages with proper date handling
|
||||
3-Month Moving Average =
|
||||
VAR CurrentDate = MAX('Date'[Date])
|
||||
VAR StartDate = EDATE(CurrentDate, -2)
|
||||
RETURN
|
||||
CALCULATE(
|
||||
DIVIDE([Total Sales], 3),
|
||||
DATESBETWEEN(
|
||||
'Date'[Date],
|
||||
StartDate,
|
||||
CurrentDate
|
||||
)
|
||||
)
|
||||
|
||||
// Quarter over quarter growth
|
||||
QoQ Growth =
|
||||
VAR CurrentQuarter = [Total Sales]
|
||||
VAR PreviousQuarter =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
DATEADD('Date'[Date], -1, QUARTER)
|
||||
)
|
||||
RETURN
|
||||
DIVIDE(CurrentQuarter - PreviousQuarter, PreviousQuarter)
|
||||
```
|
||||
|
||||
### Advanced DAX Patterns
|
||||
```dax
|
||||
// Ranking with proper context
|
||||
Product Rank =
|
||||
RANKX(
|
||||
ALL(Product[ProductName]),
|
||||
[Total Sales],
|
||||
,
|
||||
DESC,
|
||||
DENSE
|
||||
)
|
||||
|
||||
// Running totals
|
||||
Running Total =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
FILTER(
|
||||
ALL('Date'[Date]),
|
||||
'Date'[Date] <= MAX('Date'[Date])
|
||||
)
|
||||
)
|
||||
|
||||
// ABC Analysis (Pareto)
|
||||
ABC Classification =
|
||||
VAR CurrentProductSales = [Total Sales]
|
||||
VAR TotalSales = CALCULATE([Total Sales], ALL(Product))
|
||||
VAR RunningTotal =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
FILTER(
|
||||
ALL(Product),
|
||||
[Total Sales] >= CurrentProductSales
|
||||
)
|
||||
)
|
||||
VAR PercentageOfTotal = DIVIDE(RunningTotal, TotalSales)
|
||||
RETURN
|
||||
SWITCH(
|
||||
TRUE(),
|
||||
PercentageOfTotal <= 0.8, "A",
|
||||
PercentageOfTotal <= 0.95, "B",
|
||||
"C"
|
||||
)
|
||||
```
|
||||
|
||||
## Performance Optimization Techniques
|
||||
|
||||
### 1. Efficient Variable Usage
|
||||
```dax
|
||||
// ✅ Store expensive calculations in variables
|
||||
Complex Measure =
|
||||
VAR BaseCalculation =
|
||||
CALCULATE(
|
||||
SUM(Sales[Amount]),
|
||||
FILTER(
|
||||
Product,
|
||||
Product[Category] = "Electronics"
|
||||
)
|
||||
)
|
||||
VAR PreviousYear =
|
||||
CALCULATE(
|
||||
BaseCalculation,
|
||||
SAMEPERIODLASTYEAR('Date'[Date])
|
||||
)
|
||||
RETURN
|
||||
DIVIDE(BaseCalculation - PreviousYear, PreviousYear)
|
||||
```
|
||||
|
||||
### 2. Context Transition Optimization
|
||||
```dax
|
||||
// ✅ Minimize context transitions in iterator functions
|
||||
Total Product Profit =
|
||||
SUMX(
|
||||
Product,
|
||||
Product[UnitPrice] - Product[UnitCost]
|
||||
)
|
||||
|
||||
// ❌ Avoid unnecessary calculated columns in large tables
|
||||
// Create in Power Query instead when possible
|
||||
```
|
||||
|
||||
### 3. Efficient Filtering Patterns
|
||||
```dax
|
||||
// ✅ Use table expressions efficiently
|
||||
Top 10 Customers =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
TOPN(
|
||||
10,
|
||||
ALL(Customer[CustomerName]),
|
||||
[Total Sales]
|
||||
)
|
||||
)
|
||||
|
||||
// ✅ Leverage relationship filtering
|
||||
Sales with Valid Customers =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
FILTER(
|
||||
Customer,
|
||||
NOT(ISBLANK(Customer[CustomerName]))
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Common DAX Anti-Patterns to Avoid
|
||||
|
||||
### 1. Performance Anti-Patterns
|
||||
```dax
|
||||
// ❌ AVOID: Nested CALCULATE functions
|
||||
Inefficient Nested =
|
||||
CALCULATE(
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
Product[Category] = "Electronics"
|
||||
),
|
||||
'Date'[Year] = 2024
|
||||
)
|
||||
|
||||
// ✅ PREFERRED: Single CALCULATE with multiple filters
|
||||
Efficient Single =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
Product[Category] = "Electronics",
|
||||
'Date'[Year] = 2024
|
||||
)
|
||||
|
||||
// ❌ AVOID: Converting BLANK to zero unnecessarily
|
||||
Sales with Zero =
|
||||
IF(ISBLANK([Total Sales]), 0, [Total Sales])
|
||||
|
||||
// ✅ PREFERRED: Keep BLANK as BLANK for better visual behavior
|
||||
Sales = SUM(Sales[Amount])
|
||||
```
|
||||
|
||||
### 2. Readability Anti-Patterns
|
||||
```dax
|
||||
// ❌ AVOID: Complex nested expressions without variables
|
||||
Complex Without Variables =
|
||||
DIVIDE(
|
||||
CALCULATE(SUM(Sales[Revenue]), Sales[Date] >= DATE(2024,1,1)) -
|
||||
CALCULATE(SUM(Sales[Revenue]), Sales[Date] >= DATE(2023,1,1), Sales[Date] < DATE(2024,1,1)),
|
||||
CALCULATE(SUM(Sales[Revenue]), Sales[Date] >= DATE(2023,1,1), Sales[Date] < DATE(2024,1,1))
|
||||
)
|
||||
|
||||
// ✅ PREFERRED: Clear variable-based structure
|
||||
Year Over Year Growth =
|
||||
VAR CurrentYear =
|
||||
CALCULATE(
|
||||
SUM(Sales[Revenue]),
|
||||
Sales[Date] >= DATE(2024,1,1)
|
||||
)
|
||||
VAR PreviousYear =
|
||||
CALCULATE(
|
||||
SUM(Sales[Revenue]),
|
||||
Sales[Date] >= DATE(2023,1,1),
|
||||
Sales[Date] < DATE(2024,1,1)
|
||||
)
|
||||
RETURN
|
||||
DIVIDE(CurrentYear - PreviousYear, PreviousYear)
|
||||
```
|
||||
|
||||
## DAX Debugging and Testing Strategies
|
||||
|
||||
### 1. Variable-Based Debugging
|
||||
```dax
|
||||
// Use this pattern for step-by-step debugging
|
||||
Debug Measure =
|
||||
VAR Step1 = CALCULATE([Sales], 'Date'[Year] = 2024)
|
||||
VAR Step2 = CALCULATE([Sales], 'Date'[Year] = 2023)
|
||||
VAR Step3 = Step1 - Step2
|
||||
VAR Step4 = DIVIDE(Step3, Step2)
|
||||
RETURN
|
||||
-- Return different variables for testing:
|
||||
-- Step1 -- Test current year sales
|
||||
-- Step2 -- Test previous year sales
|
||||
-- Step3 -- Test difference calculation
|
||||
Step4 -- Final result
|
||||
```
|
||||
|
||||
### 2. Testing Patterns
|
||||
```dax
|
||||
// Include data validation in measures
|
||||
Validated Measure =
|
||||
VAR Result = [Complex Calculation]
|
||||
VAR IsValid =
|
||||
Result >= 0 &&
|
||||
Result <= 1 &&
|
||||
NOT(ISBLANK(Result))
|
||||
RETURN
|
||||
IF(IsValid, Result, BLANK())
|
||||
```
|
||||
|
||||
## Measure Organization and Naming
|
||||
|
||||
### 1. Naming Conventions
|
||||
```dax
|
||||
// Use descriptive, consistent naming
|
||||
Total Sales = SUM(Sales[Amount])
|
||||
Total Sales YTD = CALCULATE([Total Sales], DATESYTD('Date'[Date]))
|
||||
Total Sales PY = CALCULATE([Total Sales], SAMEPERIODLASTYEAR('Date'[Date]))
|
||||
Sales Growth % = DIVIDE([Total Sales] - [Total Sales PY], [Total Sales PY])
|
||||
|
||||
// Prefix for measure categories
|
||||
KPI - Revenue Growth = [Sales Growth %]
|
||||
Calc - Days Since Last Order = DATEDIFF(MAX(Orders[OrderDate]), TODAY(), DAY)
|
||||
Base - Order Count = COUNTROWS(Orders)
|
||||
```
|
||||
|
||||
### 2. Measure Dependencies
|
||||
```dax
|
||||
// Build measures hierarchically for reusability
|
||||
// Base measures
|
||||
Revenue = SUM(Sales[Revenue])
|
||||
Cost = SUM(Sales[Cost])
|
||||
|
||||
// Derived measures
|
||||
Profit = [Revenue] - [Cost]
|
||||
Margin % = DIVIDE([Profit], [Revenue])
|
||||
|
||||
// Advanced measures
|
||||
Profit YTD = CALCULATE([Profit], DATESYTD('Date'[Date]))
|
||||
Margin Trend = [Margin %] - CALCULATE([Margin %], PREVIOUSMONTH('Date'[Date]))
|
||||
```
|
||||
|
||||
## Model Integration Best Practices
|
||||
|
||||
### 1. Working with Star Schema
|
||||
```dax
|
||||
// Leverage proper relationships
|
||||
Sales by Category =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
Product[Category] = "Electronics"
|
||||
)
|
||||
|
||||
// Use dimension tables for filtering
|
||||
Regional Sales =
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
Geography[Region] = "North America"
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Handle Missing Relationships
|
||||
```dax
|
||||
// When direct relationships don't exist
|
||||
Cross Table Analysis =
|
||||
VAR CustomerList = VALUES(Customer[CustomerID])
|
||||
RETURN
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
FILTER(
|
||||
Sales,
|
||||
Sales[CustomerID] IN CustomerList
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Advanced DAX Concepts
|
||||
|
||||
### 1. Row Context vs Filter Context
|
||||
```dax
|
||||
// Understanding context differences
|
||||
Row Context Example =
|
||||
SUMX(
|
||||
Sales,
|
||||
Sales[Quantity] * Sales[UnitPrice] // Row context
|
||||
)
|
||||
|
||||
Filter Context Example =
|
||||
CALCULATE(
|
||||
[Total Sales], // Filter context
|
||||
Product[Category] = "Electronics"
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Context Transition
|
||||
```dax
|
||||
// When row context becomes filter context
|
||||
Sales Per Product =
|
||||
SUMX(
|
||||
Product,
|
||||
CALCULATE([Total Sales]) // Context transition happens here
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Extended Columns and Computed Tables
|
||||
```dax
|
||||
// Use for complex analytical scenarios
|
||||
Product Analysis =
|
||||
ADDCOLUMNS(
|
||||
Product,
|
||||
"Total Sales", CALCULATE([Total Sales]),
|
||||
"Rank", RANKX(ALL(Product), CALCULATE([Total Sales])),
|
||||
"Category Share", DIVIDE(
|
||||
CALCULATE([Total Sales]),
|
||||
CALCULATE([Total Sales], ALL(Product[ProductName]))
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Advanced Time Intelligence Patterns
|
||||
```dax
|
||||
// Multi-period comparisons with calculation groups
|
||||
// Example showing how to create dynamic time calculations
|
||||
Dynamic Period Comparison =
|
||||
VAR CurrentPeriodValue =
|
||||
CALCULATE(
|
||||
[Sales],
|
||||
'Time Intelligence'[Time Calculation] = "Current"
|
||||
)
|
||||
VAR PreviousPeriodValue =
|
||||
CALCULATE(
|
||||
[Sales],
|
||||
'Time Intelligence'[Time Calculation] = "PY"
|
||||
)
|
||||
VAR MTDCurrent =
|
||||
CALCULATE(
|
||||
[Sales],
|
||||
'Time Intelligence'[Time Calculation] = "MTD"
|
||||
)
|
||||
VAR MTDPrevious =
|
||||
CALCULATE(
|
||||
[Sales],
|
||||
'Time Intelligence'[Time Calculation] = "PY MTD"
|
||||
)
|
||||
RETURN
|
||||
DIVIDE(MTDCurrent - MTDPrevious, MTDPrevious)
|
||||
|
||||
// Working with fiscal years and custom calendars
|
||||
Fiscal YTD Sales =
|
||||
VAR FiscalYearStart =
|
||||
DATE(
|
||||
IF(MONTH(MAX('Date'[Date])) >= 7, YEAR(MAX('Date'[Date])), YEAR(MAX('Date'[Date])) - 1),
|
||||
7,
|
||||
1
|
||||
)
|
||||
VAR FiscalYearEnd = MAX('Date'[Date])
|
||||
RETURN
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
DATESBETWEEN(
|
||||
'Date'[Date],
|
||||
FiscalYearStart,
|
||||
FiscalYearEnd
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Advanced Performance Optimization Techniques
|
||||
```dax
|
||||
// Optimized running totals
|
||||
Running Total Optimized =
|
||||
VAR CurrentDate = MAX('Date'[Date])
|
||||
RETURN
|
||||
CALCULATE(
|
||||
[Total Sales],
|
||||
FILTER(
|
||||
ALL('Date'[Date]),
|
||||
'Date'[Date] <= CurrentDate
|
||||
)
|
||||
)
|
||||
|
||||
// Efficient ABC Analysis using RANKX
|
||||
ABC Classification Advanced =
|
||||
VAR ProductRank =
|
||||
RANKX(
|
||||
ALL(Product[ProductName]),
|
||||
[Total Sales],
|
||||
,
|
||||
DESC,
|
||||
DENSE
|
||||
)
|
||||
VAR TotalProducts = COUNTROWS(ALL(Product[ProductName]))
|
||||
VAR ClassAThreshold = TotalProducts * 0.2
|
||||
VAR ClassBThreshold = TotalProducts * 0.5
|
||||
RETURN
|
||||
SWITCH(
|
||||
TRUE(),
|
||||
ProductRank <= ClassAThreshold, "A",
|
||||
ProductRank <= ClassBThreshold, "B",
|
||||
"C"
|
||||
)
|
||||
|
||||
// Efficient Top N with ties handling
|
||||
Top N Products with Ties =
|
||||
VAR TopNValue = 10
|
||||
VAR MinTopNSales =
|
||||
CALCULATE(
|
||||
MIN([Total Sales]),
|
||||
TOPN(
|
||||
TopNValue,
|
||||
ALL(Product[ProductName]),
|
||||
[Total Sales]
|
||||
)
|
||||
)
|
||||
RETURN
|
||||
IF(
|
||||
[Total Sales] >= MinTopNSales,
|
||||
[Total Sales],
|
||||
BLANK()
|
||||
)
|
||||
```
|
||||
|
||||
### 6. Complex Analytical Scenarios
|
||||
```dax
|
||||
// Customer cohort analysis
|
||||
Cohort Retention Rate =
|
||||
VAR CohortMonth =
|
||||
CALCULATE(
|
||||
MIN('Date'[Date]),
|
||||
ALLEXCEPT(Sales, Sales[CustomerID])
|
||||
)
|
||||
VAR CurrentMonth = MAX('Date'[Date])
|
||||
VAR MonthsFromCohort =
|
||||
DATEDIFF(CohortMonth, CurrentMonth, MONTH)
|
||||
VAR CohortCustomers =
|
||||
CALCULATE(
|
||||
DISTINCTCOUNT(Sales[CustomerID]),
|
||||
'Date'[Date] = CohortMonth
|
||||
)
|
||||
VAR ActiveCustomersInMonth =
|
||||
CALCULATE(
|
||||
DISTINCTCOUNT(Sales[CustomerID]),
|
||||
'Date'[Date] = CurrentMonth,
|
||||
FILTER(
|
||||
Sales,
|
||||
CALCULATE(
|
||||
MIN('Date'[Date]),
|
||||
ALLEXCEPT(Sales, Sales[CustomerID])
|
||||
) = CohortMonth
|
||||
)
|
||||
)
|
||||
RETURN
|
||||
DIVIDE(ActiveCustomersInMonth, CohortCustomers)
|
||||
|
||||
// Market basket analysis
|
||||
Product Affinity Score =
|
||||
VAR CurrentProduct = SELECTEDVALUE(Product[ProductName])
|
||||
VAR RelatedProduct = SELECTEDVALUE('Related Product'[ProductName])
|
||||
VAR TransactionsWithBoth =
|
||||
CALCULATE(
|
||||
DISTINCTCOUNT(Sales[TransactionID]),
|
||||
Sales[ProductName] = CurrentProduct
|
||||
) +
|
||||
CALCULATE(
|
||||
DISTINCTCOUNT(Sales[TransactionID]),
|
||||
Sales[ProductName] = RelatedProduct
|
||||
) -
|
||||
CALCULATE(
|
||||
DISTINCTCOUNT(Sales[TransactionID]),
|
||||
Sales[ProductName] = CurrentProduct,
|
||||
CALCULATE(
|
||||
COUNTROWS(Sales),
|
||||
Sales[ProductName] = RelatedProduct,
|
||||
Sales[TransactionID] = EARLIER(Sales[TransactionID])
|
||||
) > 0
|
||||
)
|
||||
VAR TotalTransactions = DISTINCTCOUNT(Sales[TransactionID])
|
||||
RETURN
|
||||
DIVIDE(TransactionsWithBoth, TotalTransactions)
|
||||
```
|
||||
|
||||
### 7. Advanced Debugging and Profiling
|
||||
```dax
|
||||
// Debug measure with detailed variable inspection
|
||||
Complex Measure Debug =
|
||||
VAR Step1_FilteredSales =
|
||||
CALCULATE(
|
||||
[Sales],
|
||||
Product[Category] = "Electronics",
|
||||
'Date'[Year] = 2024
|
||||
)
|
||||
VAR Step2_PreviousYear =
|
||||
CALCULATE(
|
||||
[Sales],
|
||||
Product[Category] = "Electronics",
|
||||
'Date'[Year] = 2023
|
||||
)
|
||||
VAR Step3_GrowthAbsolute = Step1_FilteredSales - Step2_PreviousYear
|
||||
VAR Step4_GrowthPercentage = DIVIDE(Step3_GrowthAbsolute, Step2_PreviousYear)
|
||||
VAR DebugInfo =
|
||||
"Current: " & FORMAT(Step1_FilteredSales, "#,0") &
|
||||
" | Previous: " & FORMAT(Step2_PreviousYear, "#,0") &
|
||||
" | Growth: " & FORMAT(Step4_GrowthPercentage, "0.00%")
|
||||
RETURN
|
||||
-- Switch between these for debugging:
|
||||
-- Step1_FilteredSales -- Test current year
|
||||
-- Step2_PreviousYear -- Test previous year
|
||||
-- Step3_GrowthAbsolute -- Test absolute growth
|
||||
-- DebugInfo -- Show debug information
|
||||
Step4_GrowthPercentage -- Final result
|
||||
|
||||
// Performance monitoring measure
|
||||
Query Performance Monitor =
|
||||
VAR StartTime = NOW()
|
||||
VAR Result = [Complex Calculation]
|
||||
VAR EndTime = NOW()
|
||||
VAR ExecutionTime = DATEDIFF(StartTime, EndTime, SECOND)
|
||||
VAR WarningThreshold = 5 // seconds
|
||||
RETURN
|
||||
IF(
|
||||
ExecutionTime > WarningThreshold,
|
||||
"⚠️ Slow: " & ExecutionTime & "s - " & Result,
|
||||
Result
|
||||
)
|
||||
```
|
||||
|
||||
### 8. Working with Complex Data Types
|
||||
```dax
|
||||
// JSON parsing and manipulation
|
||||
Extract JSON Value =
|
||||
VAR JSONString = SELECTEDVALUE(Data[JSONColumn])
|
||||
VAR ParsedValue =
|
||||
IF(
|
||||
NOT(ISBLANK(JSONString)),
|
||||
PATHCONTAINS(JSONString, "$.analytics.revenue"),
|
||||
BLANK()
|
||||
)
|
||||
RETURN
|
||||
ParsedValue
|
||||
|
||||
// Dynamic measure selection
|
||||
Dynamic Measure Selector =
|
||||
VAR SelectedMeasure = SELECTEDVALUE('Measure Selector'[MeasureName])
|
||||
RETURN
|
||||
SWITCH(
|
||||
SelectedMeasure,
|
||||
"Revenue", [Total Revenue],
|
||||
"Profit", [Total Profit],
|
||||
"Units", [Total Units],
|
||||
"Margin", [Profit Margin %],
|
||||
BLANK()
|
||||
)
|
||||
```
|
||||
|
||||
## DAX Formula Documentation
|
||||
|
||||
### 1. Commenting Best Practices
|
||||
```dax
|
||||
/*
|
||||
Business Rule: Calculate customer lifetime value based on:
|
||||
- Average order value over customer lifetime
|
||||
- Purchase frequency (orders per year)
|
||||
- Customer lifespan (years since first order)
|
||||
- Retention probability based on last order date
|
||||
*/
|
||||
Customer Lifetime Value =
|
||||
VAR AvgOrderValue =
|
||||
DIVIDE(
|
||||
CALCULATE(SUM(Sales[Amount])),
|
||||
CALCULATE(DISTINCTCOUNT(Sales[OrderID]))
|
||||
)
|
||||
VAR OrdersPerYear =
|
||||
DIVIDE(
|
||||
CALCULATE(DISTINCTCOUNT(Sales[OrderID])),
|
||||
DATEDIFF(
|
||||
CALCULATE(MIN(Sales[OrderDate])),
|
||||
CALCULATE(MAX(Sales[OrderDate])),
|
||||
YEAR
|
||||
) + 1 -- Add 1 to avoid division by zero for customers with orders in single year
|
||||
)
|
||||
VAR CustomerLifespanYears = 3 -- Business assumption: average 3-year relationship
|
||||
RETURN
|
||||
AvgOrderValue * OrdersPerYear * CustomerLifespanYears
|
||||
```
|
||||
|
||||
### 2. Version Control and Change Management
|
||||
```dax
|
||||
// Include version history in measure descriptions
|
||||
/*
|
||||
Version History:
|
||||
v1.0 - Initial implementation (2024-01-15)
|
||||
v1.1 - Added null checking for edge cases (2024-02-01)
|
||||
v1.2 - Optimized performance using variables (2024-02-15)
|
||||
v2.0 - Changed business logic per stakeholder feedback (2024-03-01)
|
||||
|
||||
Business Logic:
|
||||
- Excludes returns and cancelled orders
|
||||
- Uses ship date for revenue recognition
|
||||
- Applies regional tax calculations
|
||||
*/
|
||||
```
|
||||
|
||||
## Testing and Validation Framework
|
||||
|
||||
### 1. Unit Testing Patterns
|
||||
```dax
|
||||
// Create test measures for validation
|
||||
Test - Sales Sum =
|
||||
VAR DirectSum = SUM(Sales[Amount])
|
||||
VAR MeasureResult = [Total Sales]
|
||||
VAR Difference = ABS(DirectSum - MeasureResult)
|
||||
RETURN
|
||||
IF(Difference < 0.01, "PASS", "FAIL: " & Difference)
|
||||
```
|
||||
|
||||
### 2. Performance Testing
|
||||
```dax
|
||||
// Monitor execution time for complex measures
|
||||
Performance Monitor =
|
||||
VAR StartTime = NOW()
|
||||
VAR Result = [Complex Calculation]
|
||||
VAR EndTime = NOW()
|
||||
VAR Duration = DATEDIFF(StartTime, EndTime, SECOND)
|
||||
RETURN
|
||||
"Result: " & Result & " | Duration: " & Duration & "s"
|
||||
```
|
||||
|
||||
Remember: Always validate DAX formulas with business users to ensure calculations match business requirements and expectations. Use Power BI's Performance Analyzer and DAX Studio for performance optimization and debugging.
|
||||
623
instructions/power-bi-devops-alm-best-practices.instructions.md
Normal file
623
instructions/power-bi-devops-alm-best-practices.instructions.md
Normal file
@@ -0,0 +1,623 @@
|
||||
---
|
||||
description: 'Comprehensive guide for Power BI DevOps, Application Lifecycle Management (ALM), CI/CD pipelines, deployment automation, and version control best practices.'
|
||||
applyTo: '**/*.{yml,yaml,ps1,json,pbix,pbir}'
|
||||
---
|
||||
|
||||
# Power BI DevOps and Application Lifecycle Management Best Practices
|
||||
|
||||
## Overview
|
||||
This document provides comprehensive instructions for implementing DevOps practices, CI/CD pipelines, and Application Lifecycle Management (ALM) for Power BI solutions, based on Microsoft's recommended patterns and best practices.
|
||||
|
||||
## Power BI Project Structure and Version Control
|
||||
|
||||
### 1. PBIP (Power BI Project) Structure
|
||||
```markdown
|
||||
// Power BI project file organization
|
||||
├── Model/
|
||||
│ ├── model.tmdl
|
||||
│ ├── tables/
|
||||
│ │ ├── FactSales.tmdl
|
||||
│ │ └── DimProduct.tmdl
|
||||
│ ├── relationships/
|
||||
│ │ └── relationships.tmdl
|
||||
│ └── measures/
|
||||
│ └── measures.tmdl
|
||||
├── Report/
|
||||
│ ├── report.json
|
||||
│ ├── pages/
|
||||
│ │ ├── ReportSection1/
|
||||
│ │ │ ├── page.json
|
||||
│ │ │ └── visuals/
|
||||
│ │ └── pages.json
|
||||
│ └── bookmarks/
|
||||
└── .git/
|
||||
```
|
||||
|
||||
### 2. Git Integration Best Practices
|
||||
```powershell
|
||||
# Initialize Power BI project with Git
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial Power BI project structure"
|
||||
|
||||
# Create feature branch for development
|
||||
git checkout -b feature/new-dashboard
|
||||
git add Model/tables/NewTable.tmdl
|
||||
git commit -m "Add new dimension table"
|
||||
|
||||
# Merge and deploy workflow
|
||||
git checkout main
|
||||
git merge feature/new-dashboard
|
||||
git tag -a v1.2.0 -m "Release version 1.2.0"
|
||||
```
|
||||
|
||||
## Deployment Pipelines and Automation
|
||||
|
||||
### 1. Power BI Deployment Pipelines API
|
||||
```powershell
|
||||
# Automated deployment using Power BI REST API
|
||||
$url = "pipelines/{0}/Deploy" -f "Insert your pipeline ID here"
|
||||
$body = @{
|
||||
sourceStageOrder = 0 # Development (0), Test (1)
|
||||
datasets = @(
|
||||
@{sourceId = "Insert your dataset ID here" }
|
||||
)
|
||||
reports = @(
|
||||
@{sourceId = "Insert your report ID here" }
|
||||
)
|
||||
dashboards = @(
|
||||
@{sourceId = "Insert your dashboard ID here" }
|
||||
)
|
||||
|
||||
options = @{
|
||||
# Allows creating new item if needed on the Test stage workspace
|
||||
allowCreateArtifact = $TRUE
|
||||
|
||||
# Allows overwriting existing item if needed on the Test stage workspace
|
||||
allowOverwriteArtifact = $TRUE
|
||||
}
|
||||
} | ConvertTo-Json
|
||||
|
||||
$deployResult = Invoke-PowerBIRestMethod -Url $url -Method Post -Body $body | ConvertFrom-Json
|
||||
|
||||
# Poll deployment status
|
||||
$url = "pipelines/{0}/Operations/{1}" -f "Insert your pipeline ID here",$deployResult.id
|
||||
$operation = Invoke-PowerBIRestMethod -Url $url -Method Get | ConvertFrom-Json
|
||||
while($operation.Status -eq "NotStarted" -or $operation.Status -eq "Executing")
|
||||
{
|
||||
# Sleep for 5 seconds
|
||||
Start-Sleep -s 5
|
||||
$operation = Invoke-PowerBIRestMethod -Url $url -Method Get | ConvertFrom-Json
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Azure DevOps Integration
|
||||
```yaml
|
||||
# Azure DevOps pipeline for Power BI deployment
|
||||
trigger:
|
||||
- main
|
||||
|
||||
pool:
|
||||
vmImage: windows-latest
|
||||
|
||||
steps:
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
Contents: '**'
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)'
|
||||
CleanTargetFolder: true
|
||||
ignoreMakeDirErrors: true
|
||||
displayName: 'Copy files from Repo'
|
||||
|
||||
- task: PowerPlatformToolInstaller@2
|
||||
inputs:
|
||||
DefaultVersion: true
|
||||
|
||||
- task: PowerPlatformExportData@2
|
||||
inputs:
|
||||
authenticationType: 'PowerPlatformSPN'
|
||||
PowerPlatformSPN: 'PowerBIServiceConnection'
|
||||
Environment: '$(BuildTools.EnvironmentUrl)'
|
||||
SchemaFile: '$(Build.ArtifactStagingDirectory)\source\schema.xml'
|
||||
DataFile: 'data.zip'
|
||||
displayName: 'Export Power BI metadata'
|
||||
|
||||
- task: PowerShell@2
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
# Deploy Power BI project using FabricPS-PBIP
|
||||
$workspaceName = "$(WorkspaceName)"
|
||||
$pbipSemanticModelPath = "$(Build.ArtifactStagingDirectory)\$(ProjectName).SemanticModel"
|
||||
$pbipReportPath = "$(Build.ArtifactStagingDirectory)\$(ProjectName).Report"
|
||||
|
||||
# Download and install FabricPS-PBIP module
|
||||
New-Item -ItemType Directory -Path ".\modules" -ErrorAction SilentlyContinue | Out-Null
|
||||
@("https://raw.githubusercontent.com/microsoft/Analysis-Services/master/pbidevmode/fabricps-pbip/FabricPS-PBIP.psm1",
|
||||
"https://raw.githubusercontent.com/microsoft/Analysis-Services/master/pbidevmode/fabricps-pbip/FabricPS-PBIP.psd1") |% {
|
||||
Invoke-WebRequest -Uri $_ -OutFile ".\modules\$(Split-Path $_ -Leaf)"
|
||||
}
|
||||
|
||||
Import-Module ".\modules\FabricPS-PBIP" -Force
|
||||
|
||||
# Authenticate and deploy
|
||||
Set-FabricAuthToken -reset
|
||||
$workspaceId = New-FabricWorkspace -name $workspaceName -skipErrorIfExists
|
||||
$semanticModelImport = Import-FabricItem -workspaceId $workspaceId -path $pbipSemanticModelPath
|
||||
$reportImport = Import-FabricItem -workspaceId $workspaceId -path $pbipReportPath -itemProperties @{"semanticModelId" = $semanticModelImport.Id}
|
||||
displayName: 'Deploy to Power BI Service'
|
||||
```
|
||||
|
||||
### 3. Fabric REST API Deployment
|
||||
```powershell
|
||||
# Complete PowerShell deployment script
|
||||
# Parameters
|
||||
$workspaceName = "[Workspace Name]"
|
||||
$pbipSemanticModelPath = "[PBIP Path]\[Item Name].SemanticModel"
|
||||
$pbipReportPath = "[PBIP Path]\[Item Name].Report"
|
||||
$currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent)
|
||||
Set-Location $currentPath
|
||||
|
||||
# Download modules and install
|
||||
New-Item -ItemType Directory -Path ".\modules" -ErrorAction SilentlyContinue | Out-Null
|
||||
@("https://raw.githubusercontent.com/microsoft/Analysis-Services/master/pbidevmode/fabricps-pbip/FabricPS-PBIP.psm1",
|
||||
"https://raw.githubusercontent.com/microsoft/Analysis-Services/master/pbidevmode/fabricps-pbip/FabricPS-PBIP.psd1") |% {
|
||||
Invoke-WebRequest -Uri $_ -OutFile ".\modules\$(Split-Path $_ -Leaf)"
|
||||
}
|
||||
|
||||
if(-not (Get-Module Az.Accounts -ListAvailable)) {
|
||||
Install-Module Az.Accounts -Scope CurrentUser -Force
|
||||
}
|
||||
Import-Module ".\modules\FabricPS-PBIP" -Force
|
||||
|
||||
# Authenticate
|
||||
Set-FabricAuthToken -reset
|
||||
|
||||
# Ensure workspace exists
|
||||
$workspaceId = New-FabricWorkspace -name $workspaceName -skipErrorIfExists
|
||||
|
||||
# Import the semantic model and save the item id
|
||||
$semanticModelImport = Import-FabricItem -workspaceId $workspaceId -path $pbipSemanticModelPath
|
||||
|
||||
# Import the report and ensure its bound to the previous imported semantic model
|
||||
$reportImport = Import-FabricItem -workspaceId $workspaceId -path $pbipReportPath -itemProperties @{"semanticModelId" = $semanticModelImport.Id}
|
||||
```
|
||||
|
||||
## Environment Management
|
||||
|
||||
### 1. Multi-Environment Strategy
|
||||
```json
|
||||
{
|
||||
"environments": {
|
||||
"development": {
|
||||
"workspaceId": "dev-workspace-id",
|
||||
"dataSourceUrl": "dev-database.database.windows.net",
|
||||
"refreshSchedule": "manual",
|
||||
"sensitivityLabel": "Internal"
|
||||
},
|
||||
"test": {
|
||||
"workspaceId": "test-workspace-id",
|
||||
"dataSourceUrl": "test-database.database.windows.net",
|
||||
"refreshSchedule": "daily",
|
||||
"sensitivityLabel": "Internal"
|
||||
},
|
||||
"production": {
|
||||
"workspaceId": "prod-workspace-id",
|
||||
"dataSourceUrl": "prod-database.database.windows.net",
|
||||
"refreshSchedule": "hourly",
|
||||
"sensitivityLabel": "Confidential"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Parameter-Driven Deployment
|
||||
```powershell
|
||||
# Environment-specific parameter management
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateSet("dev", "test", "prod")]
|
||||
[string]$Environment,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$WorkspaceName,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$DataSourceServer
|
||||
)
|
||||
|
||||
# Load environment-specific configuration
|
||||
$configPath = ".\config\$Environment.json"
|
||||
$config = Get-Content $configPath | ConvertFrom-Json
|
||||
|
||||
# Update connection strings based on environment
|
||||
$connectionString = "Data Source=$($config.dataSourceUrl);Initial Catalog=$($config.database);Integrated Security=SSPI;"
|
||||
|
||||
# Deploy with environment-specific settings
|
||||
Write-Host "Deploying to $Environment environment..."
|
||||
Write-Host "Workspace: $($config.workspaceId)"
|
||||
Write-Host "Data Source: $($config.dataSourceUrl)"
|
||||
```
|
||||
|
||||
## Automated Testing Framework
|
||||
|
||||
### 1. Data Quality Tests
|
||||
```powershell
|
||||
# Automated data quality validation
|
||||
function Test-PowerBIDataQuality {
|
||||
param(
|
||||
[string]$WorkspaceId,
|
||||
[string]$DatasetId
|
||||
)
|
||||
|
||||
# Test 1: Row count validation
|
||||
$rowCountQuery = @"
|
||||
EVALUATE
|
||||
ADDCOLUMNS(
|
||||
SUMMARIZE(Sales, Sales[Year]),
|
||||
"RowCount", COUNTROWS(Sales),
|
||||
"ExpectedMin", 1000,
|
||||
"Test", IF(COUNTROWS(Sales) >= 1000, "PASS", "FAIL")
|
||||
)
|
||||
"@
|
||||
|
||||
# Test 2: Data freshness validation
|
||||
$freshnessQuery = @"
|
||||
EVALUATE
|
||||
ADDCOLUMNS(
|
||||
ROW("LastRefresh", MAX(Sales[Date])),
|
||||
"DaysOld", DATEDIFF(MAX(Sales[Date]), TODAY(), DAY),
|
||||
"Test", IF(DATEDIFF(MAX(Sales[Date]), TODAY(), DAY) <= 1, "PASS", "FAIL")
|
||||
)
|
||||
"@
|
||||
|
||||
# Execute tests
|
||||
$rowCountResult = Invoke-PowerBIRestMethod -Url "groups/$WorkspaceId/datasets/$DatasetId/executeQueries" -Method Post -Body (@{queries=@(@{query=$rowCountQuery})} | ConvertTo-Json)
|
||||
$freshnessResult = Invoke-PowerBIRestMethod -Url "groups/$WorkspaceId/datasets/$DatasetId/executeQueries" -Method Post -Body (@{queries=@(@{query=$freshnessQuery})} | ConvertTo-Json)
|
||||
|
||||
return @{
|
||||
RowCountTest = $rowCountResult
|
||||
FreshnessTest = $freshnessResult
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Performance Regression Tests
|
||||
```powershell
|
||||
# Performance benchmark testing
|
||||
function Test-PowerBIPerformance {
|
||||
param(
|
||||
[string]$WorkspaceId,
|
||||
[string]$ReportId
|
||||
)
|
||||
|
||||
$performanceTests = @(
|
||||
@{
|
||||
Name = "Dashboard Load Time"
|
||||
Query = "EVALUATE TOPN(1000, Sales)"
|
||||
MaxDurationMs = 5000
|
||||
},
|
||||
@{
|
||||
Name = "Complex Calculation"
|
||||
Query = "EVALUATE ADDCOLUMNS(Sales, 'ComplexCalc', [Sales] * [Profit Margin %])"
|
||||
MaxDurationMs = 10000
|
||||
}
|
||||
)
|
||||
|
||||
$results = @()
|
||||
foreach ($test in $performanceTests) {
|
||||
$startTime = Get-Date
|
||||
$result = Invoke-PowerBIRestMethod -Url "groups/$WorkspaceId/datasets/$DatasetId/executeQueries" -Method Post -Body (@{queries=@(@{query=$test.Query})} | ConvertTo-Json)
|
||||
$endTime = Get-Date
|
||||
$duration = ($endTime - $startTime).TotalMilliseconds
|
||||
|
||||
$results += @{
|
||||
TestName = $test.Name
|
||||
Duration = $duration
|
||||
Passed = $duration -le $test.MaxDurationMs
|
||||
Threshold = $test.MaxDurationMs
|
||||
}
|
||||
}
|
||||
|
||||
return $results
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### 1. Infrastructure as Code
|
||||
```json
|
||||
{
|
||||
"workspace": {
|
||||
"name": "Production Analytics",
|
||||
"description": "Production Power BI workspace for sales analytics",
|
||||
"capacityId": "A1-capacity-id",
|
||||
"users": [
|
||||
{
|
||||
"emailAddress": "admin@contoso.com",
|
||||
"accessRight": "Admin"
|
||||
},
|
||||
{
|
||||
"emailAddress": "powerbi-service-principal@contoso.com",
|
||||
"accessRight": "Member",
|
||||
"principalType": "App"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"datasetDefaultStorageFormat": "Large",
|
||||
"blockResourceKeyAuthentication": true
|
||||
}
|
||||
},
|
||||
"datasets": [
|
||||
{
|
||||
"name": "Sales Analytics",
|
||||
"refreshSchedule": {
|
||||
"enabled": true,
|
||||
"times": ["06:00", "12:00", "18:00"],
|
||||
"days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||
"timeZone": "UTC"
|
||||
},
|
||||
"datasourceCredentials": {
|
||||
"credentialType": "OAuth2",
|
||||
"encryptedConnection": "Encrypted"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Secret Management
|
||||
```powershell
|
||||
# Azure Key Vault integration for secrets
|
||||
function Get-PowerBICredentials {
|
||||
param(
|
||||
[string]$KeyVaultName,
|
||||
[string]$Environment
|
||||
)
|
||||
|
||||
# Retrieve secrets from Key Vault
|
||||
$servicePrincipalId = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "PowerBI-ServicePrincipal-Id-$Environment" -AsPlainText
|
||||
$servicePrincipalSecret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "PowerBI-ServicePrincipal-Secret-$Environment" -AsPlainText
|
||||
$tenantId = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name "PowerBI-TenantId-$Environment" -AsPlainText
|
||||
|
||||
return @{
|
||||
ServicePrincipalId = $servicePrincipalId
|
||||
ServicePrincipalSecret = $servicePrincipalSecret
|
||||
TenantId = $tenantId
|
||||
}
|
||||
}
|
||||
|
||||
# Authenticate using retrieved credentials
|
||||
$credentials = Get-PowerBICredentials -KeyVaultName "PowerBI-KeyVault" -Environment "Production"
|
||||
$securePassword = ConvertTo-SecureString $credentials.ServicePrincipalSecret -AsPlainText -Force
|
||||
$credential = New-Object System.Management.Automation.PSCredential($credentials.ServicePrincipalId, $securePassword)
|
||||
Connect-PowerBIServiceAccount -ServicePrincipal -Credential $credential -TenantId $credentials.TenantId
|
||||
```
|
||||
|
||||
## Release Management
|
||||
|
||||
### 1. Release Pipeline
|
||||
```yaml
|
||||
# Multi-stage release pipeline
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: 'Build Stage'
|
||||
jobs:
|
||||
- job: Build
|
||||
steps:
|
||||
- task: PowerShell@2
|
||||
displayName: 'Validate Power BI Project'
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
script: |
|
||||
# Validate PBIP structure
|
||||
if (-not (Test-Path "Model\model.tmdl")) {
|
||||
throw "Missing model.tmdl file"
|
||||
}
|
||||
|
||||
# Validate required files
|
||||
$requiredFiles = @("Report\report.json", "Model\tables")
|
||||
foreach ($file in $requiredFiles) {
|
||||
if (-not (Test-Path $file)) {
|
||||
throw "Missing required file: $file"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "✅ Project validation passed"
|
||||
|
||||
- stage: DeployTest
|
||||
displayName: 'Deploy to Test'
|
||||
dependsOn: Build
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- deployment: DeployTest
|
||||
environment: 'PowerBI-Test'
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- template: deploy-powerbi.yml
|
||||
parameters:
|
||||
environment: 'test'
|
||||
workspaceName: '$(TestWorkspaceName)'
|
||||
|
||||
- stage: DeployProd
|
||||
displayName: 'Deploy to Production'
|
||||
dependsOn: DeployTest
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
|
||||
jobs:
|
||||
- deployment: DeployProd
|
||||
environment: 'PowerBI-Production'
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- template: deploy-powerbi.yml
|
||||
parameters:
|
||||
environment: 'prod'
|
||||
workspaceName: '$(ProdWorkspaceName)'
|
||||
```
|
||||
|
||||
### 2. Rollback Strategy
|
||||
```powershell
|
||||
# Automated rollback mechanism
|
||||
function Invoke-PowerBIRollback {
|
||||
param(
|
||||
[string]$WorkspaceId,
|
||||
[string]$BackupVersion,
|
||||
[string]$BackupLocation
|
||||
)
|
||||
|
||||
Write-Host "Initiating rollback to version: $BackupVersion"
|
||||
|
||||
# Step 1: Export current state as emergency backup
|
||||
$emergencyBackup = "emergency-backup-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
||||
Export-PowerBIReport -WorkspaceId $WorkspaceId -BackupName $emergencyBackup
|
||||
|
||||
# Step 2: Restore from backup
|
||||
$backupPath = Join-Path $BackupLocation "$BackupVersion.pbix"
|
||||
if (Test-Path $backupPath) {
|
||||
Import-PowerBIReport -WorkspaceId $WorkspaceId -FilePath $backupPath -ConflictAction "Overwrite"
|
||||
Write-Host "✅ Rollback completed successfully"
|
||||
} else {
|
||||
throw "Backup file not found: $backupPath"
|
||||
}
|
||||
|
||||
# Step 3: Validate rollback
|
||||
Test-PowerBIDataQuality -WorkspaceId $WorkspaceId
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring and Alerting
|
||||
|
||||
### 1. Deployment Health Checks
|
||||
```powershell
|
||||
# Post-deployment validation
|
||||
function Test-DeploymentHealth {
|
||||
param(
|
||||
[string]$WorkspaceId,
|
||||
[array]$ExpectedReports,
|
||||
[array]$ExpectedDatasets
|
||||
)
|
||||
|
||||
$healthCheck = @{
|
||||
Status = "Healthy"
|
||||
Issues = @()
|
||||
Timestamp = Get-Date
|
||||
}
|
||||
|
||||
# Check reports
|
||||
$reports = Get-PowerBIReport -WorkspaceId $WorkspaceId
|
||||
foreach ($expectedReport in $ExpectedReports) {
|
||||
if (-not ($reports.Name -contains $expectedReport)) {
|
||||
$healthCheck.Issues += "Missing report: $expectedReport"
|
||||
$healthCheck.Status = "Unhealthy"
|
||||
}
|
||||
}
|
||||
|
||||
# Check datasets
|
||||
$datasets = Get-PowerBIDataset -WorkspaceId $WorkspaceId
|
||||
foreach ($expectedDataset in $ExpectedDatasets) {
|
||||
$dataset = $datasets | Where-Object { $_.Name -eq $expectedDataset }
|
||||
if (-not $dataset) {
|
||||
$healthCheck.Issues += "Missing dataset: $expectedDataset"
|
||||
$healthCheck.Status = "Unhealthy"
|
||||
} elseif ($dataset.RefreshState -eq "Failed") {
|
||||
$healthCheck.Issues += "Dataset refresh failed: $expectedDataset"
|
||||
$healthCheck.Status = "Degraded"
|
||||
}
|
||||
}
|
||||
|
||||
return $healthCheck
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Automated Alerting
|
||||
```powershell
|
||||
# Teams notification for deployment status
|
||||
function Send-DeploymentNotification {
|
||||
param(
|
||||
[string]$TeamsWebhookUrl,
|
||||
[object]$DeploymentResult,
|
||||
[string]$Environment
|
||||
)
|
||||
|
||||
$color = switch ($DeploymentResult.Status) {
|
||||
"Success" { "28A745" }
|
||||
"Warning" { "FFC107" }
|
||||
"Failed" { "DC3545" }
|
||||
}
|
||||
|
||||
$teamsMessage = @{
|
||||
"@type" = "MessageCard"
|
||||
"@context" = "https://schema.org/extensions"
|
||||
"summary" = "Power BI Deployment $($DeploymentResult.Status)"
|
||||
"themeColor" = $color
|
||||
"sections" = @(
|
||||
@{
|
||||
"activityTitle" = "Power BI Deployment to $Environment"
|
||||
"activitySubtitle" = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
"facts" = @(
|
||||
@{
|
||||
"name" = "Status"
|
||||
"value" = $DeploymentResult.Status
|
||||
},
|
||||
@{
|
||||
"name" = "Duration"
|
||||
"value" = "$($DeploymentResult.Duration) minutes"
|
||||
},
|
||||
@{
|
||||
"name" = "Reports Deployed"
|
||||
"value" = $DeploymentResult.ReportsCount
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Invoke-RestMethod -Uri $TeamsWebhookUrl -Method Post -Body ($teamsMessage | ConvertTo-Json -Depth 10) -ContentType 'application/json'
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
### ✅ DevOps Best Practices
|
||||
|
||||
1. **Version Control Everything**
|
||||
- Use PBIP format for source control
|
||||
- Include model, reports, and configuration
|
||||
- Implement branching strategies (GitFlow)
|
||||
|
||||
2. **Automated Testing**
|
||||
- Data quality validation
|
||||
- Performance regression tests
|
||||
- Security compliance checks
|
||||
|
||||
3. **Environment Isolation**
|
||||
- Separate dev/test/prod environments
|
||||
- Environment-specific configurations
|
||||
- Automated promotion pipelines
|
||||
|
||||
4. **Security Integration**
|
||||
- Service principal authentication
|
||||
- Secret management with Key Vault
|
||||
- Role-based access controls
|
||||
|
||||
### ❌ Anti-Patterns to Avoid
|
||||
|
||||
1. **Manual Deployments**
|
||||
- Direct publishing from Desktop
|
||||
- Manual configuration changes
|
||||
- No rollback strategy
|
||||
|
||||
2. **Environment Coupling**
|
||||
- Hardcoded connection strings
|
||||
- Environment-specific reports
|
||||
- Manual testing only
|
||||
|
||||
3. **Poor Change Management**
|
||||
- No version control
|
||||
- Direct production changes
|
||||
- Missing audit trails
|
||||
|
||||
Remember: DevOps for Power BI requires a combination of proper tooling, automated processes, and organizational discipline. Start with basic CI/CD and gradually mature your practices based on organizational needs and compliance requirements.
|
||||
@@ -0,0 +1,752 @@
|
||||
---
|
||||
description: 'Comprehensive Power BI report design and visualization best practices based on Microsoft guidance for creating effective, accessible, and performant reports and dashboards.'
|
||||
applyTo: '**/*.{pbix,md,json,txt}'
|
||||
---
|
||||
|
||||
# Power BI Report Design and Visualization Best Practices
|
||||
|
||||
## Overview
|
||||
This document provides comprehensive instructions for designing effective, accessible, and performant Power BI reports and dashboards following Microsoft's official guidance and user experience best practices.
|
||||
|
||||
## Fundamental Design Principles
|
||||
|
||||
### 1. Information Architecture
|
||||
**Visual Hierarchy** - Organize information by importance:
|
||||
- **Primary**: Key metrics, KPIs, most critical insights (top-left, header area)
|
||||
- **Secondary**: Supporting details, trends, comparisons (main body)
|
||||
- **Tertiary**: Filters, controls, navigation elements (sidebars, footers)
|
||||
|
||||
**Content Structure**:
|
||||
```
|
||||
Report Page Layout:
|
||||
┌─────────────────────────────────────┐
|
||||
│ Header: Title, KPIs, Key Metrics │
|
||||
├─────────────────────────────────────┤
|
||||
│ Main Content Area │
|
||||
│ ┌─────────────┐ ┌─────────────────┐ │
|
||||
│ │ Primary │ │ Supporting │ │
|
||||
│ │ Visual │ │ Visuals │ │
|
||||
│ └─────────────┘ └─────────────────┘ │
|
||||
├─────────────────────────────────────┤
|
||||
│ Footer: Filters, Navigation, Notes │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2. User Experience Principles
|
||||
**Clarity**: Every element should have a clear purpose and meaning
|
||||
**Consistency**: Use consistent styling, colors, and interactions across all reports
|
||||
**Context**: Provide sufficient context for users to interpret data correctly
|
||||
**Action**: Guide users toward actionable insights and decisions
|
||||
|
||||
## Chart Type Selection Guidelines
|
||||
|
||||
### 1. Comparison Visualizations
|
||||
```
|
||||
Bar/Column Charts:
|
||||
✅ Comparing categories or entities
|
||||
✅ Ranking items by value
|
||||
✅ Showing changes over discrete time periods
|
||||
✅ When category names are long (use horizontal bars)
|
||||
|
||||
Best Practices:
|
||||
- Start axis at zero for accurate comparison
|
||||
- Sort categories by value for ranking
|
||||
- Use consistent colors within category groups
|
||||
- Limit to 7-10 categories for readability
|
||||
|
||||
Example Use Cases:
|
||||
- Sales by product category
|
||||
- Revenue by region
|
||||
- Employee count by department
|
||||
- Customer satisfaction by service type
|
||||
```
|
||||
|
||||
```
|
||||
Line Charts:
|
||||
✅ Showing trends over continuous time periods
|
||||
✅ Comparing multiple metrics over time
|
||||
✅ Identifying patterns, seasonality, cycles
|
||||
✅ Forecasting and projection scenarios
|
||||
|
||||
Best Practices:
|
||||
- Use consistent time intervals
|
||||
- Start Y-axis at zero when showing absolute values
|
||||
- Use different line styles for multiple series
|
||||
- Include data point markers for sparse data
|
||||
|
||||
Example Use Cases:
|
||||
- Monthly sales trends
|
||||
- Website traffic over time
|
||||
- Stock price movements
|
||||
- Performance metrics tracking
|
||||
```
|
||||
|
||||
### 2. Composition Visualizations
|
||||
```
|
||||
Pie/Donut Charts:
|
||||
✅ Parts-of-whole relationships
|
||||
✅ Maximum 5-7 categories
|
||||
✅ When percentages are more important than absolute values
|
||||
✅ Simple composition scenarios
|
||||
|
||||
Limitations:
|
||||
❌ Difficult to compare similar-sized segments
|
||||
❌ Not suitable for many categories
|
||||
❌ Hard to show changes over time
|
||||
|
||||
Alternative: Use stacked bar charts for better readability
|
||||
|
||||
Example Use Cases:
|
||||
- Market share by competitor
|
||||
- Budget allocation by category
|
||||
- Customer segments by type
|
||||
```
|
||||
|
||||
```
|
||||
Stacked Charts:
|
||||
✅ Showing composition and total simultaneously
|
||||
✅ Comparing composition across categories
|
||||
✅ Multiple sub-categories within main categories
|
||||
✅ When you need both part and whole perspective
|
||||
|
||||
Types:
|
||||
- 100% Stacked: Focus on proportions
|
||||
- Regular Stacked: Show both absolute and relative values
|
||||
- Clustered: Compare sub-categories side-by-side
|
||||
|
||||
Example Use Cases:
|
||||
- Sales by product category and region
|
||||
- Revenue breakdown by service type over time
|
||||
- Employee distribution by department and level
|
||||
```
|
||||
|
||||
### 3. Relationship and Distribution Visualizations
|
||||
```
|
||||
Scatter Plots:
|
||||
✅ Correlation between two continuous variables
|
||||
✅ Outlier identification
|
||||
✅ Clustering analysis
|
||||
✅ Performance quadrant analysis
|
||||
|
||||
Best Practices:
|
||||
- Use size for third dimension (bubble chart)
|
||||
- Apply color coding for categories
|
||||
- Include trend lines when appropriate
|
||||
- Label outliers and key points
|
||||
|
||||
Example Use Cases:
|
||||
- Sales vs. marketing spend by product
|
||||
- Customer satisfaction vs. loyalty scores
|
||||
- Risk vs. return analysis
|
||||
- Performance vs. cost efficiency
|
||||
```
|
||||
|
||||
```
|
||||
Heat Maps:
|
||||
✅ Showing patterns across two categorical dimensions
|
||||
✅ Performance matrices
|
||||
✅ Time-based pattern analysis
|
||||
✅ Dense data visualization
|
||||
|
||||
Configuration:
|
||||
- Use color scales that are colorblind-friendly
|
||||
- Include data labels when space permits
|
||||
- Provide clear legend with value ranges
|
||||
- Consider using conditional formatting
|
||||
|
||||
Example Use Cases:
|
||||
- Sales performance by month and product
|
||||
- Website traffic by hour and day of week
|
||||
- Employee performance ratings by department and quarter
|
||||
```
|
||||
|
||||
## Report Layout and Navigation Design
|
||||
|
||||
### 1. Page Layout Strategies
|
||||
```
|
||||
Single Page Dashboard:
|
||||
✅ Executive summaries
|
||||
✅ Real-time monitoring
|
||||
✅ Simple KPI tracking
|
||||
✅ Mobile-first scenarios
|
||||
|
||||
Design Guidelines:
|
||||
- Maximum 6-8 visuals per page
|
||||
- Clear visual hierarchy
|
||||
- Logical grouping of related content
|
||||
- Responsive design considerations
|
||||
```
|
||||
|
||||
```
|
||||
Multi-Page Report:
|
||||
✅ Complex analytical scenarios
|
||||
✅ Different user personas
|
||||
✅ Detailed drill-down analysis
|
||||
✅ Comprehensive business reporting
|
||||
|
||||
Page Organization:
|
||||
Page 1: Executive Summary (high-level KPIs)
|
||||
Page 2: Detailed Analysis (trends, comparisons)
|
||||
Page 3: Operational Details (transaction-level data)
|
||||
Page 4: Appendix (methodology, definitions)
|
||||
```
|
||||
|
||||
### 2. Navigation Patterns
|
||||
```
|
||||
Tab Navigation:
|
||||
✅ Related content areas
|
||||
✅ Different views of same data
|
||||
✅ User role-based sections
|
||||
✅ Temporal analysis (daily, weekly, monthly)
|
||||
|
||||
Implementation:
|
||||
- Use descriptive tab names
|
||||
- Maintain consistent layout across tabs
|
||||
- Highlight active tab clearly
|
||||
- Consider tab ordering by importance
|
||||
```
|
||||
|
||||
```
|
||||
Bookmark Navigation:
|
||||
✅ Predefined scenarios
|
||||
✅ Filtered views
|
||||
✅ Story-telling sequences
|
||||
✅ Guided analysis paths
|
||||
|
||||
Best Practices:
|
||||
- Create bookmarks for common filter combinations
|
||||
- Use descriptive bookmark names
|
||||
- Group related bookmarks
|
||||
- Test bookmark functionality thoroughly
|
||||
```
|
||||
|
||||
```
|
||||
Button Navigation:
|
||||
✅ Custom navigation flows
|
||||
✅ Action-oriented interactions
|
||||
✅ Drill-down scenarios
|
||||
✅ External link integration
|
||||
|
||||
Button Design:
|
||||
- Use consistent styling
|
||||
- Clear, action-oriented labels
|
||||
- Appropriate sizing for touch interfaces
|
||||
- Visual feedback for interactions
|
||||
```
|
||||
|
||||
## Interactive Features Implementation
|
||||
|
||||
### 1. Tooltip Design Strategy
|
||||
```
|
||||
Default Tooltips:
|
||||
✅ Additional context information
|
||||
✅ Formatted numeric values
|
||||
✅ Related metrics not shown in visual
|
||||
✅ Explanatory text for complex measures
|
||||
|
||||
Configuration:
|
||||
- Include relevant dimensions
|
||||
- Format numbers appropriately
|
||||
- Keep text concise and readable
|
||||
- Use consistent formatting
|
||||
|
||||
Example:
|
||||
Visual: Sales by Product Category
|
||||
Tooltip:
|
||||
- Product Category: Electronics
|
||||
- Total Sales: $2.3M (↑15% vs last year)
|
||||
- Order Count: 1,247 orders
|
||||
- Avg Order Value: $1,845
|
||||
```
|
||||
|
||||
```
|
||||
Report Page Tooltips:
|
||||
✅ Complex additional information
|
||||
✅ Mini-dashboard for context
|
||||
✅ Detailed breakdowns
|
||||
✅ Visual explanations
|
||||
|
||||
Design Requirements:
|
||||
- Optimal size: 320x240 pixels
|
||||
- Match main report styling
|
||||
- Fast loading performance
|
||||
- Meaningful additional insights
|
||||
|
||||
Implementation:
|
||||
1. Create dedicated tooltip page
|
||||
2. Set page type to "Tooltip"
|
||||
3. Configure appropriate filters
|
||||
4. Enable tooltip on target visuals
|
||||
5. Test with realistic data
|
||||
```
|
||||
|
||||
### 2. Drillthrough Implementation
|
||||
```
|
||||
Drillthrough Scenarios:
|
||||
|
||||
Summary to Detail:
|
||||
Source: Monthly Sales Summary
|
||||
Target: Transaction-level details for selected month
|
||||
Filter: Month, Product Category, Region
|
||||
|
||||
Context Enhancement:
|
||||
Source: Product Performance Metric
|
||||
Target: Comprehensive product analysis
|
||||
Content: Sales trends, customer feedback, inventory levels
|
||||
|
||||
Design Guidelines:
|
||||
✅ Clear visual indication of drillthrough availability
|
||||
✅ Consistent styling between source and target pages
|
||||
✅ Automatic back button (provided by Power BI)
|
||||
✅ Contextual filters properly applied
|
||||
✅ Hidden drillthrough pages from main navigation
|
||||
|
||||
Implementation Steps:
|
||||
1. Create target drillthrough page
|
||||
2. Add drillthrough filters in Fields pane
|
||||
3. Design page with filtered context in mind
|
||||
4. Test drillthrough functionality
|
||||
5. Configure source visuals for drillthrough
|
||||
```
|
||||
|
||||
### 3. Cross-Filtering Strategy
|
||||
```
|
||||
When to Enable Cross-Filtering:
|
||||
✅ Related visuals showing different perspectives
|
||||
✅ Clear logical connections between visuals
|
||||
✅ Enhanced analytical understanding
|
||||
✅ Reasonable performance impact
|
||||
|
||||
When to Disable Cross-Filtering:
|
||||
❌ Independent analysis requirements
|
||||
❌ Performance concerns with large datasets
|
||||
❌ Confusing or misleading interactions
|
||||
❌ Too many visuals causing cluttered highlighting
|
||||
|
||||
Configuration Best Practices:
|
||||
- Edit interactions thoughtfully for each visual pair
|
||||
- Test with realistic data volumes and user scenarios
|
||||
- Provide clear visual feedback for selections
|
||||
- Consider mobile touch interaction experience
|
||||
- Document interaction design decisions
|
||||
```
|
||||
|
||||
## Visual Design and Formatting
|
||||
|
||||
### 1. Color Strategy
|
||||
```
|
||||
Color Usage Hierarchy:
|
||||
|
||||
Semantic Colors (Consistent Meaning):
|
||||
- Green: Positive performance, growth, success, on-target
|
||||
- Red: Negative performance, decline, alerts, over-budget
|
||||
- Blue: Neutral information, base metrics, corporate branding
|
||||
- Orange: Warnings, attention needed, moderate concern
|
||||
- Gray: Inactive, disabled, or reference information
|
||||
|
||||
Brand Integration:
|
||||
✅ Use corporate color palette consistently
|
||||
✅ Maintain accessibility standards (4.5:1 contrast ratio minimum)
|
||||
✅ Consider colorblind accessibility (8% of males affected)
|
||||
✅ Test colors in different contexts (projectors, mobile, print)
|
||||
|
||||
Color Application:
|
||||
Primary Color: Main brand color for key metrics and highlights
|
||||
Secondary Colors: Supporting brand colors for categories
|
||||
Accent Colors: High-contrast colors for alerts and callouts
|
||||
Neutral Colors: Backgrounds, text, borders, inactive states
|
||||
```
|
||||
|
||||
```
|
||||
Accessibility-First Color Design:
|
||||
|
||||
Colorblind Considerations:
|
||||
✅ Don't rely solely on color to convey information
|
||||
✅ Use patterns, shapes, or text labels as alternatives
|
||||
✅ Test with colorblind simulation tools
|
||||
✅ Use high contrast color combinations
|
||||
✅ Provide alternative visual cues (icons, patterns)
|
||||
|
||||
Implementation:
|
||||
- Red-Green combinations: Add blue or use different saturations
|
||||
- Use tools like Colour Oracle for testing
|
||||
- Include data labels where color is the primary differentiator
|
||||
- Consider grayscale versions of reports for printing
|
||||
```
|
||||
|
||||
### 2. Typography and Readability
|
||||
```
|
||||
Font Hierarchy:
|
||||
|
||||
Report Titles: 18-24pt, Bold, Corporate font or clear sans-serif
|
||||
Page Titles: 16-20pt, Semi-bold, Consistent with report title
|
||||
Section Headers: 14-16pt, Semi-bold, Used for content grouping
|
||||
Visual Titles: 12-14pt, Semi-bold, Descriptive and concise
|
||||
Body Text: 10-12pt, Regular, Used in text boxes and descriptions
|
||||
Data Labels: 9-11pt, Regular, Clear and not overlapping
|
||||
Captions/Legends: 8-10pt, Regular, Supplementary information
|
||||
|
||||
Readability Guidelines:
|
||||
✅ Minimum 10pt font size for data visualization
|
||||
✅ High contrast between text and background
|
||||
✅ Consistent font family throughout report (max 2 families)
|
||||
✅ Adequate white space around text elements
|
||||
✅ Left-align text for readability (except centered titles)
|
||||
```
|
||||
|
||||
```
|
||||
Content Writing Best Practices:
|
||||
|
||||
Titles and Labels:
|
||||
✅ Clear, descriptive, and action-oriented
|
||||
✅ Avoid jargon and technical abbreviations
|
||||
✅ Use consistent terminology throughout
|
||||
✅ Include time periods and context when relevant
|
||||
|
||||
Examples:
|
||||
Good: "Monthly Sales Revenue by Product Category"
|
||||
Poor: "Sales Data"
|
||||
|
||||
Good: "Customer Satisfaction Score (1-10 scale)"
|
||||
Poor: "CSAT"
|
||||
|
||||
Data Storytelling:
|
||||
✅ Use subtitles to provide context
|
||||
✅ Include methodology notes where necessary
|
||||
✅ Explain unusual data points or outliers
|
||||
✅ Provide actionable insights in text boxes
|
||||
```
|
||||
|
||||
### 3. Layout and Spacing
|
||||
```
|
||||
Visual Spacing:
|
||||
Grid System: Use consistent spacing multiples (8px, 16px, 24px)
|
||||
Padding: Adequate white space around content areas
|
||||
Margins: Consistent margins between visual elements
|
||||
Alignment: Use alignment guides for professional appearance
|
||||
|
||||
Visual Grouping:
|
||||
Related Content: Group related visuals with consistent spacing
|
||||
Separation: Use white space to separate unrelated content areas
|
||||
Visual Hierarchy: Use size, color, and spacing to indicate importance
|
||||
Balance: Distribute visual weight evenly across the page
|
||||
```
|
||||
|
||||
## Performance Optimization for Reports
|
||||
|
||||
### 1. Visual Performance Guidelines
|
||||
```
|
||||
Visual Count Management:
|
||||
✅ Maximum 6-8 visuals per page for optimal performance
|
||||
✅ Use tabbed navigation for complex scenarios
|
||||
✅ Implement drill-through instead of cramming details
|
||||
✅ Consider multiple focused pages vs. one cluttered page
|
||||
|
||||
Query Optimization:
|
||||
✅ Apply filters early in design process
|
||||
✅ Use page-level filters for common filtering scenarios
|
||||
✅ Avoid high-cardinality fields in slicers when possible
|
||||
✅ Pre-filter large datasets to relevant subsets
|
||||
|
||||
Performance Testing:
|
||||
✅ Test with realistic data volumes
|
||||
✅ Monitor Performance Analyzer results
|
||||
✅ Test concurrent user scenarios
|
||||
✅ Validate mobile performance
|
||||
✅ Check different network conditions
|
||||
```
|
||||
|
||||
### 2. Loading Performance Optimization
|
||||
```
|
||||
Initial Page Load:
|
||||
✅ Minimize visuals on landing page
|
||||
✅ Use summary views with drill-through to details
|
||||
✅ Apply default filters to reduce initial data volume
|
||||
✅ Consider progressive disclosure of information
|
||||
|
||||
Interaction Performance:
|
||||
✅ Optimize slicer queries and combinations
|
||||
✅ Use efficient cross-filtering patterns
|
||||
✅ Minimize complex calculated visuals
|
||||
✅ Implement appropriate caching strategies
|
||||
|
||||
Visual Selection for Performance:
|
||||
Fast Loading: Card, KPI, Gauge (simple aggregations)
|
||||
Moderate: Bar, Column, Line charts (standard aggregations)
|
||||
Slower: Scatter plots, Maps, Custom visuals (complex calculations)
|
||||
Slowest: Matrix, Table with many columns (detailed data)
|
||||
```
|
||||
|
||||
## Mobile and Responsive Design
|
||||
|
||||
### 1. Mobile Layout Strategy
|
||||
```
|
||||
Mobile-First Design Principles:
|
||||
✅ Portrait orientation as primary layout
|
||||
✅ Touch-friendly interaction targets (minimum 44px)
|
||||
✅ Simplified navigation patterns
|
||||
✅ Reduced visual density and information overload
|
||||
✅ Key metrics prominently displayed
|
||||
|
||||
Mobile Layout Considerations:
|
||||
Screen Sizes: Design for smallest target device first
|
||||
Touch Interactions: Ensure buttons and slicers are easily tappable
|
||||
Scrolling: Vertical scrolling acceptable, horizontal scrolling problematic
|
||||
Text Size: Increase font sizes for mobile readability
|
||||
Visual Selection: Prefer simple chart types for mobile
|
||||
```
|
||||
|
||||
### 2. Responsive Design Implementation
|
||||
```
|
||||
Power BI Mobile Layout:
|
||||
1. Switch to Mobile layout view in Power BI Desktop
|
||||
2. Rearrange visuals for portrait orientation
|
||||
3. Resize and reposition for mobile screens
|
||||
4. Test key interactions work with touch
|
||||
5. Verify text remains readable at mobile sizes
|
||||
|
||||
Adaptive Content:
|
||||
✅ Prioritize most important information
|
||||
✅ Hide or consolidate less critical visuals
|
||||
✅ Use drill-through for detailed analysis
|
||||
✅ Simplify filter interfaces
|
||||
✅ Ensure data refresh works on mobile connections
|
||||
|
||||
Testing Strategy:
|
||||
✅ Test on actual mobile devices
|
||||
✅ Verify performance on slower networks
|
||||
✅ Check battery usage during extended use
|
||||
✅ Validate offline capabilities where applicable
|
||||
```
|
||||
|
||||
## Accessibility and Inclusive Design
|
||||
|
||||
### 1. Universal Design Principles
|
||||
```
|
||||
Visual Accessibility:
|
||||
✅ High contrast ratios (minimum 4.5:1)
|
||||
✅ Colorblind-friendly color schemes
|
||||
✅ Alternative text for images and icons
|
||||
✅ Consistent navigation patterns
|
||||
✅ Clear visual hierarchy
|
||||
|
||||
Interaction Accessibility:
|
||||
✅ Keyboard navigation support
|
||||
✅ Screen reader compatibility
|
||||
✅ Clear focus indicators
|
||||
✅ Logical tab order
|
||||
✅ Descriptive link text and button labels
|
||||
|
||||
Content Accessibility:
|
||||
✅ Plain language explanations
|
||||
✅ Consistent terminology
|
||||
✅ Context for abbreviations and acronyms
|
||||
✅ Alternative formats when needed
|
||||
```
|
||||
|
||||
### 2. Inclusive Design Implementation
|
||||
```
|
||||
Multi-Sensory Design:
|
||||
✅ Don't rely solely on color to convey information
|
||||
✅ Use patterns, shapes, and text labels
|
||||
✅ Include audio descriptions for complex visuals
|
||||
✅ Provide data in multiple formats
|
||||
|
||||
Cognitive Accessibility:
|
||||
✅ Clear, simple language
|
||||
✅ Logical information flow
|
||||
✅ Consistent layouts and interactions
|
||||
✅ Progressive disclosure of complexity
|
||||
✅ Help and guidance text where needed
|
||||
|
||||
Testing for Accessibility:
|
||||
✅ Use screen readers to test navigation
|
||||
✅ Test keyboard-only navigation
|
||||
✅ Verify with colorblind simulation tools
|
||||
✅ Get feedback from users with disabilities
|
||||
✅ Regular accessibility audits
|
||||
```
|
||||
|
||||
## Advanced Visualization Techniques
|
||||
|
||||
### 1. Conditional Formatting
|
||||
```
|
||||
Dynamic Visual Enhancement:
|
||||
|
||||
Data Bars:
|
||||
✅ Quick visual comparison within tables
|
||||
✅ Consistent scale across all rows
|
||||
✅ Appropriate color choices (light to dark)
|
||||
✅ Consider mobile visibility
|
||||
|
||||
Background Colors:
|
||||
✅ Heat map-style formatting
|
||||
✅ Status-based coloring (red/yellow/green)
|
||||
✅ Performance thresholds
|
||||
✅ Trend indicators
|
||||
|
||||
Font Formatting:
|
||||
✅ Size based on importance or values
|
||||
✅ Color based on performance metrics
|
||||
✅ Bold for emphasis and highlights
|
||||
✅ Italics for secondary information
|
||||
|
||||
Implementation Examples:
|
||||
Sales Performance Table:
|
||||
- Green background: >110% of target
|
||||
- Yellow background: 90-110% of target
|
||||
- Red background: <90% of target
|
||||
- Data bars: Relative performance within each category
|
||||
```
|
||||
|
||||
### 2. Custom Visuals Integration
|
||||
```
|
||||
Custom Visual Selection Criteria:
|
||||
|
||||
Evaluation Framework:
|
||||
✅ Active community support and regular updates
|
||||
✅ Microsoft AppSource certification (preferred)
|
||||
✅ Clear documentation and examples
|
||||
✅ Performance characteristics acceptable
|
||||
✅ Accessibility compliance
|
||||
|
||||
Due Diligence:
|
||||
✅ Test thoroughly with your data types and volumes
|
||||
✅ Verify mobile compatibility
|
||||
✅ Check refresh and performance impact
|
||||
✅ Review security and data handling
|
||||
✅ Plan for maintenance and updates
|
||||
|
||||
Governance:
|
||||
✅ Approval process for custom visuals
|
||||
✅ Standard set of approved custom visuals
|
||||
✅ Documentation of approved visuals and use cases
|
||||
✅ Monitoring and maintenance procedures
|
||||
✅ Fallback strategies if custom visual becomes unavailable
|
||||
```
|
||||
|
||||
## Report Testing and Quality Assurance
|
||||
|
||||
### 1. Functional Testing Checklist
|
||||
```
|
||||
Visual Functionality:
|
||||
□ All charts display data correctly
|
||||
□ Filters work as intended
|
||||
□ Cross-filtering behaves appropriately
|
||||
□ Drill-through functions correctly
|
||||
□ Tooltips show relevant information
|
||||
□ Bookmarks restore correct state
|
||||
□ Export functions work properly
|
||||
|
||||
Interaction Testing:
|
||||
□ Button navigation functions correctly
|
||||
□ Slicer combinations work as expected
|
||||
□ Report pages load within acceptable time
|
||||
□ Mobile layout displays properly
|
||||
□ Responsive design adapts correctly
|
||||
□ Print layouts are readable
|
||||
|
||||
Data Accuracy:
|
||||
□ Totals match source systems
|
||||
□ Calculations produce expected results
|
||||
□ Filters don't inadvertently exclude data
|
||||
□ Date ranges are correct
|
||||
□ Business rules are properly implemented
|
||||
□ Edge cases handled appropriately
|
||||
```
|
||||
|
||||
### 2. User Experience Testing
|
||||
```
|
||||
Usability Testing:
|
||||
✅ Test with actual business users
|
||||
✅ Observe user behavior and pain points
|
||||
✅ Time common tasks and interactions
|
||||
✅ Gather feedback on clarity and usefulness
|
||||
✅ Test with different user skill levels
|
||||
|
||||
Performance Testing:
|
||||
✅ Load testing with realistic data volumes
|
||||
✅ Concurrent user testing
|
||||
✅ Network condition variations
|
||||
✅ Mobile device performance
|
||||
✅ Refresh performance during peak usage
|
||||
|
||||
Cross-Platform Testing:
|
||||
✅ Desktop browsers (Chrome, Firefox, Edge, Safari)
|
||||
✅ Mobile devices (iOS, Android)
|
||||
✅ Power BI Mobile app
|
||||
✅ Different screen resolutions
|
||||
✅ Various network speeds
|
||||
```
|
||||
|
||||
### 3. Quality Assurance Framework
|
||||
```
|
||||
Review Process:
|
||||
1. Developer Testing: Initial functionality verification
|
||||
2. Peer Review: Design and technical review by colleagues
|
||||
3. Business Review: Content accuracy and usefulness validation
|
||||
4. User Acceptance: Testing with end users
|
||||
5. Performance Review: Load and response time validation
|
||||
6. Accessibility Review: Compliance with accessibility standards
|
||||
|
||||
Documentation:
|
||||
✅ Report purpose and target audience
|
||||
✅ Data sources and refresh schedule
|
||||
✅ Business rules and calculation explanations
|
||||
✅ User guide and training materials
|
||||
✅ Known limitations and workarounds
|
||||
✅ Maintenance and update procedures
|
||||
|
||||
Continuous Improvement:
|
||||
✅ Regular usage analytics review
|
||||
✅ User feedback collection and analysis
|
||||
✅ Performance monitoring and optimization
|
||||
✅ Content relevance and accuracy updates
|
||||
✅ Technology and feature adoption
|
||||
```
|
||||
|
||||
## Common Anti-Patterns to Avoid
|
||||
|
||||
### 1. Design Anti-Patterns
|
||||
```
|
||||
❌ Chart Junk:
|
||||
- Unnecessary 3D effects
|
||||
- Excessive animation
|
||||
- Decorative elements that don't add value
|
||||
- Over-complicated visual effects
|
||||
|
||||
❌ Information Overload:
|
||||
- Too many visuals on single page
|
||||
- Cluttered layouts with insufficient white space
|
||||
- Multiple competing focal points
|
||||
- Overwhelming color usage
|
||||
|
||||
❌ Poor Color Choices:
|
||||
- Red-green combinations without alternatives
|
||||
- Low contrast ratios
|
||||
- Inconsistent color meanings
|
||||
- Over-use of bright or saturated colors
|
||||
```
|
||||
|
||||
### 2. Interaction Anti-Patterns
|
||||
```
|
||||
❌ Navigation Confusion:
|
||||
- Inconsistent navigation patterns
|
||||
- Hidden or unclear navigation options
|
||||
- Broken or unexpected drill-through behavior
|
||||
- Circular navigation loops
|
||||
|
||||
❌ Performance Problems:
|
||||
- Too many visuals causing slow loading
|
||||
- Inefficient cross-filtering
|
||||
- Unnecessary real-time refresh
|
||||
- Large datasets without proper filtering
|
||||
|
||||
❌ Mobile Unfriendly:
|
||||
- Small touch targets
|
||||
- Horizontal scrolling requirements
|
||||
- Unreadable text on mobile
|
||||
- Non-functional mobile interactions
|
||||
```
|
||||
|
||||
Remember: Always design with your specific users and use cases in mind. Test early and often with real users and realistic data to ensure your reports effectively communicate insights and enable data-driven decision making.
|
||||
@@ -0,0 +1,504 @@
|
||||
---
|
||||
description: 'Comprehensive Power BI Row-Level Security (RLS) and advanced security patterns implementation guide with dynamic security, best practices, and governance strategies.'
|
||||
applyTo: '**/*.{pbix,dax,md,txt,json,csharp,powershell}'
|
||||
---
|
||||
|
||||
# Power BI Security and Row-Level Security Best Practices
|
||||
|
||||
## Overview
|
||||
This document provides comprehensive instructions for implementing robust security patterns in Power BI, focusing on Row-Level Security (RLS), dynamic security, and governance best practices based on Microsoft's official guidance.
|
||||
|
||||
## Row-Level Security Fundamentals
|
||||
|
||||
### 1. Basic RLS Implementation
|
||||
```dax
|
||||
// Simple user-based filtering
|
||||
[EmailAddress] = USERNAME()
|
||||
|
||||
// Role-based filtering with improved security
|
||||
IF(
|
||||
USERNAME() = "Worker",
|
||||
[Type] = "Internal",
|
||||
IF(
|
||||
USERNAME() = "Manager",
|
||||
TRUE(),
|
||||
FALSE() // Deny access to unexpected users
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Dynamic RLS with Custom Data
|
||||
```dax
|
||||
// Using CUSTOMDATA() for dynamic filtering
|
||||
VAR UserRole = CUSTOMDATA()
|
||||
RETURN
|
||||
SWITCH(
|
||||
UserRole,
|
||||
"SalesPersonA", [SalesTerritory] = "West",
|
||||
"SalesPersonB", [SalesTerritory] = "East",
|
||||
"Manager", TRUE(),
|
||||
FALSE() // Default deny
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Advanced Security Patterns
|
||||
```dax
|
||||
// Hierarchical security with territory lookups
|
||||
=DimSalesTerritory[SalesTerritoryKey]=LOOKUPVALUE(
|
||||
DimUserSecurity[SalesTerritoryID],
|
||||
DimUserSecurity[UserName], USERNAME(),
|
||||
DimUserSecurity[SalesTerritoryID], DimSalesTerritory[SalesTerritoryKey]
|
||||
)
|
||||
|
||||
// Multiple condition security
|
||||
VAR UserTerritories =
|
||||
FILTER(
|
||||
UserSecurity,
|
||||
UserSecurity[UserName] = USERNAME()
|
||||
)
|
||||
VAR AllowedTerritories = SELECTCOLUMNS(UserTerritories, "Territory", UserSecurity[Territory])
|
||||
RETURN
|
||||
[Territory] IN AllowedTerritories
|
||||
```
|
||||
|
||||
## Embedded Analytics Security
|
||||
|
||||
### 1. Static RLS Implementation
|
||||
```csharp
|
||||
// Static RLS with fixed roles
|
||||
var rlsidentity = new EffectiveIdentity(
|
||||
username: "username@contoso.com",
|
||||
roles: new List<string>{ "MyRole" },
|
||||
datasets: new List<string>{ datasetId.ToString()}
|
||||
);
|
||||
```
|
||||
|
||||
### 2. Dynamic RLS with Custom Data
|
||||
```csharp
|
||||
// Dynamic RLS with custom data
|
||||
var rlsidentity = new EffectiveIdentity(
|
||||
username: "username@contoso.com",
|
||||
roles: new List<string>{ "MyRoleWithCustomData" },
|
||||
customData: "SalesPersonA",
|
||||
datasets: new List<string>{ datasetId.ToString()}
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Multi-Dataset Security
|
||||
```json
|
||||
{
|
||||
"accessLevel": "View",
|
||||
"identities": [
|
||||
{
|
||||
"username": "France",
|
||||
"roles": [ "CountryDynamic"],
|
||||
"datasets": [ "fe0a1aeb-f6a4-4b27-a2d3-b5df3bb28bdc" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Database-Level Security Integration
|
||||
|
||||
### 1. SQL Server RLS Integration
|
||||
```sql
|
||||
-- Creating security schema and predicate function
|
||||
CREATE SCHEMA Security;
|
||||
GO
|
||||
|
||||
CREATE FUNCTION Security.tvf_securitypredicate(@SalesRep AS nvarchar(50))
|
||||
RETURNS TABLE
|
||||
WITH SCHEMABINDING
|
||||
AS
|
||||
RETURN SELECT 1 AS tvf_securitypredicate_result
|
||||
WHERE @SalesRep = USER_NAME() OR USER_NAME() = 'Manager';
|
||||
GO
|
||||
|
||||
-- Applying security policy
|
||||
CREATE SECURITY POLICY SalesFilter
|
||||
ADD FILTER PREDICATE Security.tvf_securitypredicate(SalesRep)
|
||||
ON sales.Orders
|
||||
WITH (STATE = ON);
|
||||
GO
|
||||
```
|
||||
|
||||
### 2. Fabric Warehouse Security
|
||||
```sql
|
||||
-- Creating schema for Security
|
||||
CREATE SCHEMA Security;
|
||||
GO
|
||||
|
||||
-- Creating a function for the SalesRep evaluation
|
||||
CREATE FUNCTION Security.tvf_securitypredicate(@UserName AS varchar(50))
|
||||
RETURNS TABLE
|
||||
WITH SCHEMABINDING
|
||||
AS
|
||||
RETURN SELECT 1 AS tvf_securitypredicate_result
|
||||
WHERE @UserName = USER_NAME()
|
||||
OR USER_NAME() = 'BatchProcess@contoso.com';
|
||||
GO
|
||||
|
||||
-- Using the function to create a Security Policy
|
||||
CREATE SECURITY POLICY YourSecurityPolicy
|
||||
ADD FILTER PREDICATE Security.tvf_securitypredicate(UserName_column)
|
||||
ON sampleschema.sampletable
|
||||
WITH (STATE = ON);
|
||||
GO
|
||||
```
|
||||
|
||||
## Advanced Security Patterns
|
||||
|
||||
### 1. Paginated Reports Security
|
||||
```json
|
||||
{
|
||||
"format": "PDF",
|
||||
"paginatedReportConfiguration":{
|
||||
"identities": [
|
||||
{"username": "john@contoso.com"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Power Pages Integration
|
||||
```html
|
||||
{% powerbi authentication_type:"powerbiembedded" path:"https://app.powerbi.com/groups/00000000-0000-0000-0000-000000000000/reports/00000000-0000-0000-0000-000000000001/ReportSection" roles:"pagesuser" %}
|
||||
```
|
||||
|
||||
### 3. Multi-Tenant Security
|
||||
```json
|
||||
{
|
||||
"datasets": [
|
||||
{
|
||||
"id": "fff1a505-xxxx-xxxx-xxxx-e69f81e5b974",
|
||||
}
|
||||
],
|
||||
"reports": [
|
||||
{
|
||||
"allowEdit": false,
|
||||
"id": "10ce71df-xxxx-xxxx-xxxx-814a916b700d"
|
||||
}
|
||||
],
|
||||
"identities": [
|
||||
{
|
||||
"username": "YourUsername",
|
||||
"datasets": [
|
||||
"fff1a505-xxxx-xxxx-xxxx-e69f81e5b974"
|
||||
],
|
||||
"roles": [
|
||||
"YourRole"
|
||||
]
|
||||
}
|
||||
],
|
||||
"datasourceIdentities": [
|
||||
{
|
||||
"identityBlob": "eyJ…",
|
||||
"datasources": [
|
||||
{
|
||||
"datasourceType": "Sql",
|
||||
"connectionDetails": {
|
||||
"server": "YourServerName.database.windows.net",
|
||||
"database": "YourDataBaseName"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Security Design Patterns
|
||||
|
||||
### 1. Partial RLS Implementation
|
||||
```dax
|
||||
// Create summary table for partial RLS
|
||||
SalesRevenueSummary =
|
||||
SUMMARIZECOLUMNS(
|
||||
Sales[OrderDate],
|
||||
"RevenueAllRegion", SUM(Sales[Revenue])
|
||||
)
|
||||
|
||||
// Apply RLS only to detail level
|
||||
Salesperson Filter = [EmailAddress] = USERNAME()
|
||||
```
|
||||
|
||||
### 2. Hierarchical Security
|
||||
```dax
|
||||
// Manager can see all, others see their own
|
||||
VAR CurrentUser = USERNAME()
|
||||
VAR UserRole = LOOKUPVALUE(
|
||||
UserRoles[Role],
|
||||
UserRoles[Email], CurrentUser
|
||||
)
|
||||
RETURN
|
||||
SWITCH(
|
||||
UserRole,
|
||||
"Manager", TRUE(),
|
||||
"Salesperson", [SalespersonEmail] = CurrentUser,
|
||||
"Regional Manager", [Region] IN (
|
||||
SELECTCOLUMNS(
|
||||
FILTER(UserRegions, UserRegions[Email] = CurrentUser),
|
||||
"Region", UserRegions[Region]
|
||||
)
|
||||
),
|
||||
FALSE()
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Time-Based Security
|
||||
```dax
|
||||
// Restrict access to recent data based on role
|
||||
VAR UserRole = LOOKUPVALUE(UserRoles[Role], UserRoles[Email], USERNAME())
|
||||
VAR CutoffDate =
|
||||
SWITCH(
|
||||
UserRole,
|
||||
"Executive", DATE(1900,1,1), // All historical data
|
||||
"Manager", TODAY() - 365, // Last year
|
||||
"Analyst", TODAY() - 90, // Last 90 days
|
||||
TODAY() // Current day only
|
||||
)
|
||||
RETURN
|
||||
[Date] >= CutoffDate
|
||||
```
|
||||
|
||||
## Security Validation and Testing
|
||||
|
||||
### 1. Role Validation Patterns
|
||||
```dax
|
||||
// Security testing measure
|
||||
Security Test =
|
||||
VAR CurrentUsername = USERNAME()
|
||||
VAR ExpectedRole = "TestRole"
|
||||
VAR TestResult =
|
||||
IF(
|
||||
HASONEVALUE(SecurityRoles[Role]) &&
|
||||
VALUES(SecurityRoles[Role]) = ExpectedRole,
|
||||
"PASS: Role applied correctly",
|
||||
"FAIL: Incorrect role or multiple roles"
|
||||
)
|
||||
RETURN
|
||||
"User: " & CurrentUsername & " | " & TestResult
|
||||
```
|
||||
|
||||
### 2. Data Exposure Audit
|
||||
```dax
|
||||
// Audit measure to track data access
|
||||
Data Access Audit =
|
||||
VAR AccessibleRows = COUNTROWS(FactTable)
|
||||
VAR TotalRows = CALCULATE(COUNTROWS(FactTable), ALL(FactTable))
|
||||
VAR AccessPercentage = DIVIDE(AccessibleRows, TotalRows) * 100
|
||||
RETURN
|
||||
"User: " & USERNAME() &
|
||||
" | Accessible: " & FORMAT(AccessibleRows, "#,0") &
|
||||
" | Total: " & FORMAT(TotalRows, "#,0") &
|
||||
" | Access: " & FORMAT(AccessPercentage, "0.00") & "%"
|
||||
```
|
||||
|
||||
## Governance and Administration
|
||||
|
||||
### 1. Automated Security Group Management
|
||||
```powershell
|
||||
# Add security group to Power BI workspace
|
||||
# Sign in to Power BI
|
||||
Login-PowerBI
|
||||
|
||||
# Set up the security group object ID
|
||||
$SGObjectID = "<security-group-object-ID>"
|
||||
|
||||
# Get the workspace
|
||||
$pbiWorkspace = Get-PowerBIWorkspace -Filter "name eq '<workspace-name>'"
|
||||
|
||||
# Add the security group to the workspace
|
||||
Add-PowerBIWorkspaceUser -Id $($pbiWorkspace.Id) -AccessRight Member -PrincipalType Group -Identifier $($SGObjectID)
|
||||
```
|
||||
|
||||
### 2. Security Monitoring
|
||||
```powershell
|
||||
# Monitor Power BI access patterns
|
||||
$workspaces = Get-PowerBIWorkspace
|
||||
foreach ($workspace in $workspaces) {
|
||||
$users = Get-PowerBIWorkspaceUser -Id $workspace.Id
|
||||
Write-Host "Workspace: $($workspace.Name)"
|
||||
foreach ($user in $users) {
|
||||
Write-Host " User: $($user.UserPrincipalName) - Access: $($user.AccessRight)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Compliance Reporting
|
||||
```dax
|
||||
// Compliance dashboard measures
|
||||
Users with Data Access =
|
||||
CALCULATE(
|
||||
DISTINCTCOUNT(AuditLog[Username]),
|
||||
AuditLog[AccessType] = "DataAccess",
|
||||
AuditLog[Date] >= TODAY() - 30
|
||||
)
|
||||
|
||||
High Privilege Users =
|
||||
CALCULATE(
|
||||
DISTINCTCOUNT(UserRoles[Email]),
|
||||
UserRoles[Role] IN {"Admin", "Manager", "Executive"}
|
||||
)
|
||||
|
||||
Security Violations =
|
||||
CALCULATE(
|
||||
COUNTROWS(AuditLog),
|
||||
AuditLog[EventType] = "SecurityViolation",
|
||||
AuditLog[Date] >= TODAY() - 7
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices and Anti-Patterns
|
||||
|
||||
### ✅ Security Best Practices
|
||||
|
||||
#### 1. Principle of Least Privilege
|
||||
```dax
|
||||
// Always default to restrictive access
|
||||
Default Security =
|
||||
VAR UserPermissions =
|
||||
FILTER(
|
||||
UserAccess,
|
||||
UserAccess[Email] = USERNAME()
|
||||
)
|
||||
RETURN
|
||||
IF(
|
||||
COUNTROWS(UserPermissions) > 0,
|
||||
[Territory] IN SELECTCOLUMNS(UserPermissions, "Territory", UserAccess[Territory]),
|
||||
FALSE() // No access if not explicitly granted
|
||||
)
|
||||
```
|
||||
|
||||
#### 2. Explicit Role Validation
|
||||
```dax
|
||||
// Validate expected roles explicitly
|
||||
Role-Based Filter =
|
||||
VAR UserRole = LOOKUPVALUE(UserRoles[Role], UserRoles[Email], USERNAME())
|
||||
VAR AllowedRoles = {"Analyst", "Manager", "Executive"}
|
||||
RETURN
|
||||
IF(
|
||||
UserRole IN AllowedRoles,
|
||||
SWITCH(
|
||||
UserRole,
|
||||
"Analyst", [Department] = LOOKUPVALUE(UserDepartments[Department], UserDepartments[Email], USERNAME()),
|
||||
"Manager", [Region] = LOOKUPVALUE(UserRegions[Region], UserRegions[Email], USERNAME()),
|
||||
"Executive", TRUE()
|
||||
),
|
||||
FALSE() // Deny access for unexpected roles
|
||||
)
|
||||
```
|
||||
|
||||
### ❌ Security Anti-Patterns to Avoid
|
||||
|
||||
#### 1. Overly Permissive Defaults
|
||||
```dax
|
||||
// ❌ AVOID: This grants full access to unexpected users
|
||||
Bad Security Filter =
|
||||
IF(
|
||||
USERNAME() = "SpecificUser",
|
||||
[Type] = "Internal",
|
||||
TRUE() // Dangerous default
|
||||
)
|
||||
```
|
||||
|
||||
#### 2. Complex Security Logic
|
||||
```dax
|
||||
// ❌ AVOID: Overly complex security that's hard to audit
|
||||
Overly Complex Security =
|
||||
IF(
|
||||
OR(
|
||||
AND(USERNAME() = "User1", WEEKDAY(TODAY()) <= 5),
|
||||
AND(USERNAME() = "User2", HOUR(NOW()) >= 9, HOUR(NOW()) <= 17),
|
||||
AND(CONTAINS(VALUES(SpecialUsers[Email]), SpecialUsers[Email], USERNAME()), [Priority] = "High")
|
||||
),
|
||||
[Type] IN {"Internal", "Confidential"},
|
||||
[Type] = "Public"
|
||||
)
|
||||
```
|
||||
|
||||
## Security Integration Patterns
|
||||
|
||||
### 1. Azure AD Integration
|
||||
```csharp
|
||||
// Generate token with Azure AD user context
|
||||
var tokenRequest = new GenerateTokenRequestV2(
|
||||
reports: new List<GenerateTokenRequestV2Report>() { new GenerateTokenRequestV2Report(reportId) },
|
||||
datasets: datasetIds.Select(datasetId => new GenerateTokenRequestV2Dataset(datasetId.ToString())).ToList(),
|
||||
targetWorkspaces: targetWorkspaceId != Guid.Empty ? new List<GenerateTokenRequestV2TargetWorkspace>() { new GenerateTokenRequestV2TargetWorkspace(targetWorkspaceId) } : null,
|
||||
identities: new List<EffectiveIdentity> { rlsIdentity }
|
||||
);
|
||||
|
||||
var embedToken = pbiClient.EmbedToken.GenerateToken(tokenRequest);
|
||||
```
|
||||
|
||||
### 2. Service Principal Authentication
|
||||
```csharp
|
||||
// Service principal with RLS for embedded scenarios
|
||||
public EmbedToken GetEmbedToken(Guid reportId, IList<Guid> datasetIds, [Optional] Guid targetWorkspaceId)
|
||||
{
|
||||
PowerBIClient pbiClient = this.GetPowerBIClient();
|
||||
|
||||
var rlsidentity = new EffectiveIdentity(
|
||||
username: "username@contoso.com",
|
||||
roles: new List<string>{ "MyRole" },
|
||||
datasets: new List<string>{ datasetId.ToString()}
|
||||
);
|
||||
|
||||
var tokenRequest = new GenerateTokenRequestV2(
|
||||
reports: new List<GenerateTokenRequestV2Report>() { new GenerateTokenRequestV2Report(reportId) },
|
||||
datasets: datasetIds.Select(datasetId => new GenerateTokenRequestV2Dataset(datasetId.ToString())).ToList(),
|
||||
targetWorkspaces: targetWorkspaceId != Guid.Empty ? new List<GenerateTokenRequestV2TargetWorkspace>() { new GenerateTokenRequestV2TargetWorkspace(targetWorkspaceId) } : null,
|
||||
identities: new List<EffectiveIdentity> { rlsIdentity }
|
||||
);
|
||||
|
||||
var embedToken = pbiClient.EmbedToken.GenerateToken(tokenRequest);
|
||||
|
||||
return embedToken;
|
||||
}
|
||||
```
|
||||
|
||||
## Security Monitoring and Auditing
|
||||
|
||||
### 1. Access Pattern Analysis
|
||||
```dax
|
||||
// Identify unusual access patterns
|
||||
Unusual Access Pattern =
|
||||
VAR UserAccessCount =
|
||||
CALCULATE(
|
||||
COUNTROWS(AccessLog),
|
||||
AccessLog[Date] >= TODAY() - 7
|
||||
)
|
||||
VAR AvgUserAccess =
|
||||
CALCULATE(
|
||||
AVERAGE(AccessLog[AccessCount]),
|
||||
ALL(AccessLog[Username]),
|
||||
AccessLog[Date] >= TODAY() - 30
|
||||
)
|
||||
RETURN
|
||||
IF(
|
||||
UserAccessCount > AvgUserAccess * 3,
|
||||
"⚠️ High Activity",
|
||||
"Normal"
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Data Breach Detection
|
||||
```dax
|
||||
// Detect potential data exposure
|
||||
Potential Data Exposure =
|
||||
VAR UnexpectedAccess =
|
||||
CALCULATE(
|
||||
COUNTROWS(AccessLog),
|
||||
AccessLog[AccessResult] = "Denied",
|
||||
AccessLog[Date] >= TODAY() - 1
|
||||
)
|
||||
RETURN
|
||||
IF(
|
||||
UnexpectedAccess > 10,
|
||||
"🚨 Multiple Access Denials - Review Required",
|
||||
"Normal"
|
||||
)
|
||||
```
|
||||
|
||||
Remember: Security is layered - implement defense in depth with proper authentication, authorization, data encryption, network security, and comprehensive auditing. Regularly review and test security implementations to ensure they meet current requirements and compliance standards.
|
||||
Reference in New Issue
Block a user