├── Tabular Editor 2 ├── README.md └── Power BI Model Refresh Cancel.csx ├── Basic ├── Enable Single Value Slider (convert to what-if parameter).csx ├── Exclude Selected Tables From Model Refresh.csx ├── Format Column To Short Date.csx ├── DAX Expression to Description.csx ├── Hide Columns Ending with 'Key' in Selected Tables.csx ├── Move All Columns to a DisplayFolder.csx ├── Select Columns Remove Auto Aggregations.csx ├── Select Measures and apply Whole Number Format.csx ├── Add Prefix to Selected Measure Names.csx ├── Replace String Across All Power Queries.csx ├── Nuke All Implicit Measures.csx ├── Replace String In All Measure Descriptions.csx ├── Replace Dataset Source Dataflow IDs.csx ├── Hide columns on the many side of a relationship.csx ├── Import Best Practice Analyer Rules.csx ├── Create Sum Measures for All Selected Columns.csx ├── Replace String In All Table Names.csx ├── Create Selected Measure.csx ├── Create Drill Through Help Text.csx ├── Format All Measures.csx ├── Autogenerate ROWS measures.csx ├── Delete Relationships by Column Name.csx ├── Create countrows measures.csx ├── Change String in Selected Measures Names.csx ├── Export All Partition Expressions.csx ├── Autogenerate SUM Measures.csx ├── Format DAX Calculated Columns.csx ├── Add or Update DAX Epression to Description.csx ├── Default Measure Description.csx ├── Create Sum Measure for Selected Cols with Folder.csx ├── Wrap Measure in HASONEVALUE Check.csx ├── Extended Properties Preview │ ├── Preview Distinct Values of Selected Columns.csx │ ├── ep_Peak Sel Column by Global Measure.csx │ ├── ep_Create Dynamic Rolling Avg Measures.csx │ └── ep_Model Extended Properties Generator.csx ├── Create Build direct or indirect dependency tree of selected or all measures.csx ├── DAX Comment to Measure Description.csx ├── Preview top50 values of selected columns by number of rows.csx ├── Generate Last Updated Table.csx └── Format All Power Query Tables.csx ├── Advanced ├── One-Click Macros │ ├── New Calculate Measure.csx │ ├── Filtered Measures by column values.csx │ ├── Create Field Parameter.csx │ ├── Referential Integrity Check Measures.csx │ ├── Color Measures.csx │ ├── Column Width Calc Group.csx │ ├── Time Int Labels.csx │ ├── Measure in Calc Item.csx │ ├── Dynamic Measure Calculation Group Script.csx │ ├── Data Problems Button.csx │ ├── Alert Summary Measures.csx │ ├── Create measures with a calculation group.csx │ └── Time Intelligence Calculation Group Creation.csx └── TopN_PlusOthers.csx ├── LICENSE ├── Intermediate ├── Replace Text Strings that appear in any Measure Expression.csx ├── Clean Up Columns Measures.csx ├── Create Measure for Each Item in Selected Columns.csx ├── Search and Replace Matched Pairs of Strings in Selected Measures.csx ├── Create Time Intelligence Measures Using Calculation Groups.csx ├── Create Perspective From Fact Tables.csx ├── Dynamic measure selector.csx ├── Clean object names.csx ├── Create Visuals Translations Measures.csx ├── Create Parent-Child Hierarchy.csx ├── New Measure Choice Selector.csx ├── Dynamic YtD same period.csx └── Create Explicit Measures.csx └── README.md /Tabular Editor 2/README.md: -------------------------------------------------------------------------------- 1 | # Tabular Editor 2 2 | Created for Tabular Editor 2 scripts -------------------------------------------------------------------------------- /Basic/Enable Single Value Slider (convert to what-if parameter).csx: -------------------------------------------------------------------------------- 1 | // 2021-11-18 / B.Agullo / 2 | // by Bernat Agulló 3 | // twitter: @AgulloBernat 4 | // www.esbrina-ba.com/blog 5 | // 6 | 7 | Selected.Column.SetExtendedProperty("ParameterMetadata", "{\"version\":0}", ExtendedPropertyType.Json); -------------------------------------------------------------------------------- /Basic/Exclude Selected Tables From Model Refresh.csx: -------------------------------------------------------------------------------- 1 | // Title: ExcludeSelectedTablesFromModelRefresh.csx 2 | // Author: @JamesDBartlett3@techhub.social 3 | // Description: Exclude selected tables from model refresh 4 | 5 | foreach(var table in Selected.Tables) { 6 | table.ExcludeFromModelRefresh = true; 7 | } -------------------------------------------------------------------------------- /Basic/Format Column To Short Date.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Format All Columns to Short Date 3 | * 4 | * Author: Tommy Puglia, https://powerbi.tips/explicit-measures-power-bi-podcast/ 5 | * 6 | * This script simply formats selected columns in a table to the Short Date format. 7 | * 8 | */ 9 | 10 | 11 | foreach(var c in Selected.Columns) { 12 | c.FormatString = "Short Date"; 13 | } 14 | -------------------------------------------------------------------------------- /Basic/DAX Expression to Description.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Copy DAX Expression into the measure's description field. 3 | * 4 | * Author: Reid Havens, https://www.havensconsulting.net/ 5 | * 6 | * This script, when executed, will loop through all the measures in the model and 7 | * copy the DAX epression into the field's description for documentation purposes. 8 | */ 9 | 10 | foreach (var m in Model.AllMeasures) { 11 | m.Description = m.Expression; 12 | } 13 | -------------------------------------------------------------------------------- /Basic/Hide Columns Ending with 'Key' in Selected Tables.csx: -------------------------------------------------------------------------------- 1 | // Hide all columns ending with "Key" in selected tables from report 2 | 3 | var keySuffix = "Key"; 4 | 5 | // Loop through all currently selected tables: 6 | foreach(var t in Selected.Tables) 7 | { 8 | // Loop through all columns ending with "Key" on the current table: 9 | foreach(var k in t.Columns.Where(c => c.Name.EndsWith(keySuffix))) 10 | { 11 | k.IsHidden = true; 12 | } 13 | } -------------------------------------------------------------------------------- /Basic/Move All Columns to a DisplayFolder.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Move All Columns to a DisplayFolder 3 | * 4 | * Author: Matt Allington, https://exceleratorbi.com.au 5 | * 6 | * move all columns into a display folder. 7 | * read why at https://exceleratorbi.com.au/column-sub-folders-better-than-measure-sub-folders/ 8 | */ 9 | 10 | 11 | //Move all columns to display folder 12 | foreach (var c in Model.AllColumns) 13 | { 14 | c.DisplayFolder = "_Columns"; 15 | } 16 | -------------------------------------------------------------------------------- /Basic/Select Columns Remove Auto Aggregations.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Set Summarize by to None on Selected Columns 3 | * 4 | * Author: Mike Carlo, https://PowerBI.tips 5 | * 6 | * User can select columns. From the selection each column will be set to Aggregate = None or Summarize by = None, as observed in Power BI Desktop. 7 | * Only Selected columns will be modified. 8 | * 9 | */ 10 | 11 | 12 | 13 | foreach(var column in Selected.columns) { 14 | 15 | column.SummarizeBy = AggregateFunction.None; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Basic/Select Measures and apply Whole Number Format.csx: -------------------------------------------------------------------------------- 1 | /* For each selected measure change the formatting to a whole number 2 | * 3 | * Author: Mike Carlo, https://powerbi.tips 4 | * 5 | * Select measures using Control + Left Click 6 | * Script change the measure formatting to a whole number 7 | * 8 | */ 9 | 10 | // for each selected measure specifies a format. 11 | foreach (var m in Selected.Measures) 12 | { 13 | 14 | // Set the format string on the new measure: 15 | m.FormatString = "#,##0"; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Basic/Add Prefix to Selected Measure Names.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Add Prefix to Selected Measures 3 | * 4 | * Author: Mike Carlo, https://PowerBI.tips 5 | * 6 | * This script loops through all the selected measures and adds a Prefix String to the name. 7 | * Only Selected measures will be modified. 8 | * 9 | */ 10 | 11 | /* Cycle over all Selected measures in model */ 12 | foreach (var m in Selected.Measures) 13 | { 14 | /* Grab the current name as a variable, prepend some text */ 15 | m.Name = "Test " + m.Name; 16 | } -------------------------------------------------------------------------------- /Basic/Replace String Across All Power Queries.csx: -------------------------------------------------------------------------------- 1 | // Title: ReplaceStringAcrossAllPowerQueries.csx 2 | // Author: @JamesDBartlett3@techhub.social 3 | // Description: Replaces a string in PowerQuery on all partitions in the model 4 | 5 | var oldPQString = ""; 6 | var newPQString = ""; 7 | 8 | // Loop through all partitions on the model, replacing oldPQString with newPQString 9 | foreach(var p in Model.AllPartitions.OfType()) 10 | { 11 | p.Expression = p.Expression 12 | .Replace(oldPQString, newPQString); 13 | } -------------------------------------------------------------------------------- /Basic/Nuke All Implicit Measures.csx: -------------------------------------------------------------------------------- 1 | // Title: Nuke All Implicit Measures.csx 2 | // Author: @JamesDBartlett3@techhub.social 3 | // Description: Uses two methods to disable all Implicit Measures in the model 4 | 5 | // 1. Set "Discourage Implicit Measures" option in Model = true 6 | Model.DiscourageImplicitMeasures = true; 7 | 8 | // 2. Set "Summarize By" property on all columns = AggregateFunction.None 9 | foreach(var column in Model.Tables.SelectMany(t => t.Columns)) { 10 | column.SummarizeBy = AggregateFunction.None; 11 | } 12 | -------------------------------------------------------------------------------- /Basic/Replace String In All Measure Descriptions.csx: -------------------------------------------------------------------------------- 1 | // Title: Replace String In All Model Descriptions.csx 2 | // Author: Dan Meissner 3 | // Description: Replaces text in all descriptions for any measure in the model, replacing one string with another string. 4 | 5 | var oldTableNameString = "OldString"; 6 | var newTableNameString = "NewString"; 7 | 8 | // Loop through all tables in the model, replacing oldTableNameString with newTableNameString 9 | foreach(var m in Model.AllMeasures) 10 | { 11 | m.Description = m.Description.Replace(oldTableNameString, newTableNameString); 12 | } -------------------------------------------------------------------------------- /Basic/Replace Dataset Source Dataflow IDs.csx: -------------------------------------------------------------------------------- 1 | // Title: ReplaceDatasetSourceDataflowIDs.csx 2 | // Author: @JamesDBartlett3@techhub.social 3 | // Description: Replace the source DataflowID & WorkspaceID on all partitions in the model 4 | 5 | var oldWorkspaceId = ""; 6 | var newWorkspaceId = ""; 7 | var oldDataflowId = ""; 8 | var newDataflowId = ""; 9 | 10 | // Loop through all partitions on the model, replacing the DataflowIDs & WorkspaceIDs 11 | foreach(var p in Model.AllPartitions.OfType()) 12 | { 13 | p.Expression = p.Expression 14 | .Replace(oldWorkspaceId, newWorkspaceId) 15 | .Replace(oldDataflowId, newDataflowId); 16 | } -------------------------------------------------------------------------------- /Basic/Hide columns on the many side of a relationship.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Hide columns on the many side of a relationship 3 | * 4 | * Author: Matt Allington, https://exceleratorbi.com.au 5 | * 6 | * it is dangerous to use columns on the many side of a relationship as it can 7 | * produce unexpected results, so it is a best practice to hide these columns 8 | * to discourage their use in reports. 9 | */ 10 | 11 | // Hide all columns on many side of a join 12 | 13 | foreach (var r in Model.Relationships) 14 | { // hide all columns on the many side of a join 15 | var c = r.FromColumn.Name; 16 | var t = r.FromTable.Name; 17 | Model.Tables[t].Columns[c].IsHidden = true; 18 | } 19 | -------------------------------------------------------------------------------- /Advanced/One-Click Macros/New Calculate Measure.csx: -------------------------------------------------------------------------------- 1 | #r "Microsoft.VisualBasic" 2 | using Microsoft.VisualBasic; 3 | 4 | // this sctipt creates a CALCULATE expression based on an existing measure 5 | // 2021-10-11 B.Agullo @AgulloBernat 6 | 7 | if(Selected.Measures.Count != 1) { 8 | Error("Select one and only one measure"); 9 | return; 10 | } 11 | 12 | var selectedMeasure = Selected.Measure; 13 | var parentTable = selectedMeasure.Table; 14 | 15 | string newMeasureName = Interaction.InputBox("New Measure name", "Name", selectedMeasure.Name + " modified", 740, 400); 16 | string newMeasureExpression = "CALCULATE([" + selectedMeasure.Name + "])"; 17 | 18 | parentTable.AddMeasure(newMeasureName,newMeasureExpression); 19 | -------------------------------------------------------------------------------- /Basic/Import Best Practice Analyer Rules.csx: -------------------------------------------------------------------------------- 1 | /* 2 | Best Practice Analyzer enables options in Tools to check best practices in models and apply fixes 3 | to items in the model which may hinder performance. Past code in advanced editor and run, 4 | then close tabular editor, re-open and click Tools at the top. 5 | */ 6 | 7 | 8 | 9 | System.Net.WebClient w = new System.Net.WebClient(); 10 | 11 | string path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData); 12 | string url = "https://raw.githubusercontent.com/microsoft/Analysis-Services/master/BestPracticeRules/BPARules.json"; 13 | string downloadLoc = path+@"\TabularEditor\BPARules.json"; 14 | w.DownloadFile(url, downloadLoc); -------------------------------------------------------------------------------- /Basic/Create Sum Measures for All Selected Columns.csx: -------------------------------------------------------------------------------- 1 | // Creates a SUM measure for every currently selected column and hide the column. 2 | foreach(var c in Selected.Columns) 3 | { 4 | var newMeasure = c.Table.AddMeasure( 5 | "Sum of " + c.Name, // Name 6 | "SUM(" + c.DaxObjectFullName + ")", // DAX expression 7 | c.DisplayFolder // Display Folder 8 | ); 9 | 10 | // Set the format string on the new measure: 11 | newMeasure.FormatString = "0.00"; 12 | 13 | // Provide some documentation: 14 | newMeasure.Description = "This measure is the sum of column " + c.DaxObjectFullName; 15 | 16 | // Hide the base column: 17 | c.IsHidden = true; 18 | } 19 | -------------------------------------------------------------------------------- /Basic/Replace String In All Table Names.csx: -------------------------------------------------------------------------------- 1 | // Title: Replace String In All Table Names.csx 2 | // Author: @JamesDBartlett3@techhub.social 3 | // Description: Renames all tables in the model, replacing one string with another string 4 | 5 | var oldTableNameString = ""; 6 | var newTableNameString = ""; 7 | 8 | // Loop through all tables in the model, replacing oldTableNameString with newTableNameString 9 | foreach(var t in Model.Tables) 10 | { 11 | t.Name = t.Name.Replace(oldTableNameString, newTableNameString); 12 | // Loop through all partitions in the table, replacing oldTableNameString with newTableNameString 13 | foreach(var p in t.Partitions) 14 | { 15 | p.Name = p.Name.Replace(oldTableNameString, newTableNameString); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Basic/Create Selected Measure.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Create a Selected Value for a Drill Through Page 3 | * 4 | * Author: Tommy Puglia, https://powerbi.tips/explicit-measures-power-bi-podcast/ 5 | * 6 | * This script will loop through selected columns to create a text measure that shows what was selected for a Drill Through Page 7 | * 8 | */ 9 | foreach(var c in Selected.Columns) 10 | { 11 | var newMeasure = c.Table.AddMeasure( 12 | 13 | // Create name of measure using a prefix 14 | "Sel. " + c.Name, 15 | 16 | // Create full measure 17 | "IF(ISFILTERED(" + c.DaxObjectFullName + "), SELECTEDVALUE(" + c.DaxObjectFullName + ", \" Multiple Selected \" ), \"None Selected \" )" 18 | ); 19 | 20 | // Display Folder 21 | newMeasure.DisplayFolder = "_Sel"; 22 | } -------------------------------------------------------------------------------- /Basic/Create Drill Through Help Text.csx: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Title: Create a Drill Through Help Text for a Button 4 | * 5 | * Author: Tommy Puglia, https://powerbi.tips/explicit-measures-power-bi-podcast/ 6 | * 7 | * This script will loop through selected columns to create the text for a button to drill through 8 | * 9 | */ 10 | 11 | foreach(var c in Selected.Columns) 12 | { 13 | var newMeasure = c.Table.AddMeasure( 14 | 15 | // Appends a prefix to the name of the column 16 | "dr_text_" + c.Name , 17 | 18 | // Generate full measure 19 | "IF(HASONEVALUE(" + c.DaxObjectFullName + "), \"See details for \" & SELECTEDVALUE(" + c.DaxObjectFullName + "), \"Click a Company to See Details\" )" 20 | 21 | // Display Folder; 22 | newMeasure.DisplayFolder = "_Sel"; 23 | } 24 | -------------------------------------------------------------------------------- /Basic/Format All Measures.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Format All Measures 3 | * 4 | * Author: Matt Allington, https://exceleratorbi.com.au 5 | * 6 | * This script loops through all the measures in your model and calls out to daxformatter.com 7 | * in order to format them. 8 | * 9 | * CAUTION: If your model has many measures (> 100) please use with care, as this will perform 10 | * many requests against the www.daxformatter.com web service. It will take some time to run, 11 | * and also, we don't want to DDoS attack daxformatter.com :-) 12 | */ 13 | 14 | //Format All Measures 15 | foreach (var m in Model.AllMeasures) 16 | { 17 | m.Expression = FormatDax(m.Expression); 18 | /* Cycle over all measures in model and format 19 | them all using DAX Formatter */ 20 | } 21 | -------------------------------------------------------------------------------- /Basic/Autogenerate ROWS measures.csx: -------------------------------------------------------------------------------- 1 | //'2022-07-10 / B.Agullo / 2 | // creates "Rows of ... " measures to return the numer of visible rows in each selected table 3 | // adapted from Daniel Otykier's code 4 | 5 | // Loop through all currently selected columns: 6 | foreach(Table t in Selected.Tables) 7 | { 8 | Measure newMeasure = t.AddMeasure( 9 | "Rows of " + t.Name, // Name 10 | "COUNTROWS(" + t.DaxObjectFullName + ")", // DAX expression 11 | "Other Measures" // Display Folder 12 | ); 13 | 14 | // Set the format string on the new measure: 15 | newMeasure.FormatString = "#,##0"; 16 | 17 | // Provide some documentation: 18 | newMeasure.Description = "This measure is number of rows of " + t.Name; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Basic/Delete Relationships by Column Name.csx: -------------------------------------------------------------------------------- 1 | // Title: Delete Relationships by Column Name.csx 2 | // Author: @JamesDBartlett3 3 | // Description: Deletes all relationships where the column name 4 | // on at least one side of the relationship matches the declared 5 | // ColumnName variable. Automatically accounts for spaces. 6 | 7 | var ColumnName = ""; 8 | 9 | List relationshipsToDelete = new List(); 10 | 11 | foreach(var r in Model.Relationships) { 12 | string[] c = { 13 | r.FromColumn.Name.ToString().Replace(" ",""), 14 | r.ToColumn.Name.ToString().Replace(" ",""), 15 | r.FromColumn.Name.ToString(), 16 | r.ToColumn.Name.ToString() 17 | }; 18 | if(c.Contains(ColumnName.Replace(" ",""))) { 19 | relationshipsToDelete.Add(r); 20 | } 21 | } 22 | 23 | foreach(var r in relationshipsToDelete) { 24 | r.Delete(); 25 | } 26 | -------------------------------------------------------------------------------- /Basic/Create countrows measures.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Auto-generate COUNTROWS measures from tables 3 | * 4 | * Author: Edgar Walther, twitter.com/edgarwalther 5 | * 6 | * This script, when executed, will loop through the currently selected tables, 7 | * creating one COUNTROWS measure for each table. 8 | */ 9 | 10 | // Loop through all currently selected tables: 11 | foreach(var table in Selected.Tables) { 12 | 13 | var newMeasure = table.AddMeasure( 14 | "# Rows in " + table.Name, // Name 15 | "COUNTROWS(" + table.DaxObjectFullName + ")" // DAX expression 16 | ); 17 | 18 | // Set the format string on the new measure: 19 | newMeasure.FormatString = "0"; 20 | 21 | // Provide some documentation: 22 | newMeasure.Description = "This measure is the number of rows in table " + table.DaxObjectFullName; 23 | 24 | } -------------------------------------------------------------------------------- /Basic/Change String in Selected Measures Names.csx: -------------------------------------------------------------------------------- 1 | // 2 | // Title: Replace Text Strings in name of the selected measures 3 | // 4 | // Author: Artur Nawrocki 5 | // 6 | // This script, when executed, will loop through the currently selected measures 7 | // and replace the FromString with the ToString. 8 | // 9 | 10 | // Replace Text Strings that appear in name of selected measures 11 | 12 | // String of text that is desired to be found 13 | // Update the value of the text for your desired usecase 14 | // This replacement example shows the replacement of a DAX expression and replaces the Target with a text of Forecast 15 | 16 | var FromString = "Target"; 17 | 18 | // String of text that is the replaced value 19 | // Update the value of the text for your desired usecase 20 | var ToString = "Forecast"; 21 | 22 | foreach (var m in Selected.Measures) 23 | { 24 | m.Name = m.Name.Replace(FromString,ToString); 25 | } 26 | -------------------------------------------------------------------------------- /Basic/Export All Partition Expressions.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Export All Partition Expressions 3 | * 4 | * Author: Dan Meissner 5 | * Last Modified: 2023JAN09 6 | * 7 | * This script exports the partition expressions for all table partitions to the screen or a tab separated file of your choice. 8 | * The output has one row per table partition. 9 | * 10 | * Change the filename and filepath as necessary below. 11 | * 12 | * Thanks to Kurt Buhler (Data Goblins) for most of the work and the inspiration. 13 | */ 14 | 15 | /* Assign variables */ 16 | var filename = "TableExpressionOutput.tsv"; 17 | var filepath = @"C:\Sandbox\"; 18 | var tsv = " "; 19 | 20 | // Export Properties 21 | tsv = tsv + ExportProperties(Model.AllPartitions, "Expression"); 22 | 23 | // Output the results to a dialog box pop up on the screen (can copy to clipboard from the dialog box) 24 | tsv.Output(); 25 | 26 | // Output the results to a tab separated file at filepath/filename 27 | //SaveFile( filepath + filename, tsv); -------------------------------------------------------------------------------- /Basic/Autogenerate SUM Measures.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Auto-generate SUM measures from columns 3 | * 4 | * Author: Daniel Otykier, twitter.com/DOtykier 5 | * 6 | * This script, when executed, will loop through the currently selected columns, 7 | * creating one SUM measure for each column and also hiding the column itself. 8 | */ 9 | 10 | // Loop through all currently selected columns: 11 | foreach(var c in Selected.Columns) 12 | { 13 | var newMeasure = c.Table.AddMeasure( 14 | "Sum of " + c.Name, // Name 15 | "SUM(" + c.DaxObjectFullName + ")", // DAX expression 16 | c.DisplayFolder // Display Folder 17 | ); 18 | 19 | // Set the format string on the new measure: 20 | newMeasure.FormatString = "0.00"; 21 | 22 | // Provide some documentation: 23 | newMeasure.Description = "This measure is the sum of column " + c.DaxObjectFullName; 24 | 25 | // Hide the base column: 26 | c.IsHidden = true; 27 | } 28 | -------------------------------------------------------------------------------- /Basic/Format DAX Calculated Columns.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Formats all the calculated columns 3 | * 4 | * Author: Ricardo Rincón, twitter.com/nexus150 , www.bitodata.com 5 | * 6 | * This script, when executed, formats all the calculated columns of the model 7 | * using the SQLBI.COM service https://www.daxformatter.com/. 8 | * please use the version with Selected.Columns as long as you can 9 | * to minimize the load on the service so that we can all enjoy it. 10 | */ 11 | 12 | // Format (Only Selected) Calculated Columns (if you don't need to format ALL calculated columns, please use this version) 13 | foreach(var m in Selected.Columns) 14 | { 15 | if(m.Type.ToString() == "Calculated"){ 16 | var y = m as CalculatedColumn; 17 | y.FormatDax(); 18 | } 19 | } 20 | 21 | // Format All Calculated Columns 22 | //foreach(var m in Model.AllColumns) 23 | //{ 24 | // if(m.Type.ToString() == "Calculated"){ 25 | // var y = m as CalculatedColumn; 26 | // y.FormatDax(); 27 | // } 28 | //} -------------------------------------------------------------------------------- /Basic/Add or Update DAX Epression to Description.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Update/Replace the DAX Expression in a Measure's Description 3 | * 4 | * Author: Dan Meissner 5 | * 6 | * This script, when executed, will loop through all the measures in the model and 7 | * Either (1) add the DAX expression into the field's description for documentation purposes if it is missing 8 | * or (2) remove the existing description DAX expression and updated it with the current DAX expression. 9 | * 10 | * This script assumes the DAX expression is the last text in the description and is preceded with a single line of text "Expression:" 11 | */ 12 | 13 | foreach (var m in Selected.Measures) 14 | { 15 | int index = m.Description.IndexOf("Expression:"); 16 | if (index >= 0) 17 | { 18 | m.Description = m.Description.Substring(0, index+11) + System.Environment.NewLine + m.Expression; 19 | } 20 | else 21 | { 22 | m.Description = m.Description + System.Environment.NewLine + System.Environment.NewLine + "Expression:" + System.Environment.NewLine + m.Expression; 23 | }; 24 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 PowerBI-tips 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Basic/Default Measure Description.csx: -------------------------------------------------------------------------------- 1 | //Name: Default Measure Description 2 | //Tooltip: Adds expression to the description 3 | //Enable: Model 4 | //Created by Mihaly Kavasi (updated by Ed Hansberry's idea) 5 | //Tabular Editor version 2.16.0 6 | 7 | //It is better to format DAX before adding it into the descriptions 8 | foreach(var m in Model.AllMeasures) 9 | { 10 | if(m.Description == "") 11 | { 12 | m.Description = "Expression:" + "\n" + m.Expression; 13 | } 14 | else if (!m.Description.Contains("Expression")) 15 | { 16 | m.Description = m.Description + "\n" + "Expression:" + "\n" + m.Expression; 17 | } 18 | else 19 | { 20 | // '2021-07-05 / B.Agullo / reset expressions already added 21 | int pos = m.Description.IndexOf("Expression",0); 22 | bool onlyExpression = (pos == 0); 23 | 24 | if (onlyExpression) { 25 | m.Description = "Expression:" + "\n" + m.Expression; 26 | } else { 27 | m.Description = m.Description.Substring(0,pos-1) + "\n" + "Expression:" + "\n" + m.Expression; 28 | } 29 | } 30 | } 31 | 32 | Model.AllMeasures.FormatDax(); -------------------------------------------------------------------------------- /Basic/Create Sum Measure for Selected Cols with Folder.csx: -------------------------------------------------------------------------------- 1 | /* Creates a SUM measure for every currently selected column(s) 2 | * 3 | * Author: Mike Carlo, https://powerbi.tips 4 | * 5 | * Select Columns using Control + Left Click 6 | * Script will create one measure for each column selected 7 | * Then Change the formatting of the column, adds a description, 8 | * and places measures in a named folder. 9 | * 10 | */ 11 | 12 | // Loop through the list of selected columns 13 | foreach(var c in Selected.Columns) 14 | { 15 | var newMeasure = c.Table.AddMeasure( 16 | "Sum of " + c.Name, // Name 17 | "SUM(" + c.DaxObjectFullName + ")", // DAX expression 18 | c.DisplayFolder // Display Folder 19 | ); 20 | 21 | // Set the format string on the new measure: 22 | newMeasure.FormatString = "0"; 23 | 24 | // Provide some documentation: 25 | newMeasure.Description = "This measure is the sum of column " + c.DaxObjectFullName; 26 | 27 | // Create all measures within a Named Folder 28 | newMeasure.DisplayFolder = "_Model"; 29 | 30 | // Hide the base column: 31 | c.IsHidden = true; 32 | } 33 | -------------------------------------------------------------------------------- /Intermediate/Replace Text Strings that appear in any Measure Expression.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Replace Text Strings that appear in many measures 3 | * 4 | * Author: Matt Allington http://xbi.com.au 5 | * 6 | * This script, when executed, will loop through the currently selected measures 7 | * and replace the FromString with the ToString. 8 | */ 9 | 10 | // Replace Text Strings that appear in many measures 11 | 12 | // String of text that is desired to be found 13 | // Update the value of the text for your desired usecase 14 | // This replacement example shows the replacement of a DAX expression and replaces the SUM(table[ColumnName]) with a well defined measure of [Total Sales] 15 | // It is best practice to use a single measure for Sum of Extended Amount and reference this measure in other DAX formulas. 16 | var FromString = "CALCULATE(SUM(Sales[ExtendedAmount])"; 17 | 18 | // String of text that is the replaced value 19 | // Update the value of the text for your desired usecase 20 | var ToString = "CALCULATE([Total Sales])"; 21 | 22 | foreach (var m in Model.AllMeasures) 23 | { 24 | m.Expression = m.Expression.Replace(FromString,ToString); 25 | } 26 | -------------------------------------------------------------------------------- /Basic/Wrap Measure in HASONEVALUE Check.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Add "HASONEVALUE" check to Selected Measures 3 | * 4 | * Author: Dan Meissner 5 | * Last Modified: 2022DEC20 6 | * 7 | * This script loops through all the selected measures and adds an IF statement check to see if it "HASONEVALUE" 8 | * relative to the single selected column. 9 | 10 | * Only selected measures will be modified. 11 | * Must select only one column in addition to any number of measures for which you want to check for one value. 12 | * 13 | */ 14 | 15 | /* Assign variables */ 16 | var measures = Selected.Measures; 17 | var columns = Selected.Columns; 18 | 19 | /* Check for at least one measure and only one column selected */ 20 | if (measures.Count == 0) 21 | { 22 | Error("Select one or more measures"); 23 | return; 24 | }; 25 | 26 | if (columns.Count != 1) 27 | { 28 | Error("Select only one column along with any measures"); 29 | return; 30 | }; 31 | 32 | /* Cycle over all Selected measures in model */ 33 | foreach (var c in columns) 34 | { 35 | foreach (var m in measures) 36 | { 37 | /* Wrap the current measure in a HASONEVALUE(column) check */ 38 | m.Expression = "IF ( HASONEVALUE( " + c.DaxObjectFullName + " ), " + m.Expression + " )"; 39 | }; 40 | } -------------------------------------------------------------------------------- /Basic/Extended Properties Preview/Preview Distinct Values of Selected Columns.csx: -------------------------------------------------------------------------------- 1 | // Author: Tommy Puglia 2 | // Website: https://pugliabi.com 3 | // Check out the Explicit Measures Podcast on Apple, spotify, & YouTube! 4 | // https://www.youtube.com/playlist?list=PLn1m_aBmgsbHr83c1P6uqaWF5PLdFzOjj 5 | // 6 | // 7 | // An incredibly simple script to easily show the distinct values based on selected columns. This is a great way to peak quickly over multiple columns when you are dealing with a new data model. 8 | // 9 | // 10 | // Pre-Req: 11 | // Save this Macro with the following: 12 | // Name: Preview\Distinct Values 13 | // Marco Context: Column 14 | // (Hint, you can organize your macros in TE3 by grouping based on a "\" separator!") 15 | // 16 | // Instructions: 17 | // 1. Select the columns you want to see the distinct values for 18 | // 2. Run the script (Right Click, on the selected columns, and choose the Preview --> Distinct Values macro) 19 | // 3. For every column chosen, a pop-up will appear 20 | 21 | 22 | foreach(var c in Selected.Columns) 23 | { 24 | string cName = c.Name; 25 | var cDAX = c.DaxObjectFullName; 26 | string ev = "Evaluate \n"; 27 | string newli = "\n"; 28 | string DistinctN = "Distinct("; 29 | var cResult = EvaluateDax(DistinctN + cDAX + ")"); 30 | cResult.Output(); 31 | } 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Intermediate/Clean Up Columns Measures.csx: -------------------------------------------------------------------------------- 1 | // 2 | // 3 | // by Tommy Puglia 4 | // twitter: @tommypuglia 5 | // pugliabi.com 6 | // 7 | // Cleans up Columns & Measures, mainly 8 | // Columns: 9 | // ---- Changes Any Date Column to a better FormatString 10 | // ---- Updates any Column that is number to not be summed 11 | // ---- Adds All Columns to a __columns Display Folder 12 | // 13 | // Measures: 14 | // ---- Updates Measures that have no format to Whole Number 15 | // ---- Adds Description to measures (DAX) 16 | // ---- Adds measures to a Display Folder (__KPI) 17 | // 18 | 19 | var sb = new System.Text.StringBuilder(); 20 | string newline = Environment.NewLine; 21 | 22 | foreach(var c in Model.AllColumns.Where(a => a.DataType == DataType.DateTime)) { 23 | c.FormatString = "m/d/yyyy"; 24 | 25 | 26 | } 27 | foreach(var c in Model.AllColumns.Where(a => a.DataType == DataType.Int64 || a.DataType == DataType.Decimal || a.DataType == DataType.Double)) { 28 | c.SummarizeBy = AggregateFunction.None; 29 | 30 | } 31 | foreach(var c in Model.AllColumns) { 32 | c.DisplayFolder = "_columns"; 33 | } 34 | 35 | 36 | 37 | foreach(var m in Model.AllMeasures) { 38 | string colFomratString = ""; 39 | if (m.FormatString == colFomratString) { 40 | m.FormatString = "#,0"; 41 | } 42 | if(string.IsNullOrEmpty(m.Description)) 43 | { 44 | m.Description =m.Expression; 45 | 46 | } 47 | if(m.DisplayFolder == "") 48 | { 49 | m.DisplayFolder = "__KPI"; 50 | 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Basic/Create Build direct or indirect dependency tree of selected or all measures.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Build direct or indirect dependency tree of selected or all measures 3 | * Author: Daniel Otykier, twitter.com/DOtykier 4 | * Co-Author: nexus150, https://github.com/nexus150 5 | * 6 | * This script, when executed, will loop through the currently selected measures (or all measures), 7 | * exporting all measures that depends on it, both direct or indirect way. 8 | * 9 | * This script was created in response to: 10 | * https://github.com/TabularEditor/TabularEditor/issues/897 11 | * 12 | */ 13 | 14 | string tsv = "Measure\tDependsOnMeasure"; // TSV file header row 15 | 16 | // Loop through all measures: (Change to Model.AllMeasures to Selected.Measures if only want Loop through selected measures) 17 | foreach(var m in Model.AllMeasures /**/) { 18 | 19 | // Get a list of all measures referenced directly by the current measure: 20 | var allReferences = m.DependsOn.Measures; 21 | 22 | // Get a list of ALL measures referenced by this measure (both directly and indirectly through other measures): 23 | //var allReferences = m.DependsOn.Deep().OfType().Distinct(); 24 | 25 | // Output TSV rows - one for each measure reference: 26 | foreach(var m2 in allReferences) 27 | tsv += string.Format("\r\n{0}\t{1}", m.Name, m2.Name); 28 | } 29 | 30 | tsv.Output(); 31 | // SaveFile("c:\\MyProjects\\SSAS\\MeasureDependencies.tsv", tsv); // Uncomment this line to save output to a file -------------------------------------------------------------------------------- /Intermediate/Create Measure for Each Item in Selected Columns.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Create Measure for Each Item in Selected Columns 3 | * 4 | * Author: Michael Mays, twitter.com/ItsAMaysin 5 | * 6 | * This script, when executed, will loop through the currently selected columns and 7 | * create a measure for each item in the column. In the below, the newly 8 | * created measure will be of the format 9 | * CALCULATE ( [BaseMeasure], Table[SelectedColumn] = "Item N in Selected Column") 10 | */ 11 | 12 | foreach (var c in Selected.Columns) 13 | { 14 | string MyColumn = c.DaxObjectFullName; 15 | string GetItems = "EVALUATE VALUES (" + MyColumn + ")"; // List of items from column 16 | 17 | using (var reader = Model.Database.ExecuteReader(GetItems)) 18 | { 19 | 20 | // Create a loop for every row in the resultset 21 | while(reader.Read()) 22 | { 23 | string MyItem = reader.GetValue(0).ToString(); 24 | string MyExpression; 25 | string MeasureName = "***Your Measure Here****"; 26 | string NamePreFix = "# of "; 27 | string NamePostFix = ""; 28 | 29 | //Create measure for item 30 | MyExpression = 31 | "CALCULATE ( [" + MeasureName + "], " + MyColumn + " = \"" + MyItem + "\" )" ; 32 | c.Table.AddMeasure( 33 | NamePreFix + MyItem + NamePostFix, 34 | MyExpression 35 | ); 36 | 37 | } 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /Tabular Editor 2/Power BI Model Refresh Cancel.csx: -------------------------------------------------------------------------------- 1 | // Force stops a refresh in the Power-BI service for the specified model. 2 | // Important – When cancelling a refresh, remember the service will perform a re-try 3 times. 3 | // You will need to cancel the refresh 3 times. When a refresh is cancelled it may take a couple minutes before 4 | // the re-try kicks in and a new process is started. 5 | 6 | 7 | #r "Microsoft.AnalysisServices.Core.dll" 8 | 9 | var DMV_Cmd = ExecuteDax("SELECT [SESSION_ID],[SESSION_LAST_COMMAND] FROM $SYSTEM.DISCOVER_SESSIONS").Tables[0]; 10 | bool runTMSL = true; 11 | string databaseID = Model.Database.ID; 12 | string databaseName = Model.Database.Name; 13 | string sID = string.Empty; 14 | 15 | for (int r = 0; r < DMV_Cmd.Rows.Count; r++) 16 | { 17 | string sessionID = DMV_Cmd.Rows[r][0].ToString(); 18 | string cmdText = DMV_Cmd.Rows[r][1].ToString(); 19 | 20 | // Capture refresh command for the database 21 | if (cmdText.StartsWith(""+databaseID+"")) 22 | { 23 | sID = sessionID; 24 | } 25 | } 26 | 27 | if (sID == string.Empty) 28 | { 29 | Error("No processing Session ID found for the '"+databaseName+"' model."); 30 | return; 31 | } 32 | 33 | if (runTMSL) 34 | { 35 | Model.Database.TOMDatabase.Server.CancelSession(sID); 36 | Info("Processing for the '"+databaseName+"' model has been cancelled (Session ID: "+sID+")."); 37 | } 38 | else 39 | { 40 | sID.Output(); 41 | } -------------------------------------------------------------------------------- /Intermediate/Search and Replace Matched Pairs of Strings in Selected Measures.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Multi-string Search and Replace in any selected measure 3 | * 4 | * Author: Dan Meissner 5 | * 6 | * This script iterates through all currently-selected measures, using the matched pairs of strings 7 | * in the 2-D array named "ReplacementPair" below as a template, replacing all instances of "FromStringX" 8 | * in the current measure's DAX expression with the corresponding "ToStringX" 9 | * 10 | */ 11 | 12 | // Replace Text Strings Pairs that appear in selected measures 13 | 14 | // Update the value of the text arrays for your desired usecase. Make sure to structure the list as: 15 | // var ReplacementPair = new string[,] {{"FromString1","ToString1"},{"FromString2","ToString2"},{"FromString3","ToString3"}}; 16 | 17 | // Add as many From and To pairs to the array as needed. 18 | // (technically C# has a 2GB memory size and 4 Billion array item limit, but... really...) 19 | 20 | // If the string you are either searching for or replacing with contains a double quote " then you need to 'escape it' by 21 | // preceding it with a backslash (as in \") to have that quote character within the respective text string 22 | 23 | var ReplacementPair = new string[,] { {"FromString1","ToString1"}, 24 | {"FromString2","ToString2"}, 25 | {"FromString3","ToString3"} }; 26 | 27 | foreach (var m in Selected.Measures) 28 | { 29 | for (int i=0; i < ReplacementPair.GetLength(0);) 30 | { 31 | m.Expression = m.Expression.Replace(ReplacementPair[i,0],ReplacementPair[i,1]); 32 | i++; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Basic/DAX Comment to Measure Description.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Copy DAX Comment into the measure's description field. 3 | * 4 | * Author: Dan Meissner, twitter.com/danmeissner 5 | * 6 | * This script, when executed, will loop through all the measures in the model and 7 | * look for the first comment (aka. green text :) ) as designated by "//". It will copy the entire comment 8 | * until the next line break and put that into the measure's description. 9 | * 10 | * If no comment of this type exists within the DAX Expression, then it copies the entire 11 | * DAX expression into the measure's description. 12 | * 13 | * At this point it only works on the first comment and for comments using slashes "//" but 14 | * hope someone (or myself) can improve it to collect all comments with slashes 15 | * or slash star formats. 16 | */ 17 | 18 | foreach(var m in Model.AllMeasures) { 19 | // Find the first comment using the two forward slashes 20 | string DAXCode = m.Expression; 21 | string separator1 = "//"; 22 | string result1 = ""; 23 | 24 | // Part A: get index of separator. 25 | int separatorIndex1 = DAXCode.IndexOf(separator1); 26 | 27 | // Part B: if separator exists, get substring. 28 | if (separatorIndex1 >= 0) { 29 | result1 = DAXCode.Substring(separatorIndex1 + separator1.Length); 30 | } 31 | // Repeat to find the first line break after the first comment. 32 | string separator2 = "\n"; 33 | string result2 = ""; 34 | 35 | // Part A: get index of separator. 36 | int separatorIndex2 = result1.IndexOf(separator2); 37 | 38 | // Part B: if separator exists, get substring. 39 | if (separatorIndex2 >= 0) { 40 | result2 = result1.Substring(0,separatorIndex2); 41 | } 42 | // If there is a comment, then add it to the description, 43 | // otherwise add the entire expression 44 | if (m.Expression.Contains("//")) { 45 | m.Description = result2; 46 | } else { 47 | m.Description = m.Expression; 48 | }; 49 | } -------------------------------------------------------------------------------- /Intermediate/Create Time Intelligence Measures Using Calculation Groups.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Generate time intelligence measures based on calculation group items already created 3 | * 4 | * Author: Benoit Fedit, https://datakuity.com/ 5 | * 6 | * You must have created the calculation group items beforehand (see link below) 7 | * https://docs.microsoft.com/en-us/analysis-services/tabular-models/calculation-groups?view=asallproducts-allversions 8 | * To add more measure simply copy/paste the YTD script and replace YTD by your calculation item name 9 | */ 10 | 11 | 12 | // For each selected measure create YTY, PY, PY YTD, YOY, YOY% measures 13 | foreach(var m in Selected.Measures) { 14 | 15 | // YTD 16 | m.Table.AddMeasure( 17 | m.Name + " YTD", // Name 18 | "Calculate(" + m.DaxObjectName + ", 'Time Intelligence'[Time Calculation]=\"YTD\")", 19 | m.DisplayFolder // Display Folder 20 | ); 21 | 22 | // PY 23 | m.Table.AddMeasure( 24 | m.Name + " YTD", // Name 25 | "Calculate(" + m.DaxObjectName + ", 'Time Intelligence'[Time Calculation]=\"PY\")", 26 | m.DisplayFolder // Display Folder 27 | ); 28 | 29 | // PY YTD 30 | m.Table.AddMeasure( 31 | m.Name + " PY YTD", // Name 32 | "Calculate(" + m.DaxObjectName + ", 'Time Intelligence'[Time Calculation]=\"PY YTD\")", 33 | m.DisplayFolder // Display Folder 34 | ); 35 | 36 | // YOY 37 | m.Table.AddMeasure( 38 | m.Name + " YOY", // Name 39 | "Calculate(" + m.DaxObjectName + ", 'Time Intelligence'[Time Calculation]=\"YOY\")", 40 | m.DisplayFolder // Display Folder 41 | ).FormatString = "0.0 %"; 42 | 43 | // YOY% 44 | m.Table.AddMeasure( 45 | m.Name + " YOY%", // Name 46 | "Calculate(" + m.DaxObjectName + ", 'Time Intelligence'[Time Calculation]=\"YOY%\")", 47 | m.DisplayFolder // Display Folder 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /Intermediate/Create Perspective From Fact Tables.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Create Perspective From Fact Tables 3 | * 4 | * Author: Curtis Stallings, https://www.linkedin.com/in/curtisrs/ 5 | * 6 | * This script will take the selected tables and create a perspective that includes all the related tables, measures, & calc groups. 7 | * It will take into consideration nested measures and tables. 8 | * This script is useful for adding context to rather large models with multiple fact tables, 9 | * Letting you narrow down the view for an end user, so they are not overwhelmed 10 | * 11 | */ 12 | 13 | //You can select 1 or multiple tables, each table will get it's own perspective. 14 | foreach(var table in Selected.Tables){ 15 | 16 | //Check if Table Name already exists as a persp, Creates Perspective, Then Adds to Perspective 17 | if (!Model.Perspectives.Any(a => a.Name == table.Name)) 18 | { 19 | Model.AddPerspective(table.Name); 20 | } 21 | Model.Tables[table.Name].InPerspective[table.Name] = true; 22 | 23 | //Gets and adds Related Tables to Perspective 24 | foreach(var related in table.RelatedTables){ 25 | related.InPerspective[table.Name] = true; 26 | } 27 | 28 | //Gets and adds All Related Measures... Also includes Nested Measures. 29 | foreach(var measure in Model.AllMeasures){ 30 | foreach(var measure_dependency in measure.DependsOn.Deep()){ 31 | if(measure_dependency.ObjectType.ToString() == "Table" 32 | && Model.Tables[measure_dependency.Name].InPerspective[table.Name] == true 33 | && Model.Tables[measure_dependency.Name] == Model.Tables[table.Name]){ 34 | measure.InPerspective[table.Name] = true; 35 | } 36 | } 37 | } 38 | 39 | //Gets and adds All Calculation Groups... And the Calculation Group Dependencies 40 | foreach(var calc_group in Model.CalculationGroups){ 41 | foreach(var calc_item in calc_group.CalculationItems){ 42 | foreach(var calc_dependency in calc_item.DependsOn.Deep()){ 43 | if(calc_dependency.ObjectType.ToString() == "Table" 44 | && Model.Tables[calc_dependency.Name].InPerspective[table.Name]){ 45 | calc_group.InPerspective[table.Name] = true; 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Filtered Measures by column values.csx: -------------------------------------------------------------------------------- 1 | // '2022-09-24 / B.Agullo / Removed ScriptHost to make it TE2 compatible 2 | // '2022-05-21 / B.Agullo / 3 | // FILTERED MEASURES BY COLUMN VALUES SCRIPT 4 | // creates a measure for each of the values in a column filtering the selected base measure 5 | // step by step instructions at https://www.esbrina-ba.com/creating-filtered-measures-or-how-to-show-the-total-along-with-the-detail-in-a-chart/ 6 | 7 | var measures = Selected.Measures; 8 | 9 | if (measures.Count == 0) 10 | { 11 | Error("Select one or more measures"); 12 | } 13 | 14 | Table table = SelectTable(); 15 | Column column = SelectColumn(table); 16 | 17 | string query = "EVALUATE DISTINCT(" + column.DaxObjectFullName + ")"; 18 | 19 | using (var reader = Model.Database.ExecuteReader(query)) 20 | { 21 | // Create a loop for every row in the resultset 22 | while (reader.Read()) 23 | { 24 | string columnValue = reader.GetValue(0).ToString(); 25 | string formulaColumnValue = columnValue; 26 | 27 | 28 | 29 | if (column.DataType.Equals(DataType.String)) 30 | { 31 | formulaColumnValue = "\"" + columnValue + "\""; 32 | } 33 | 34 | 35 | foreach (Measure measure in measures) 36 | { 37 | string measureName = measure.Name + " " + columnValue; 38 | string measureExpression = 39 | string.Format("CALCULATE({0},{1}={2})", 40 | measure.DaxObjectName, 41 | column.DaxObjectFullName, 42 | formulaColumnValue 43 | ); 44 | string measureDescription = 45 | string.Format("{0} filtered by {1} = {2}", 46 | measure.Name, 47 | column.Name, 48 | columnValue 49 | ); 50 | string displayFolderName = 51 | string.Format("{0} by {1}", 52 | measure.Name, 53 | column.Name 54 | ); 55 | Measure newMeasure = 56 | measure.Table.AddMeasure( 57 | name: measureName, 58 | expression: measureExpression, 59 | displayFolder: displayFolderName 60 | ); 61 | newMeasure.Description = measureDescription; 62 | newMeasure.FormatDax(); 63 | newMeasure.FormatString = measure.FormatString; 64 | 65 | 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Intermediate/Dynamic measure selector.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Dynamic measure selector 3 | * 4 | * Author: Daniel Otykier, twitter.com/DOtykier 5 | * 6 | * Use this script to auto-generate a disconnected measure selector table 7 | * along with a single SWITCH measure, for a selection of measures. 8 | * More info: https://tabulareditor.com/2020/08/24/Generating-a-dynamic-measure-selector.html 9 | */ 10 | 11 | // (1) Name of disconnected selector table: 12 | var selectorTableName = "Measure Selector"; 13 | 14 | // (2) Name of column on selector table: 15 | var selectorTableColumnName = "Measure"; 16 | 17 | // (3) Name of dynamic switch measure: 18 | var dynamicMeasureName = "Dynamic Measure"; 19 | 20 | // (4) Name of dynamic switch measure's parent table: 21 | var dynamicMeasureTableName = "Measure Selector"; 22 | 23 | // (5) Fallback DAX expression: 24 | var fallbackDax = "BLANK()"; 25 | 26 | // ----- Do not modify script below this line ----- 27 | 28 | if(Selected.Measures.Count == 0) { 29 | Error("Select one or more measures"); 30 | return; 31 | } 32 | 33 | // Get or create selector table: 34 | CalculatedTable selectorTable; 35 | if(!Model.Tables.Contains(selectorTableName)) Model.AddCalculatedTable(selectorTableName); 36 | selectorTable = Model.Tables[selectorTableName] as CalculatedTable; 37 | 38 | // Get or create dynamic measure: 39 | Measure dynamicMeasure; 40 | if(!Model.Tables[dynamicMeasureTableName].Measures.Contains(dynamicMeasureName)) 41 | Model.Tables[dynamicMeasureTableName].AddMeasure(dynamicMeasureName); 42 | dynamicMeasure = Model.Tables[dynamicMeasureTableName].Measures[dynamicMeasureName]; 43 | 44 | // Generate DAX for disconnected table: 45 | // SELECTCOLUMNS({"Measure 1", "Measure 2", ...}, "Measure", [Value]) 46 | var selectorTableDax = "SELECTCOLUMNS(\n {\n " + 47 | string.Join(",\n ", Selected.Measures.Select(m => "\"" + m.Name + "\"").ToArray()) + 48 | "\n },\n \"" + selectorTableColumnName + "\", [Value]\n)"; 49 | 50 | // Generate DAX for dynamic metric: 51 | // VAR _s = SELECTEDVALUE('Metric Selection'[Value]) RETURN SWITCH(_s, ...) 52 | var dynamicMeasureDax = 53 | "VAR _s =\n SELECTEDVALUE('" + selectorTableName + "'[" + selectorTableColumnName + "])\n" + 54 | "RETURN\n SWITCH(\n _s,\n " + 55 | string.Join(",\n ", Selected.Measures.Select(m => "\"" + m.Name + "\", " + m.DaxObjectFullName).ToArray()) + 56 | ",\n " + fallbackDax + "\n )"; 57 | 58 | // Assign DAX expressions: 59 | selectorTable.Expression = selectorTableDax; 60 | dynamicMeasure.Expression = dynamicMeasureDax; -------------------------------------------------------------------------------- /Basic/Preview top50 values of selected columns by number of rows.csx: -------------------------------------------------------------------------------- 1 | // Author: Martin de la Herran, expanded from original work from Tommy Puglia ( https://pugliabi.com ) 2 | // Script to show the top 50 distinct values based on selected columns. 3 | // It shows the top 50 values of the selected columns and number and % of rows, 4 | // The [@SPECIAL] column indicates clearly blank vs empty string, and also shows the total remainig (after the top50) (in this case, disregard the individual value, it is a placeholder) 5 | 6 | // Pre-Req: 7 | // Save this Macro with the following: 8 | // Name: Preview\Top50 9 | // Marco Context: Column 10 | // (Hint, you can organize your macros in TE3 by grouping based on a "\" separator!") 11 | 12 | // Instructions: 13 | // 1. Select the columns you want to see the distinct values for 14 | // 2. Run the script (Right Click, on the selected columns, and choose the Preview --> Top50 macro) 15 | // 3. For every column chosen, a pop-up will appear 16 | 17 | 18 | foreach(var c in Selected.Columns) 19 | { 20 | string cName = c.Name; 21 | string cTable = c.Table.DaxObjectFullName; 22 | var cColumn = c.DaxObjectFullName; 23 | var cColumnName = c.DaxObjectName; 24 | string daxQuery = 25 | " VAR _TotalRows = COUNTROWS( " + cTable + " ) " 26 | +" VAR _TOP = " 27 | +" TOPN(50 , " 28 | +" ADDCOLUMNS( " 29 | +" VALUES( " + cColumn + " ) " 30 | +" , \"@SPECIAL\" , IF(ISBLANK( " + cColumn + " ) , \"!!____ BLANK ____!!\" " 31 | +" , IF(FORMAT( " + cColumn + " , 0.0000 )==\"\", \"!!____ EMPTY STRING ____!!\" " 32 | +" , blank() )) " 33 | +" , \"@CR\" , CALCULATE(COUNTROWS(+ " + cTable + " )) " 34 | +" , \"@%CR\" ,ROUND(100*CALCULATE(COUNTROWS(+ " + cTable + " ))/_TotalRows , 2) " 35 | +" ) " 36 | +" , [@CR] , DESC , " + cColumnName + " , ASC " 37 | +" ) " 38 | +" VAR _REMAINING = _TotalRows - sumx(_TOP, [@CR]) " 39 | +" var _DUMMY = MIN( " + cColumn + " ) " 40 | +" VAR _ROWREMAIN = { ( _DUMMY , \"!!____ OTHER ____!!\" , _REMAINING , ROUND(100*_REMAINING/_TotalRows , 2) )} " 41 | +" VAR _FINAL = FILTER(UNION( _TOP , _ROWREMAIN), [@CR] > 0 ) " 42 | + "RETURN _FINAL " 43 | + "ORDER BY [@SPECIAL] DESC, [@CR] DESC " ; 44 | 45 | //daxQuery.Output() ; // uncomment this is you want to see the DAX query 46 | 47 | var cResult = EvaluateDax(daxQuery); 48 | 49 | cResult.Output(); 50 | } 51 | -------------------------------------------------------------------------------- /Basic/Extended Properties Preview/ep_Peak Sel Column by Global Measure.csx: -------------------------------------------------------------------------------- 1 | // Author: Tommy Puglia 2 | // Website: https://pugliabi.com 3 | // Check out the Explicit Measures Podcast on Apple, spotify, & YouTube! 4 | // https://www.youtube.com/playlist?list=PLn1m_aBmgsbHr83c1P6uqaWF5PLdFzOjj 5 | // 6 | // 7 | // A simple test that may change how we develop and share scripts and easily create tests, new measures, and sharing content. 8 | 9 | // An all too column problem with sharing macros is usually we need to configure some of the variables (measure name, date table name, etc). This becomes especially tedious as this has to occur per data model, rather than for a given script. This greatly reduces the ability of utilizing macros in a more productive way. 10 | 11 | // Enter Extended Properties. Tied to the model is the ability to create a key and value pair, which can is defined in the model properties. Further, we can use Tabular Editor 3's scripts to actually return the value of these properties. 12 | 13 | // This can solve and open up solutions such as: 14 | 15 | // - Creating a macro that when multiple columns are chosen, will default to always summarize / group by a given measure 16 | // - define the Date Table / Date Column in a model for Calculated Groups or time intelligence 17 | // - define the date column in the fact table once for time intelligence 18 | // - set other variables that are used in multiple macros 19 | 20 | // This is simply a test to see what is returned. It will group a column and return a Summarize based on the "Global Measure" From the Extended Properties. 21 | 22 | // Instructions: 23 | // 1. Open the model in Tabular Editor 3 24 | // 2. Go to the properties of the model, and choose the Extended Properties field (under Annotations) 25 | // 3. Add a new property with the the DAX Object Name of a measure in the model (such as [Total Sales]) 26 | // 4. Choose the Column in the object explorer, and run the script! 27 | 28 | 29 | 30 | var s = Model.GetExtendedProperties(); 31 | var tt = Model.GetExtendedProperty(0); 32 | string ev = "Evaluate \n"; 33 | string newli = "\n"; 34 | string Summa = "SUMMARIZE("; 35 | string TableFromColumn = Selected.Column.Table.Name; 36 | string newlinecomma = ", \n \n"; 37 | string ColumnChosen = Selected.Column.DaxObjectFullName; 38 | string MeasureNameSummarize = ", \n \n \"YourMeasure!\" , "; 39 | string MeasureFromProperty = tt; 40 | string closeit = ")"; 41 | string orby = "Order By " + tt + " DESC"; 42 | string stringallTogether = newli + Summa + TableFromColumn + newlinecomma + ColumnChosen + MeasureNameSummarize 43 | + MeasureFromProperty; 44 | var result = EvaluateDax(stringallTogether + ")" + " \n" + orby); 45 | result.Output(); 46 | 47 | -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Create Field Parameter.csx: -------------------------------------------------------------------------------- 1 | #r "Microsoft.VisualBasic" 2 | using Microsoft.VisualBasic; 3 | 4 | //99% comes from Daniel Otykier --> https://github.com/TabularEditor/TabularEditor3/issues/541#issuecomment-1129228481 5 | //1% B.Agullo --> pop-up to choose parameter name. 6 | 7 | // Before running the script, select the measures or columns that you 8 | // would like to use as field parameters (hold down CTRL to select multiple 9 | // objects). Also, you may change the name of the field parameter table 10 | // below. NOTE: If used against Power BI Desktop, you must enable unsupported 11 | // features under File > Preferences (TE2) or Tools > Preferences (TE3). 12 | var name = Interaction.InputBox("Provide the name for the field parameter","Parameter name","Parameter"); 13 | 14 | if(Selected.Columns.Count == 0 && Selected.Measures.Count == 0) throw new Exception("No columns or measures selected!"); 15 | 16 | // Construct the DAX for the calculated table based on the current selection: 17 | var objects = Selected.Columns.Any() ? Selected.Columns.Cast() : Selected.Measures; 18 | var dax = "{\n " + string.Join(",\n ", objects.Select((c,i) => string.Format("(\"{0}\", NAMEOF('{1}'[{0}]), {2})", c.Name, c.Table.Name, i))) + "\n}"; 19 | 20 | // Add the calculated table to the model: 21 | var table = Model.AddCalculatedTable(name, dax); 22 | 23 | // In TE2 columns are not created automatically from a DAX expression, so 24 | // we will have to add them manually: 25 | var te2 = table.Columns.Count == 0; 26 | var nameColumn = te2 ? table.AddCalculatedTableColumn(name, "[Value1]") : table.Columns["Value1"] as CalculatedTableColumn; 27 | var fieldColumn = te2 ? table.AddCalculatedTableColumn(name + " Fields", "[Value2]") : table.Columns["Value2"] as CalculatedTableColumn; 28 | var orderColumn = te2 ? table.AddCalculatedTableColumn(name + " Order", "[Value3]") : table.Columns["Value3"] as CalculatedTableColumn; 29 | 30 | if(!te2) { 31 | // Rename the columns that were added automatically in TE3: 32 | nameColumn.IsNameInferred = false; 33 | nameColumn.Name = name; 34 | fieldColumn.IsNameInferred = false; 35 | fieldColumn.Name = name + " Fields"; 36 | orderColumn.IsNameInferred = false; 37 | orderColumn.Name = name + " Order"; 38 | } 39 | // Set remaining properties for field parameters to work 40 | // See: https://twitter.com/markbdi/status/1526558841172893696 41 | nameColumn.SortByColumn = orderColumn; 42 | nameColumn.GroupByColumns.Add(fieldColumn); 43 | fieldColumn.SortByColumn = orderColumn; 44 | fieldColumn.SetExtendedProperty("ParameterMetadata", "{\"version\":3,\"kind\":2}", ExtendedPropertyType.Json); 45 | fieldColumn.IsHidden = true; 46 | orderColumn.IsHidden = true; -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Referential Integrity Check Measures.csx: -------------------------------------------------------------------------------- 1 | //Select the desired table to store all data quality measures 2 | 3 | 4 | string overallCounterExpression = ""; 5 | string overallCounterName = "Total Unmapped Items"; 6 | 7 | string overallDetailExpression = "\"\""; 8 | string overallDetailName = "Data Problems"; 9 | 10 | Table tableToStoreMeasures = Selected.Tables.First(); 11 | 12 | foreach (var r in Model.Relationships) 13 | { 14 | 15 | 16 | bool isOneToMany = 17 | r.FromCardinality == RelationshipEndCardinality.One 18 | & r.ToCardinality == RelationshipEndCardinality.Many; 19 | 20 | bool isManyToOne = 21 | r.FromCardinality == RelationshipEndCardinality.Many 22 | & r.ToCardinality == RelationshipEndCardinality.One; 23 | 24 | Column manyColumn = null as Column; 25 | Column oneColumn = null as Column; 26 | bool isOneToManyOrManyToOne = true; 27 | if (isOneToMany) 28 | { 29 | manyColumn = r.ToColumn; 30 | oneColumn = r.FromColumn; 31 | 32 | } 33 | else if (isManyToOne) 34 | { 35 | manyColumn = r.FromColumn; 36 | oneColumn = r.ToColumn; 37 | } 38 | else 39 | { 40 | isOneToManyOrManyToOne = false; 41 | } 42 | 43 | if (isOneToManyOrManyToOne) 44 | { 45 | 46 | string orphanCountExpression = 47 | "CALCULATE(" 48 | + "SUMX(VALUES(" + manyColumn.DaxObjectFullName + "),1)," 49 | + oneColumn.DaxObjectFullName + " = BLANK()" 50 | + ")"; 51 | string orphanMeasureName = 52 | manyColumn.Name + " not mapped in " + manyColumn.Table.Name; 53 | 54 | Measure newCounter = tableToStoreMeasures.AddMeasure(name: orphanMeasureName, expression: orphanCountExpression,displayFolder:"_Data quality Measures"); 55 | newCounter.FormatDax(); 56 | 57 | string orphanTableTitleMeasureExpression = newCounter.DaxObjectFullName + " & \" " + newCounter.Name + "\""; 58 | string orphanTableTitleMeasureName = newCounter.Name + " Title"; 59 | 60 | Measure newTitle = tableToStoreMeasures.AddMeasure(name: orphanTableTitleMeasureName, expression: orphanTableTitleMeasureExpression, displayFolder: "_Data quality Titles"); 61 | newTitle.FormatDax(); 62 | 63 | overallCounterExpression = overallCounterExpression + "+" + newCounter.DaxObjectFullName; 64 | overallDetailExpression = overallDetailExpression 65 | + " & IF(" + newCounter.DaxObjectFullName + "> 0," 66 | + newTitle.DaxObjectFullName + " & UNICHAR(10))"; 67 | 68 | }; 69 | 70 | }; 71 | 72 | Measure counter = tableToStoreMeasures.AddMeasure(name: overallCounterName, expression: overallCounterExpression); 73 | counter.FormatDax(); 74 | 75 | 76 | Measure descr = tableToStoreMeasures.AddMeasure(name: overallDetailName, expression: overallDetailExpression); 77 | descr.FormatDax(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tabular Editor Scripts 2 | Community repository for sharing and discussing scripts for use with Tabular Editor. Originally forked from [TabularEditor Scripts](https://github.com/TabularEditor/Scripts). 3 | 4 | ## Using scripts from this repository 5 | If you'd like to use a script from this repository, simply copy the script content into the Advanced Scripting pane of Tabular Editor. For more details about scripting, [read this article](https://github.com/otykier/TabularEditor/wiki/Advanced-Scripting). You may also [store scripts as Custom Actions](https://github.com/otykier/TabularEditor/wiki/Custom-Actions) that are integrated in Tabular Editor's UI. 6 | 7 | **DISCLAIMER:** THE SCRIPTS IN THIS REPOSITORY ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. You are responsible for ensuring that any scripts you execute does not contain malicious code, or does not cause unwanted changes to your tabular models. If you do not understand what the code does, do not blindly execute it! A script has access to the full power of the .NET platform, meaning it can theoretically alter system files, download stuff from the internet, etc. Do not execute a script from sources you do not trust. 8 | 9 | ## How to contribute 10 | Fork the repo, add your scripts and submit a pull request - it's that simple! 11 | 12 | Scripts should use the `.csx` file extension. If you plan to submit a collection of multiple scripts, feel free to put them into a subfolder and provide a `README.md` file with additional documentation. 13 | 14 | Please ensure that your script is thoroughly documented with a comment section at the top of the file. Feel free to use the following snippet as a template: 15 | 16 | ```csharp 17 | /* 18 | * Title: Auto-generate SUM measures from columns 19 | * 20 | * Author: Daniel Otykier, twitter.com/DOtykier 21 | * 22 | * This script, when executed, will loop through the currently selected columns, 23 | * creating one SUM measure for each column and also hiding the column itself. 24 | */ 25 | 26 | // Loop through all currently selected columns: 27 | foreach(var c in Selected.Columns) 28 | { 29 | var newMeasure = c.Table.AddMeasure( 30 | "Sum of " + c.Name, // Name 31 | "SUM(" + c.DaxObjectFullName + ")", // DAX expression 32 | c.DisplayFolder // Display Folder 33 | ); 34 | 35 | // Set the format string on the new measure: 36 | newMeasure.FormatString = "0.00"; 37 | 38 | // Provide some documentation: 39 | newMeasure.Description = "This measure is the sum of column " + c.DaxObjectFullName; 40 | 41 | // Hide the base column: 42 | c.IsHidden = true; 43 | } 44 | ``` 45 | ## Tips and tricks 46 | 47 | - When trying to add quotation marks for string value (aka quotation within quatation) the "\" should be added. It is added for opening and ending quatation 48 | - Experiment, but make a copy of the file. If anything goes worn CTRL+Z and you will undo the script 49 | - 50 | -------------------------------------------------------------------------------- /Intermediate/Clean object names.csx: -------------------------------------------------------------------------------- 1 | #r "System.Text.RegularExpressions" 2 | 3 | /* 4 | * Title: Clean Object Names 5 | * 6 | * Author: Darren Gosbell, twitter.com/DarrenGosbell 7 | * 8 | * This script, when executed, will loop through your model and update the 9 | * names of any tables and columns with CamelCaseNames and insert spaces before upper 10 | * case characters so you end up with 11 | * 12 | * before: CalendarYearNum 13 | * after: Calendar Year Num 14 | * 15 | * This script ignores any columns that already have spaces in the names 16 | * and any hidden columns. It also skips adjacent upper case characters 17 | * so "MyTXTColumn" becomes "My TXT Column" 18 | */ 19 | 20 | // this regular expression splits strings on underscores and changes from lower to upper case 21 | // so "my_column_name" becomes an array like {"my", "_", "column", "_", "name"} 22 | // and "MyOtherColumnName" becomes an array like {"My", "Other", "Column", "Name"} 23 | var rex = new System.Text.RegularExpressions.Regex( "(^[a-z]+|[A-Z]+(?![a-z])|[A-Z][a-z]+|[^A-Z,a-z]+|[_]|[a-z]+)"); 24 | 25 | // if any of the following are the first word of a table name they will be stripped out 26 | List tablePrefixesToIgnore = new List() {"dim","fact", "vw","tbl","vd","td","tf","vf"}; 27 | 28 | // if any of the following are the last word of a table name they will be stripped out 29 | List tableSuffixesToIgnore = new List() {"dim", "fact"}; 30 | 31 | foreach (var tbl in Model.Tables) 32 | { 33 | if (!tbl.IsHidden && !tbl.Name.Contains(" ")) 34 | { 35 | string name = tbl.Name; 36 | var matches = rex.Matches(name); 37 | var firstWord = matches[0]; 38 | var lastWord = matches[matches.Count-1]; 39 | string[] words = matches 40 | .OfType() 41 | .Where(m => 42 | // skip words that are just underscores so that they are replaced with spaces 43 | m.Value != "_" 44 | // skip the first word if it matches one of the prefixes to ignore 45 | && !(m == firstWord && tablePrefixesToIgnore.Contains(m.Value,System.StringComparer.OrdinalIgnoreCase)) 46 | // skip the last word if it matches one of the suffixes to ignore 47 | && !(m == lastWord && tableSuffixesToIgnore.Contains(m.Value,System.StringComparer.OrdinalIgnoreCase )) 48 | ) 49 | .Select(m => char.ToUpper(m.Value.First()) + m.Value.Substring(1)) 50 | .ToArray(); 51 | string result = string.Join(" ", words); 52 | tbl.Name = result; 53 | } 54 | 55 | foreach (var col in tbl.Columns) 56 | { 57 | if (!col.IsHidden && !col.Name.Contains(" ")) 58 | { 59 | string name = col.Name; 60 | string[] words = rex.Matches(name) 61 | .OfType() 62 | // skip underscores 63 | .Where(m => m.Value != "_" ) 64 | .Select(m => char.ToUpper(m.Value.First()) + m.Value.Substring(1)) 65 | .ToArray(); 66 | string result = string.Join(" ", words); 67 | col.Name = result; 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Basic/Generate Last Updated Table.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: Generate Last Updated table 3 | * 4 | * Author: Irfan Charania 5 | * 6 | * There are times when you want to know when your model was last processed at the table level. 7 | * SSAS Tabular does not provide an easy built-in way to do this. 8 | * 9 | * This script, when executed, will loop through all the tables in the model, and 10 | * 1. create (or update) a hidden "Last Processed" calculated column, and 11 | * 2. create (or update) a "Last Updated Tabular" calculated table containing data from the above calculated column 12 | * 13 | * Ref: https://www.sqlbi.com/articles/last-process-date-in-ssas-tabular/ 14 | */ 15 | 16 | // Edit these values as desired 17 | var columnName = "Last Processed"; 18 | var tableName = "Last Updated Tabular"; 19 | var dateFormatString = "yyyy-MMM-dd h:mm AM/PM"; 20 | 21 | // --------------------------- 22 | // 1. create (or update) a hidden "Last Processed" calculated column 23 | // --------------------------- 24 | 25 | // Loop through all tables in model 26 | foreach(var table in Model.Tables) { 27 | 28 | // for all non-calculated tables 29 | if (table.SourceType.ToString() != "Calculated" ){ 30 | 31 | CalculatedColumn column; 32 | 33 | // if calculated column does not exist, create one 34 | if (!table.Columns.Contains(columnName)) 35 | { 36 | column = table.AddCalculatedColumn( 37 | "Last Processed", // Name 38 | "NOW()", // DAX Expression 39 | "_Debug" // Display Folder 40 | ); 41 | } 42 | // else retreive the calculated column 43 | else{ 44 | column = (CalculatedColumn)table.Columns[columnName]; 45 | } 46 | 47 | // and update it's properties 48 | column.DataType = DataType.DateTime; 49 | column.IsHidden = true; 50 | column.FormatString = dateFormatString; 51 | column.Description = "Shows when SSAS table was last processed"; 52 | } 53 | } 54 | 55 | // --------------------------- 56 | // 2. create (or update) a "Last Updated Tabular" calculated table 57 | // containing data from the above calculated column 58 | // --------------------------- 59 | 60 | List rowsList = new List(); 61 | 62 | 63 | // Loop through all tables in model 64 | foreach(var table in Model.Tables) { 65 | 66 | // if calculated column exists 67 | if (table.Columns.Contains(columnName)) 68 | { 69 | // create DAX row expression to create row data 70 | var s = String.Format(@" 71 | ROW ( 72 | ""Last Processed"", FORMAT ( MAX ( {0} ), ""{1}""), 73 | ""Table Name"", ""{2}"" 74 | )" 75 | , table.Columns[columnName].DaxObjectFullName 76 | , dateFormatString 77 | , table.Name); 78 | 79 | // add row expression to list 80 | rowsList.Add(s); 81 | } 82 | } 83 | 84 | // Combine all row statements into a single DAX expression 85 | var rows = String.Join(", ", rowsList); 86 | var expression = String.Format("UNION ( {0} )", rows); 87 | 88 | CalculatedTable tbl; 89 | 90 | // If calculated table already exists, retreive it 91 | if (Model.Tables.Contains(tableName)){ 92 | tbl = (CalculatedTable)Model.Tables[tableName]; 93 | } 94 | // else add new calculated table to the model 95 | else{ 96 | tbl = Model.AddCalculatedTable(tableName); 97 | } 98 | 99 | // update calculated table's expression to earlier-built DAX expression 100 | tbl.Expression = expression; -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Color Measures.csx: -------------------------------------------------------------------------------- 1 | // '2022-08-27 / B.Agullo / one-click enabled, guessing of name and hex color columns 2 | // '2021-05-09 / B.Agullo / added transparent color 3 | // by Bernat Agulló 4 | // www.esbrina-ba.com 5 | 6 | //adapted from Darren Gosbell's script at 7 | // https://darren.gosbell.com/2020/08/the-best-way-to-generate-data-driven-measures-in-power-bi-using-tabular-editor/ 8 | 9 | //This script creates the color measures for each of the colors included in the theme color table. 10 | // See http://www.esbrina-ba.com/theme-compliant-conditional-formatting-measures/ 11 | // Also https://www.esbrina-ba.com/building-a-fading-bar-chart/ 12 | 13 | //adjust to fit your particular model 14 | 15 | 16 | string colorNameTag = "Name"; //string likely to indicate a color name column 17 | string colorCodeTag = "Hex"; //string likely to indicate a color code column 18 | 19 | // -------- do not modify beyond this line (if you don't know what you are doing) 20 | 21 | if(Selected.Tables.Count() != 1) { 22 | Error("Select only table containing color definitions and try again"); 23 | }; 24 | 25 | string colorTableName = Selected.Table.Name; 26 | 27 | //get the first column containing "Name" in its name 28 | string colorColumnNameCandidate = 29 | Selected.Table.Columns 30 | .Where(x => x.Name.Contains(colorNameTag)) 31 | .First().Name; 32 | 33 | 34 | // let the user confirm if the guess is correct 35 | string colorColumnName = 36 | SelectColumn( 37 | Selected.Table, 38 | Selected.Table.Columns[colorColumnNameCandidate], 39 | "Select color Name Column" 40 | ).Name; 41 | 42 | 43 | //get the first column containing "Hex" in its name 44 | string colorColumnCodeCandidate = 45 | Selected.Table.Columns 46 | .Where(x => x.Name.Contains(colorCodeTag)) 47 | .First().Name; 48 | 49 | // let the user confirm if the guess is correct 50 | string hexCodeColumnName = 51 | SelectColumn( 52 | Selected.Table, 53 | Selected.Table.Columns[colorColumnCodeCandidate], 54 | "Select color code Column" 55 | ).Name; 56 | 57 | bool createTransparentColor = true; 58 | 59 | // do not change code below this line 60 | 61 | 62 | string colorColumnNameWithTable = "'" + colorTableName + "'[" + colorColumnName + "]"; 63 | string hexCodeColumnNameWithTable = "'" + colorTableName + "'[" + hexCodeColumnName + "]"; 64 | 65 | string query = "EVALUATE VALUES(" + colorColumnNameWithTable + ")"; 66 | 67 | using (var reader = Model.Database.ExecuteReader(query)) 68 | { 69 | // Create a loop for every row in the resultset 70 | while(reader.Read()) 71 | { 72 | string myColor = reader.GetValue(0).ToString(); 73 | string measureName = myColor; 74 | string myExpression = "VAR HexCode = CALCULATE( SELECTEDVALUE( " + hexCodeColumnNameWithTable + "), " + colorColumnNameWithTable + " = \"" + myColor + "\") VAR Result = FORMAT(hexCode,\"@\") RETURN Result "; 75 | 76 | if(Model.AllMeasures.Any(x => x.Name == measureName)) { 77 | foreach (Measure m in Model.AllMeasures.Where(x => x.Name == measureName).ToList()) { m.Delete(); }; 78 | }; 79 | 80 | var newColorMeasure = Model.Tables[colorTableName].AddMeasure(measureName, myExpression); 81 | newColorMeasure.FormatDax(); 82 | 83 | } 84 | } 85 | 86 | if(createTransparentColor){ 87 | 88 | if(Model.AllMeasures.Any(x => x.Name == "Transparent")) { 89 | foreach (Measure m in Model.AllMeasures.Where(x => x.Name == "Transparent").ToList()) { m.Delete(); }; 90 | }; 91 | 92 | var transparentMeasure = Model.Tables[colorTableName].AddMeasure("Transparent","\"#FFFFFF00\""); 93 | }; 94 | 95 | -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Column Width Calc Group.csx: -------------------------------------------------------------------------------- 1 | #r "Microsoft.VisualBasic" 2 | using Microsoft.VisualBasic; 3 | 4 | // '2021-10-03 / B.Agullo / 5 | // Calc group definition by B.Agullo as presented in https://www.esbrina-ba.com/the-ultimate-hack-to-set-column-widths-in-a-matrix/ 6 | // Code to generate the calc group mostly generated by @Avatori's script published in https://powerofbi.org/2021/09/14/backup-restore-all-calculation-groups-workaround-required-for-some-visuals/ 7 | // "Auto" calc item needs manual tuning that can be done once the calc group is created, instructions included in the calc item. 8 | 9 | // all parameters are required at run time so it can be stored as macro at model level 10 | 11 | 12 | 13 | string calcGroupName = Interaction.InputBox("Provide a name for your Calc Group", "Calc Group Name", "Column Width", 740, 400); 14 | string minWidth = Interaction.InputBox("Enter minimum column width", "Min Column Width", "5", 740, 400); 15 | 16 | int minWidthInt; 17 | 18 | bool isMinWidthANumber = Int32.TryParse(minWidth, out minWidthInt); 19 | 20 | if (!isMinWidthANumber) { 21 | Error(minWidth + " is not a positive integer. Invalid width"); 22 | return; 23 | } else if(minWidthInt < 0) { 24 | Error(minWidth + " is smaller than 0. Invalid Min width."); 25 | return; 26 | }; 27 | 28 | string maxWidth = Interaction.InputBox("Enter minimum column width", "Min Column Width", "7", 740, 400); 29 | 30 | int maxWidthInt; 31 | 32 | bool isMaxWidthANumber = Int32.TryParse(maxWidth, out maxWidthInt); 33 | 34 | if (!isMaxWidthANumber) { 35 | Error(maxWidth + " is not a positive integer. Invalid width"); 36 | return; 37 | } else if(maxWidthInt < minWidthInt) { 38 | Error(maxWidth + " is smaller than " + minWidthInt +". Invalid Max width."); 39 | return; 40 | }; 41 | 42 | 43 | //create calculation group Column Width 44 | var calculationGroupTable1 = Model.AddCalculationGroup (); 45 | (Model.Tables["New Calculation Group"] as CalculationGroupTable).CalculationGroup.Precedence = 1; 46 | calculationGroupTable1.Name = calcGroupName; 47 | calculationGroupTable1.Columns["Name"].Name = calcGroupName; 48 | 49 | for( int i = minWidthInt; i<=maxWidthInt; i++) { 50 | 51 | //create calculation item 5 52 | var calculationItem1 = calculationGroupTable1.AddCalculationItem(Convert.ToString(i,10)); 53 | calculationItem1.Expression = @"SELECTEDMEASURE()"; 54 | calculationItem1.FormatStringExpression = @"VAR currentValue = 55 | SELECTEDMEASURE() 56 | VAR currentLength = 57 | LEN( CONVERT( currentValue, STRING ) ) 58 | 59 | VAR selectedLength = CONVERT( SELECTEDVALUE( 'Column Width'[Column Width], currentLength ), INTEGER ) 60 | 61 | VAR extraSpaces = IF( selectedLength > currentLength, selectedLength - currentLength, 0 ) 62 | 63 | VAR prefix = 64 | REPT( UNICHAR(0160), extraSpaces ) 65 | RETURN 66 | """""""" & prefix & """""""" 67 | & SELECTEDMEASUREFORMATSTRING()"; 68 | calculationItem1.FormatDax(); 69 | 70 | }; 71 | 72 | 73 | 74 | //create calculation item auto; 75 | var calculationItemAuto = calculationGroupTable1.AddCalculationItem("auto"); 76 | calculationItemAuto.Expression = @"SELECTEDMEASURE()"; 77 | calculationItemAuto.FormatStringExpression = @"//// instructions to enable auto-mode: 78 | // 1. delete line SELECTEDMEASUREFORMATSTRING() that is not commented 79 | // 2. uncomment the rest of the code 80 | // 3. replace the expression for maxValue for an expression that will return the max value of your matrix 81 | // You can duplicate the calc item to use in different matrices of your report 82 | 83 | SELECTEDMEASUREFORMATSTRING() 84 | 85 | //VAR currentValue = 86 | // SELECTEDMEASURE() 87 | //VAR currentLength = 88 | // LEN( CONVERT( currentValue, STRING ) ) 89 | // 90 | //VAR maxValue = CALCULATE(MAXX(SUMMARIZE('Sales','Sales'[Order number],'Sales'[Customer]),SELECTEDMEASURE()),ALLSELECTED( sales )) 91 | //VAR maxLength = LEN( TRIM( CONVERT( maxValue, STRING ) )) //+ X if you want some padding 92 | // 93 | //VAR extraSpaces = IF( maxLength > currentLength, maxLength - currentLength, 0 ) 94 | // 95 | //VAR prefix = 96 | // REPT( UNICHAR(0160), extraSpaces ) 97 | //RETURN 98 | // """""""" & prefix & """""""" 99 | // & SELECTEDMEASUREFORMATSTRING()"; 100 | calculationItemAuto.FormatDax(); -------------------------------------------------------------------------------- /Basic/Extended Properties Preview/ep_Create Dynamic Rolling Avg Measures.csx: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Tommy Puglia 3 | // Website: https://pugliabi.com 4 | // Check out the Explicit Measures Podcast on Apple, spotify, & YouTube! 5 | // https://www.youtube.com/playlist?list=PLn1m_aBmgsbHr83c1P6uqaWF5PLdFzOjj 6 | // TITLE!! 7 | // Create Dynamic Rolling Days Measures based on Extended Properties 8 | 9 | // !!!! NOTE 10 | // This script should only be run after you have set up your Global Extended Properties (see the Global Model Extended Properties Template Generator script) 11 | // !!! NOTE 12 | 13 | // This script will create 3 measures per selected measure in your model. The 3 measures are: 14 | // 1. Rolling Days current period 15 | // This is based on your date config (extended properties) and what the default rolling days are (also extended properties) 16 | 17 | // 2. Rolling Days previous period 18 | // This is based on your date config (extended properties) and what the default rolling days are (also extended properties) \ 19 | 20 | // 3. Rolling Days period over period 21 | // This is based on your date config (extended properties) and what the default rolling days are (also extended properties) 22 | 23 | 24 | // Instructions 25 | // pre-requisites: ensure your model extended properties are part of the model and saved back (see top) 26 | // 1. Select measures in your model 27 | // 2. Run the script 28 | // 3. Done! 29 | 30 | 31 | var dateExtended = Model.GetExtendedProperty("DateColumn"); 32 | var factDateColumn = Model.GetExtendedProperty("FactDateColumn"); 33 | string newline = Environment.NewLine; 34 | var displayfolder = "__RollingDays"; 35 | var DefaultDays = Model.GetExtendedProperty("DefaultRollingDays"); 36 | var defaultDaysNumber = Int16.Parse(DefaultDays); 37 | 38 | foreach (var m in Selected.Measures) 39 | { 40 | // Rolling Days 41 | var RollingFirst = m.Table.AddMeasure( 42 | m.Name + " Rolling Days", 43 | "VAR DatesWOFirst = CALCULATE( LASTNONBLANK( " 44 | + dateExtended 45 | + " , " 46 | + m.DaxObjectName 47 | + " ), ALL( " 48 | + factDateColumn 49 | + " ))" 50 | + newline 51 | + " VAR firstDay = ( DatesWOFirst -" 52 | + defaultDaysNumber 53 | + " )" 54 | + newline 55 | + "VAR FirstType = DATESBETWEEN(" 56 | + dateExtended 57 | + " , firstDay, DatesWOFirst )" 58 | + newline 59 | + "VAR LastPrev = FIRSTDATE( FirstType ) - 1 " 60 | + newline 61 | + " VAR FirstPrev = LastPrev - DatesWOFirst " 62 | + newline 63 | + "RETURN " 64 | + newline 65 | + " CALCULATE( " 66 | + m.DaxObjectName 67 | + " , DATESBETWEEN( " 68 | + dateExtended 69 | + ", firstDay, DatesWOFirst - 1 ) ) ", 70 | displayfolder 71 | ); 72 | 73 | 74 | // Previous Rolling 75 | var RollingPrevious = m.Table.AddMeasure( 76 | m.Name + " Rolling Days Prev", 77 | "VAR DaysRolling = " 78 | + defaultDaysNumber 79 | + newline 80 | + "VAR DatesWOFirst = CALCULATE( LASTNONBLANK( " 81 | + dateExtended 82 | + " , " 83 | + m.DaxObjectName 84 | + " ), ALL( " 85 | + factDateColumn 86 | + " ) )" 87 | + newline 88 | + " VAR firstDay = ( DatesWOFirst - DaysRolling )" 89 | + newline 90 | + "VAR FirstType = DATESBETWEEN(" 91 | + dateExtended 92 | + " , firstDay, DatesWOFirst )" 93 | + newline 94 | + "VAR LastPrev = FIRSTDATE( FirstType ) - 1 " 95 | + newline 96 | + " VAR FirstPrev = LastPrev - DaysRolling " 97 | + newline 98 | + "RETURN " 99 | + newline 100 | + " CALCULATE( " 101 | + m.DaxObjectName 102 | + " , DATESBETWEEN( " 103 | + dateExtended 104 | + ", FirstPrev, LastPrev - 1 ) ) ", 105 | displayfolder 106 | ); 107 | 108 | // Pop 109 | var RollingPercent = m.Table 110 | .AddMeasure( 111 | m.Name + " PoP", 112 | "Divide((" 113 | + m.DaxObjectName 114 | + " - [" 115 | + m.Name 116 | + " Rolling Days Prev]), + [" 117 | + m.Name 118 | + " Rolling Days Prev],Blank())", 119 | displayfolder 120 | ); 121 | RollingFirst.FormatDax(); 122 | RollingPrevious.FormatDax(); 123 | RollingPercent.FormatDax(); 124 | 125 | } 126 | 127 | -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Time Int Labels.csx: -------------------------------------------------------------------------------- 1 | #r "Microsoft.VisualBasic" 2 | using Microsoft.VisualBasic; 3 | 4 | // CHANGE LOG: 5 | // '2021-10-16 / B.Agullo / converted to macro using code from Stephen Maguire and Daniel Otykier, and people in twitter helping with C#! 6 | // '2021-07-10 / B.Agullo / 7 | // by Bernat Agulló 8 | // www.esbrina-ba.com 9 | 10 | // FULL EXPLANATION: 11 | // https://www.esbrina-ba.com/time-intelligence-dynamic-legend-in-line-charts/ 12 | 13 | //this script creates an extra calculation group to work together with Time Calculation group 14 | //you need to create the Time calculation group script with the dynamic label measures before running this script 15 | //the names of the measure and affected measure table must match 16 | //if you changed the default valures on the time intel calc group, change them heere too . 17 | 18 | 19 | var ts = Model.Tables.Where(x => x.GetAnnotation("@AgulloBernat") == "Time Intel Calc Group"); 20 | 21 | var timeIntelCalcGroup = null as CalculationGroupTable; 22 | 23 | if (ts.Count() == 1 ) { 24 | timeIntelCalcGroup = ts.First() as CalculationGroupTable; 25 | } else if (ts.Count() < 1) { 26 | Error("Time Itelligence Calc group script by @AgulloBernat has not been successfuly executed yet. Execute it first and then run again the present script"); 27 | return; 28 | } else { 29 | //this should never happen -- who needs two calc groups for time intelligence? 30 | timeIntelCalcGroup = SelectTable(ts, label:"Select your existing time intelligence calc group calculation group table:") as CalculationGroupTable; 31 | }; 32 | 33 | if (timeIntelCalcGroup == null) { return; } // doesn't work in TE3 as cancel button doesn't return null in TE3 34 | 35 | 36 | //init Affected Measure Table 37 | ts = Model.Tables.Where(x => x.GetAnnotation("@AgulloBernat") == "Time Intel Affected Measures Table"); 38 | 39 | var affectedMeasuresTable = null as Table; 40 | 41 | if (ts.Count() == 1 ) { 42 | affectedMeasuresTable = ts.First(); 43 | } else if (ts.Count() < 1) { 44 | Error("Time Itelligence Calc group script by @AgulloBernat has not been successfuly executed yet. Execute it first and then run again the present script"); 45 | return; 46 | } else { 47 | //this should never happen -- who needs two time intelligence affected measures calc tables? 48 | affectedMeasuresTable = SelectTable(ts, label:"Select your existing time intelligence affected measures table:") as CalculationGroupTable; 49 | }; 50 | 51 | if (affectedMeasuresTable == null) { return; } // doesn't work in TE3 as cancel button doesn't return null in TE3 52 | 53 | string labelsCalculationGroupName = Interaction.InputBox("Provide a name for the dynamic labels Calc Group", "Calc Group Name", "Labels", 740, 400); 54 | if(labelsCalculationGroupName == "") return; 55 | 56 | string labelsCalculationGroupColumnName = Interaction.InputBox("Provide a name for the column of the Calc Group", "Calc Group Name", labelsCalculationGroupName, 740, 400); 57 | if(labelsCalculationGroupColumnName == "") return; 58 | 59 | string labelsCalculationItemName = "Last Point Time Calculation"; 60 | 61 | string affectedMeasuresTableName = "Time Intelligence Affected Measures"; //affectedMeasuresTable.Name; 62 | string affectedMeasuresColumnName = "Measure"; // affectedMeasuresTable.Columns[0].Name; 63 | 64 | //add the name of the existing time intel calc group here 65 | string calcGroupName = "Time Intelligence"; 66 | 67 | //add the name for date table of the model 68 | string dateTableName = "Date"; 69 | string dateTableDateColumnName = "Date"; 70 | 71 | string labelAsValueMeasureName = "Label as Value Measure"; 72 | 73 | string flagExpression = "UNICHAR( 8204 )"; 74 | 75 | 76 | //generates new calc group 77 | var calculationGroupTable1 = (Model.AddCalculationGroup(labelsCalculationGroupName) as CalculationGroupTable); 78 | 79 | calculationGroupTable1.Description = "Calculation group to manipulate data labels"; 80 | 81 | //sees the default precedence number assigned 82 | int labelGroupPrecedence = (Model.Tables[labelsCalculationGroupName] as CalculationGroupTable).CalculationGroup.Precedence; 83 | int timeIntelGroupPrecedence = (Model.Tables[calcGroupName] as CalculationGroupTable).CalculationGroup.Precedence; 84 | 85 | //if time intel has lower precedence... 86 | if(labelGroupPrecedence > timeIntelGroupPrecedence) { 87 | //...swap precedence values 88 | (Model.Tables[labelsCalculationGroupName] as CalculationGroupTable).CalculationGroup.Precedence = timeIntelGroupPrecedence; 89 | (Model.Tables[calcGroupName] as CalculationGroupTable).CalculationGroup.Precedence = labelGroupPrecedence; 90 | }; 91 | 92 | 93 | (Model.Tables["Labels"].Columns["Name"] as DataColumn).Name = labelsCalculationGroupColumnName; 94 | var calculationItem1 = calculationGroupTable1.AddCalculationItem(labelsCalculationItemName); 95 | calculationItem1.Expression = "SELECTEDMEASURE()"; 96 | calculationItem1.FormatStringExpression = 97 | "SWITCH(" + 98 | "\n TRUE()," + 99 | "\n SELECTEDMEASURENAME()" + 100 | "\n IN VALUES( '" + affectedMeasuresTableName + "'[" + affectedMeasuresColumnName + "] )," + 101 | "\n VAR maxDateInVisual =" + 102 | "\n CALCULATE( MAX( '" + dateTableName + "'[" +dateTableDateColumnName + "] ), ALLSELECTED( '" + dateTableName + "' ) )" + 103 | "\n VAR maxDateInDataPoint =" + 104 | "\n MAX( '" + dateTableName + "'[" + dateTableDateColumnName + "] )" + 105 | "\n VAR result =" + 106 | "\n IF( maxDateInDataPoint = maxDateInVisual, [" + labelAsValueMeasureName +"] )" + 107 | "\n RETURN" + 108 | "\n " + flagExpression + " & \"\"\"\" & result & \"\"\";\"\"\" & result & \"\"\";\"\"\" & result & \"\"\";\"\"\" & result & \"\"\"\"," + 109 | "\n SELECTEDMEASUREFORMATSTRING()" + 110 | "\n)"; 111 | 112 | calculationItem1.Description = "Show dynamic label as data label of the last point in a line series over a time axis"; 113 | -------------------------------------------------------------------------------- /Intermediate/Create Visuals Translations Measures.csx: -------------------------------------------------------------------------------- 1 | /* 2 | * Create visuals translations measures 3 | * Author: Didier Terrien https://thebipower.fr 4 | * This script generates measures to be used in visuals conditional formating in order to translate 5 | * visuals labels (title, ...). It reads a tsv file which contains the measure list 6 | * 7 | * More details here : https://thebipower.fr/index.php/2020/05/07/visuals-labels-translations-in-power-bi-reports 8 | * Download the example tsv file in the blog post 9 | * There are different methods which have their own advantages. Be sure to read the post above before to use in production. 10 | */ 11 | 12 | // Modify the path and name of the visuals labels file to load 13 | var Visuals_labels_file = @"...\TE visuals labels.tsv"; 14 | 15 | // Modify the name of the table that should hold the measures. It will be created automatically 16 | var Target_table_name = "Visuals translations measures"; 17 | 18 | // Modify the prefix of new measures to avoid conflicts with existing measures 19 | var Measures_prefix = "AT_"; 20 | 21 | // Select a method between "USERCULTURE" and "SLICER" 22 | var Method = "SLICER"; 23 | 24 | 25 | // Optionally select the file manually (Comment code below to use) 26 | 27 | // using (var openFileDialog = new System.Windows.Forms.OpenFileDialog()) { 28 | // openFileDialog.Title = "Select a visuals labels file to load"; 29 | // if (System.IO.File.Exists(Visuals_labels_file)){ 30 | // openFileDialog.InitialDirectory = System.IO.Directory.GetParent(Visuals_labels_file).ToString(); 31 | // } 32 | // openFileDialog.Filter = "tsv files (*.tsv)|*.tsv|All files (*.*)|*.*"; 33 | // openFileDialog.FilterIndex = 1; 34 | // openFileDialog.RestoreDirectory = true; 35 | // if (openFileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { 36 | // //Get the path of specified file 37 | // Visuals_labels_file = openFileDialog.FileName; 38 | // //System.Windows.Forms.MessageBox.Show("Selected file: " + Visuals_labels_file); 39 | // } else { 40 | // Visuals_labels_file = ""; 41 | // } 42 | // } 43 | 44 | 45 | // If file exists 46 | if (System.IO.File.Exists(Visuals_labels_file)) { 47 | // Load the file content 48 | var tsvFileContent = ReadFile(Visuals_labels_file); 49 | 50 | // Split it into rows 51 | var Rows = tsvFileContent.Split(new[] {'\r','\n'},StringSplitOptions.RemoveEmptyEntries); 52 | 53 | // Delete the target table to build it again from scratch 54 | if (Model.Tables.Contains(Target_table_name)) { 55 | Model.Tables[Target_table_name].Delete(); 56 | } 57 | var Target_table = Model.AddCalculatedTable(Target_table_name, "{0}"); 58 | var Fake_column = Target_table.AddCalculatedTableColumn("Fake Column", "[Fake Column]", "", DataType.Int64); 59 | Fake_column.IsHidden = true; 60 | Target_table.Description = "This table is auto generated by a Tabular Editor script. Do not modify it manually"; 61 | Target_table.SetAnnotation("VTAUTOGEN", "1"); // Set a special annotation on the table. It might be useful in the future// Model.Tables.First().Columns("Fake column").Is 62 | 63 | // Iterate all rows 64 | for(int Index_row = 1; Index_row < Rows.Length; Index_row++) { 65 | var Columns = Rows[Index_row].Split(new[] {'\t'},StringSplitOptions.None); // Split the current row into columns 66 | var Object_ID = Columns[0]; 67 | var Page_name = Columns[1]; 68 | var Object_type = Columns[2]; 69 | var Visual_name = Columns[3]; 70 | var Reference_text = Columns[4]; 71 | 72 | // If Reference text is not empty 73 | if (Reference_text.Trim() != "") { 74 | 75 | // Create the measure 76 | var VT_measure = Target_table.AddMeasure(Measures_prefix + Visual_name); 77 | var sb = new System.Text.StringBuilder(); 78 | if (Method == "SLICER"){ 79 | sb.Append("\r\n"); 80 | sb.Append("VAR Reference_text = " + '"' + Visual_name + '"' + "\r\n"); 81 | sb.Append("VAR Filtered_translations = FILTER( 'Visuals translations' ," + "\r\n"); 82 | sb.Append("\t" + "'Visuals translations'[Reference text] = Reference_text )" + "\r\n"); 83 | sb.Append("RETURN COALESCE( CALCULATE(" + "\r\n"); 84 | sb.Append("\t" + "\t" + "\t" + "FIRSTNONBLANK( 'Visuals translations'[Translated text] , TRUE()) ," + "\r\n"); 85 | sb.Append("\t" + "\t" + "\t" + "\t" + "Filtered_translations )" + "\r\n"); 86 | sb.Append("\t" + "\t" + " , Reference_text" + "\r\n"); 87 | sb.Append("\t" + ")"); 88 | } else if (Method == "USERCULTURE") { 89 | sb.Append("\r\n"); 90 | sb.Append("VAR Reference_text = " + '"' + Visual_name + '"' + "\r\n"); 91 | sb.Append("VAR Filtered_translations = FILTER( ALL( 'Visuals translations') ," + "\r\n"); 92 | sb.Append("\t" + "'Visuals translations'[Reference text] = Reference_text &&" + "\r\n"); 93 | sb.Append("\t" + "\t" + "\t" + "'Visuals translations'[Language code] = USERCULTURE()" + "\r\n"); 94 | sb.Append("\t" + ")"); 95 | sb.Append("RETURN COALESCE( CALCULATE(" + "\r\n"); 96 | sb.Append("\t" + "\t" + "\t" + "FIRSTNONBLANK( 'Visuals translations'[Translated text] , TRUE()) ," + "\r\n"); 97 | sb.Append("\t" + "\t" + "\t" + "\t" + "Filtered_translations )" + "\r\n"); 98 | sb.Append("\t" + "\t" + " , Reference_text" + "\r\n"); 99 | sb.Append("\t" + ")"); 100 | } 101 | VT_measure.Expression = sb.ToString(); 102 | VT_measure.Description = "This measure is auto generated by a Tabular Editor script. Do not modify it manually"; 103 | VT_measure.DisplayFolder = ""; 104 | VT_measure.SetAnnotation("VTAUTOGEN", "1"); // Set a special annotation on the measure. It might be useful in the future 105 | } 106 | } 107 | } else { 108 | Output("Visuals labels file doesn't exist. Please modify it at the top of the script"); 109 | } -------------------------------------------------------------------------------- /Intermediate/Create Parent-Child Hierarchy.csx: -------------------------------------------------------------------------------- 1 | // Title: Create Parent-Child Hierarchy.csx 2 | // 3 | // Author: Sergio Murru, https://sergiomurru.com 4 | // 5 | // Implementing Parent-Child hierarchies in DAX requires the usage of a special set of functions, PATH, PATHITEM and PATHLENGTH, specifically designed for this purpose. 6 | // These functions are used to flatten the hierarchy as as set of calculated columns. 7 | // This means that the depth of the hierarchy is fixed and limited to the number of calculated columns that was decided at design time. 8 | // This script to automatically generate these parent-child hierarchies. 9 | 10 | // *************************************************************************************************************** 11 | //the following variables are allowing controling the script 12 | 13 | // configuration start 14 | 15 | int levels = 4; 16 | 17 | string tableName = "Entity"; 18 | var table = Model.Tables[tableName]; 19 | 20 | string pathName = "EntityPath"; 21 | string keyName = "EntityKey"; 22 | string nameName = "EntityName"; 23 | string parentKeyName = "ParentEntityKey"; 24 | string levelNameFormat = "Level{0}"; 25 | string depthName = "Depth"; 26 | string rowDepthMeasureName = "EntityRowDepth"; 27 | string browseDepthMeasureName = "EntityBrowseDepth"; 28 | string wrapperMeasuresTableName = "StrategyPlan"; 29 | string hierarchyName = "Entities"; 30 | var wrapperMeasuresTable = Model.Tables[wrapperMeasuresTableName]; 31 | SortedDictionary measuresToWrap = 32 | new SortedDictionary 33 | { 34 | { "# Categories", "# Categories Base"}, 35 | { "Sum Amount", "Total Base" } 36 | }; 37 | // configuration end 38 | 39 | 40 | string daxPath = string.Format( "PATH({0}[{1}], {0}[{2}])", 41 | tableName, keyName, parentKeyName); 42 | 43 | 44 | // cleanup 45 | 46 | var hierarchiesCollection = table.Hierarchies.Where( 47 | m => m.Name == hierarchyName ); 48 | if (hierarchiesCollection.Count() > 0) 49 | { 50 | hierarchiesCollection.First().Delete(); 51 | } 52 | 53 | foreach (var wrapperMeasurePair in measuresToWrap) 54 | { 55 | var wrapperMeasuresCollection = wrapperMeasuresTable.Measures.Where( 56 | m => m.Name == wrapperMeasurePair.Value ); 57 | if (wrapperMeasuresCollection.Count() > 0) 58 | { 59 | wrapperMeasuresCollection.First().Delete(); 60 | } 61 | } 62 | 63 | var browseDepthMeasureCollection = table.Measures.Where( 64 | m => m.Name == browseDepthMeasureName ); 65 | if (browseDepthMeasureCollection.Count() > 0) 66 | { 67 | browseDepthMeasureCollection.First().Delete(); 68 | } 69 | 70 | var rowDepthMeasureCollection = table.Measures.Where( 71 | m => m.Name == rowDepthMeasureName ); 72 | if (rowDepthMeasureCollection.Count() > 0) 73 | { 74 | rowDepthMeasureCollection.First().Delete(); 75 | } 76 | 77 | var depthCollection = table.CalculatedColumns.Where( 78 | m => m.Name == depthName ); 79 | if (depthCollection.Count() > 0) 80 | { 81 | depthCollection.First().Delete(); 82 | } 83 | 84 | for (int i = 1; i <= levels; ++i) 85 | { 86 | string levelName = string.Format(levelNameFormat, i); 87 | var levelCalculatedColumnCollection = 88 | table.CalculatedColumns.Where( m => m.Name == levelName ); 89 | if (levelCalculatedColumnCollection.Count() > 0) 90 | { 91 | levelCalculatedColumnCollection.First().Delete(); 92 | } 93 | } 94 | 95 | var pathCalculatedColumnCollection = table.CalculatedColumns.Where( 96 | m => m.Name == pathName ); 97 | if (pathCalculatedColumnCollection.Count() > 0) 98 | { 99 | pathCalculatedColumnCollection.First().Delete(); 100 | } 101 | 102 | 103 | // create calculated columns 104 | table.AddCalculatedColumn(pathName, daxPath); 105 | 106 | string daxLevelFormat = 107 | @"VAR LevelNumber = {0} 108 | VAR LevelKey = PATHITEM( {1}[{2}], LevelNumber, INTEGER ) 109 | VAR LevelName = LOOKUPVALUE( {1}[{3}], {1}[{4}], LevelKey ) 110 | VAR Result = LevelName 111 | RETURN 112 | Result 113 | "; 114 | 115 | for (int i = 1; i <= levels; ++i) 116 | { 117 | string levelName = string.Format(levelNameFormat, i); 118 | string daxLevel = string.Format(daxLevelFormat, i, 119 | tableName, pathName, nameName, keyName); 120 | table.AddCalculatedColumn(levelName, daxLevel); 121 | } 122 | 123 | string daxDepthFormat = "PATHLENGTH( {0}[{1}] )"; 124 | string daxDepth = string.Format( 125 | daxDepthFormat, tableName, pathName); 126 | table.AddCalculatedColumn(depthName, daxDepth); 127 | 128 | 129 | // Create Hierarchy 130 | 131 | table.AddHierarchy(hierarchyName); 132 | for (int i = 1; i <= levels; ++i) 133 | { 134 | string levelName = string.Format(levelNameFormat, i); 135 | string daxLevel = string.Format(daxLevelFormat, i, 136 | tableName, pathName, nameName, keyName); 137 | table.Hierarchies[hierarchyName].AddLevel(levelName); 138 | } 139 | 140 | 141 | // Create measures 142 | string daxRowDepthMeasureFormat = "MAX( {0}[{1}])"; 143 | string daxRowDepthMeasure = string.Format( 144 | daxRowDepthMeasureFormat, tableName, depthName ); 145 | table.AddMeasure(rowDepthMeasureName, daxRowDepthMeasure); 146 | 147 | string daxBrowseDepthMeasure = ""; 148 | for (int i = 1; i <= levels; ++i) 149 | { 150 | string levelMeasureFormat = "ISINSCOPE( {0}[{1}] )"; 151 | string levelName = string.Format(levelNameFormat, i); 152 | daxBrowseDepthMeasure += string.Format( 153 | levelMeasureFormat, tableName, levelName); 154 | if (i < levels) 155 | { 156 | daxBrowseDepthMeasure += " + "; 157 | } 158 | } 159 | table.AddMeasure(browseDepthMeasureName, daxBrowseDepthMeasure); 160 | 161 | string daxWrapperMeasureFormat = 162 | @"VAR Val = [{0}] 163 | VAR ShowRow = [{1}] <= [{2}] 164 | VAR Result = IF( ShowRow, Val ) 165 | RETURN 166 | Result 167 | "; 168 | 169 | foreach (var wrapperMeasurePair in measuresToWrap) 170 | { 171 | string daxWrapperMeasure = string.Format(daxWrapperMeasureFormat, 172 | wrapperMeasurePair.Key, // measure to be wrapped 173 | browseDepthMeasureName, 174 | rowDepthMeasureName); 175 | wrapperMeasuresTable.AddMeasure(wrapperMeasurePair.Value, daxWrapperMeasure); 176 | 177 | } 178 | 179 | table.Measures.FormatDax(false); 180 | wrapperMeasuresTable.Measures.FormatDax(false); 181 | -------------------------------------------------------------------------------- /Intermediate/New Measure Choice Selector.csx: -------------------------------------------------------------------------------- 1 | 2 | // Create visuals translations measures 3 | // Author: Tommy Puglia powerbi.tips/podcast 4 | // This script creates a pop-up form that provides the calculation type and the format to apply to muliptple columns 5 | // You can use this to create new measures with currency, or even percentage. Choose Sum, Average, or Count. 6 | // Note that the form pop up is not pretty, but it works. 7 | 8 | 9 | #r "System.Drawing" 10 | 11 | using System.Drawing; 12 | using System.Windows.Forms; 13 | 14 | System.Windows.Forms.Form newForm = new System.Windows.Forms.Form(); 15 | 16 | System.Windows.Forms.Panel midPanel = new System.Windows.Forms.Panel(); 17 | System.Windows.Forms.Panel topMidPanel = new System.Windows.Forms.Panel(); 18 | System.Windows.Forms.RadioButton newmodelButton = new System.Windows.Forms.RadioButton(); 19 | System.Windows.Forms.RadioButton Measure = new System.Windows.Forms.RadioButton(); 20 | System.Windows.Forms.Button goButton = new System.Windows.Forms.Button(); 21 | System.Windows.Forms.TextBox enterTextBox = new System.Windows.Forms.TextBox(); 22 | System.Windows.Forms.ComboBox enterComboBox = new System.Windows.Forms.ComboBox(); 23 | System.Windows.Forms.ComboBox enterCurrency = new System.Windows.Forms.ComboBox(); 24 | System.Windows.Forms.Button JustMeasuresButton = new System.Windows.Forms.Button(); 25 | 26 | 27 | int startScreenX = 230; 28 | int startScreenY = 150; 29 | //EnterCombo 30 | // Enter Combo Box 31 | enterComboBox.Visible = true; 32 | enterComboBox.Size = new Size(100,40); 33 | enterComboBox.Location = new Point(startScreenX+44,startScreenY+44); 34 | enterComboBox.Items.Add("Average"); 35 | enterComboBox.Items.Add("Sum"); 36 | enterComboBox.Items.Add("CountRows"); 37 | enterComboBox.Items.Add("CountDistinct"); 38 | 39 | // Enter Combo Box 40 | enterCurrency.Visible = true; 41 | enterCurrency.Size = new Size(100,40); 42 | enterCurrency.Location = new Point(startScreenX+33,startScreenY+20); 43 | enterCurrency.Items.Add("$"); 44 | enterCurrency.Items.Add("Number"); 45 | enterCurrency.Items.Add("%"); 46 | 47 | //Choice 48 | 49 | //NE 50 | int formWidth = 1000; 51 | int formHeight = 700; 52 | newForm.TopMost = true; 53 | newForm.Text = "Create Measures Button"; 54 | newForm.Size = new Size(formWidth,formHeight); 55 | 56 | // New Model Button 57 | goButton.Size = new Size(100,25); 58 | goButton.Location = new Point(startScreenX+25,startScreenY+80); 59 | goButton.Text = "Go"; 60 | goButton.Visible = true; 61 | goButton.Enabled = true; 62 | 63 | // New Model Button 64 | JustMeasuresButton.Size = new Size(100,25); 65 | JustMeasuresButton.Location = new Point(startScreenX+15,startScreenY+60); 66 | JustMeasuresButton.Text = "Just Measures"; 67 | JustMeasuresButton.Visible = true; 68 | JustMeasuresButton.Enabled = true; 69 | 70 | string calType = string.Empty; 71 | string forType = string.Empty; 72 | string formatString = string.Empty; 73 | 74 | goButton.Click += (sender2, e2) => { 75 | 76 | calType = enterComboBox.Text; 77 | forType = enterCurrency.Text; 78 | if( forType == "$") 79 | { 80 | formatString = "$ #,##0"; 81 | } 82 | else if (forType == "%") 83 | { 84 | formatString = "0.0%"; 85 | } 86 | else 87 | { 88 | formatString = "#,0"; 89 | 90 | } 91 | if(calType == "Average") 92 | { 93 | 94 | foreach(var c in Selected.Columns) 95 | { 96 | var newMeasure = c.Table.AddMeasure( 97 | "Avg. " + c.Name, // Name 98 | "Average(" + c.DaxObjectFullName + ")" // DAX expression 99 | // Display Folder 100 | ); 101 | newMeasure.FormatString = formatString; 102 | c.IsHidden = true; 103 | newMeasure.DisplayFolder = "_KPI"; 104 | } 105 | } 106 | else if (calType == "Sum") 107 | { 108 | foreach(var c in Selected.Columns) 109 | { 110 | var newMeasure = c.Table.AddMeasure( 111 | "Total " + c.Name, // Name 112 | "SUM(" + c.DaxObjectFullName + ")" 113 | // DAX expression 114 | // Display Folder 115 | ); 116 | 117 | newMeasure.FormatString = formatString; 118 | newMeasure.DisplayFolder = "_KPI"; 119 | 120 | c.IsHidden = true; 121 | 122 | } 123 | } 124 | else if (calType == "CountRows") 125 | { 126 | foreach(var c in Selected.Columns) 127 | { 128 | var newMeasure = c.Table.AddMeasure( 129 | "Count of " + c.Name, // Name 130 | "COUNTROWS(" + c.Table + ")" 131 | // DAX expression 132 | // Display Folder 133 | ); 134 | 135 | newMeasure.FormatString = formatString; 136 | newMeasure.DisplayFolder = "_KPI"; 137 | 138 | c.IsHidden = true; 139 | 140 | } 141 | } 142 | else 143 | { 144 | foreach(var c in Selected.Columns) 145 | { 146 | var newMeasure = c.Table.AddMeasure( 147 | "Distinct " + c.Name, // Name 148 | "DISTINCTCOUNT(" + c.DaxObjectFullName + ")" 149 | // DAX expression 150 | // Display Folder 151 | ); 152 | 153 | newMeasure.FormatString = formatString; 154 | newMeasure.DisplayFolder = "_KPI"; 155 | 156 | c.IsHidden = true; 157 | 158 | } 159 | } 160 | }; 161 | JustMeasuresButton.Click += (sender3, e3) => { 162 | 163 | 164 | forType = enterCurrency.Text; 165 | if( forType == "$") 166 | { 167 | formatString = "$ #,##0"; 168 | } 169 | else if (forType == "%") 170 | { 171 | formatString = "0.0%"; 172 | } 173 | else 174 | { 175 | formatString = "#,0"; 176 | 177 | } 178 | 179 | foreach(var c in Selected.Measures) 180 | { 181 | 182 | c.FormatString = formatString; 183 | } 184 | }; 185 | 186 | 187 | 188 | 189 | string perspName = string.Empty; 190 | 191 | newForm.Controls.Add(midPanel); 192 | newForm.Controls.Add(topMidPanel); 193 | newForm.Controls.Add(goButton); 194 | newForm.Controls.Add(enterTextBox); 195 | newForm.Controls.Add(Measure); 196 | newForm.Controls.Add(newmodelButton); 197 | newForm.Controls.Add(enterComboBox); 198 | newForm.Controls.Add(enterCurrency); 199 | newForm.Controls.Add(JustMeasuresButton); 200 | newForm.Show(); -------------------------------------------------------------------------------- /Basic/Format All Power Query Tables.csx: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Author: Tommy Puglia 4 | // Original Author: Kurt Buhler 5 | // Much Thanks goes to Kurt based on this article on a selected partition, only updated I made was all m partitions in the model. 6 | 7 | // Please see Kurt's original article: 8 | // https://data-goblins.com/power-bi/format-power-query-automatically 9 | 10 | 11 | // Check out the Explicit Measures Podcast on Apple, spotify, & YouTube! 12 | // https://www.youtube.com/playlist?list=PLn1m_aBmgsbHr83c1P6uqaWF5PLdFzOjj 13 | // 14 | // Format all Power Query tables in the model 15 | // 16 | // Simply run this on on the model, and done! 17 | 18 | #r "C:\Program Files\Tabular Editor 3\TabularEditor3.exe" // *** Needed for C# scripting, remove in TE3 *** 19 | #r "C:\Program Files (x86)\Tabular Editor 3\TabularEditor3.exe" // *** Needed for C# scripting, remove in TE3 *** 20 | 21 | using TabularEditor.TOMWrapper; // *** Needed for C# scripting, remove in TE3 *** 22 | using TabularEditor.Scripting; // *** Needed for C# scripting, remove in TE3 *** 23 | using System.Net.Http; 24 | using System.Net.Http.Headers; 25 | using System.Text; 26 | using Newtonsoft.Json; 27 | using Newtonsoft.Json.Linq; 28 | 29 | Model Model; // *** Needed for C# scripting, remove in TE3 *** 30 | TabularEditor.Shared.Interaction.Selection Selected; // *** Needed for C# scripting, remove in TE3 *** 31 | 32 | 33 | // URL of the powerqueryformatter.com API 34 | string powerqueryformatterAPI = "https://m-formatter.azurewebsites.net/api/v2"; 35 | 36 | // HttpClient method to initiate the API call POST method for the URL 37 | HttpClient client = new HttpClient(); 38 | HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, powerqueryformatterAPI); 39 | 40 | int _partitions = 0; 41 | int _whatifparameters = 0; 42 | int _fieldparameters = 0; 43 | foreach ( var _table in Model.Tables ) 44 | { 45 |   foreach ( var _partition in _table.Partitions ) 46 |   { 47 | string _type = Convert.ToString(_partition.SourceType); 48 | string _exp = Convert.ToString(_partition.Expression); 49 | if ( _type == "M" ) 50 |   { 51 | _partitions = _partitions + 1; 52 | string partitionExpression = _partition.Expression; 53 | 54 | 55 | var requestBody = JsonConvert.SerializeObject( 56 | new { 57 | code = partitionExpression, // Mandatory config 58 | resultType = "text", // Mandatory config 59 | lineWidth = 40 // Optional config 60 | // alignLineCommentsToPosition = true, // Optional config 61 | // includeComments = true // Optional config 62 | }); 63 | 64 | // Set the "Content-Type" header of the request to "application/json" and the encoding to UTF-8 65 | var content = new StringContent(requestBody, Encoding.UTF8, "application/json"); 66 | content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); 67 | 68 | // Retrieve the response 69 | var response = client.PostAsync(powerqueryformatterAPI, content).Result; 70 | 71 | // If the response is successful 72 | if (response.IsSuccessStatusCode) 73 | { 74 | // Get the result of the response 75 | var result = response.Content.ReadAsStringAsync().Result; 76 | 77 | // Parse the response JSON object from the string 78 | JObject data = JObject.Parse(result.ToString()); 79 | 80 | // Get the formatted Power Query response 81 | string formattedPowerQuery = (string)data["result"]; 82 | 83 | ////////////////////////////////////////////////////////////////////////// Can remove everything in this section 84 | // OPTIONAL MANUAL FORMATTING // Additional formatting on top of API config 85 | // Manually add a new line and comment to each step // 86 | var replace = new Dictionary // 87 | { // 88 | { "\n//", "\n\n//" }, // New line at comment 89 | { "\n #", "\n\n // Step\n #" }, // New line & comment at new standard step 90 | { "\n Source", "\n\n // Data Source\n Source" }, // New line & comment at Source step 91 | { "\n Dataflow", "\n\n // Dataflow Connection Info\n Dataflow" },// New line & comment at Dataflow step 92 | {"\n Data =", "\n\n // Step\n Data ="}, // New line & comment at Data step 93 | {"\n Navigation =", "\n\n // Step\n Navigation ="}, // New line & comment at Navigation step 94 | {"in\n\n // Step\n #", "in\n #"}, // 95 | {"\nin", "\n\n// Result\nin"} // Format final step as result 96 | }; // 97 | // 98 | // Replace the first string in the dictionary with the second // 99 | var manuallyformattedPowerQuery = replace.Aggregate( // 100 | formattedPowerQuery, // 101 | (before, after) => before.Replace(after.Key, after.Value)); // 102 | // 103 | // Replace the auto-formatted code with the manually formatted version // 104 | formattedPowerQuery = manuallyformattedPowerQuery; // 105 | ////////////////////////////////////////////////////////////////////////// 106 | 107 | // Replace the unformatted M expression with the formatted expression 108 | _partition.Expression = formattedPowerQuery; 109 | 110 | // Pop-up to inform of completion 111 | 112 | } 113 | 114 | // Otherwise return an error message 115 | else 116 | { 117 | Info( 118 | "API call unsuccessful." + 119 | "\nCheck that you are selecting a partition with a valid M Expression." 120 | ); 121 | } 122 | 123 | 124 | 125 | 126 | 127 | } 128 | else if ( _type == "Calculated" && _exp.Contains("NAMEOF") ) 129 | { 130 | _fieldparameters = _fieldparameters + 1; 131 | } 132 | else if ( _type == "Calculated" && _exp.Contains("GENERATESERIES") ) 133 | { 134 | _whatifparameters = _whatifparameters + 1; 135 | } 136 | 137 |    } 138 | } 139 | -------------------------------------------------------------------------------- /Intermediate/Dynamic YtD same period.csx: -------------------------------------------------------------------------------- 1 | #r "Microsoft.VisualBasic" 2 | using Microsoft.VisualBasic; 3 | // 4 | // 5 | // by Tommy Puglia 6 | // twitter: @tommypuglia 7 | // pugliabi.com 8 | // 9 | // REFERENCE: 10 | // based on this article https://www.daxpatterns.com/standard-time-related-calculations/ 11 | // 12 | // use this to create YtD, PY YtD, and YOY based on same period in the current year 13 | 14 | //AT LEAST ONE MEASURE HAS TO BE AFFECTED!, 15 | //either by selecting it or typing its name in the preSelectedMeasures Variable 16 | 17 | string DisplayFolderName = Interaction.InputBox("Provide the name of the Display Folder", "Display", "KPI"); 18 | if (DisplayFolderName == "") return; 19 | 20 | string[] preSelectedMeasures = {}; //include measure names in double quotes, like: {"Profit","Total Cost"}; 21 | string prefactTableName = "afactKiboMerge"; 22 | string prefactTableDateColumnName = "PaymentDate"; 23 | string predateTableName = "DimDateDF"; 24 | string predateTableDateColumnName = "FullDate"; 25 | string predateTableYearColumnName = "Calendar Year"; 26 | string affectedMeasures = "{"; 27 | string[] AllDATES = { 28 | prefactTableName, 29 | prefactTableDateColumnName, 30 | predateTableName, 31 | predateTableDateColumnName, 32 | predateTableYearColumnName 33 | }; 34 | string factTableName = prefactTableName; 35 | string dateTableName = predateTableName; 36 | string factTableDateColumnName = prefactTableDateColumnName; 37 | string dateTableDateColumnName = predateTableDateColumnName; 38 | string dateTableYearColumnName = predateTableYearColumnName; 39 | string ADYname = "ADY"; 40 | string ShowValueForDatesMeasureName = "ShowValueForDates"; 41 | string dateTableNameV = dateTableName; 42 | 43 | string ValueSelected = Interaction.InputBox("Choose if you want to Select", "Select Columns", "Y"); 44 | string ValueSelected = "N"; 45 | if (ValueSelected == "N") { 46 | var factTablea = Model.Tables[factTableName]; 47 | var dateTablea = Model.Tables[dateTableName]; 48 | 49 | var = Model.Tables[factTableName].Columns[factTableDateColumnName]; 50 | 51 | var dateTableDateColumn = Model.Tables[dateTableName].Columns[dateTableDateColumnName]; 52 | var dateTableYearColumn = Model.Tables[dateTableName].Columns[dateTableYearColumnName]; 53 | } else { 54 | // Select Tables 55 | var factTablen = SelectTable(label: "Select your fact table"); 56 | if (factTablen == null) return; 57 | var factTableDateColumn = SelectColumn(factTablen.Columns, label: "Select the main date column"); 58 | if (factTableDateColumn == null) return; 59 | 60 | var dateTablen = SelectTable(label: "Select your date table"); 61 | if (dateTablen == null) { 62 | Error("You just aborted the script"); 63 | return; 64 | }; 65 | 66 | var dateTableDateColumn = SelectColumn(dateTablen.Columns, label: "Select the date column"); 67 | if (dateTableDateColumn == null) { 68 | Error("You just aborted the script"); 69 | return; 70 | }; 71 | var dateTableYearColumn = SelectColumn(dateTablen.Columns, label: "Select the year column"); 72 | if (dateTableYearColumn == null) return; 73 | factTableName = factTablen.Name; 74 | factTableDateColumnName = factTableDateColumn.Name; 75 | dateTableName = dateTablen.Name; 76 | dateTableDateColumnName = dateTableDateColumn.Name; 77 | dateTableYearColumnName = dateTableYearColumn.Name; 78 | }; 79 | 80 | var factDateColumnWithTableNA = Model.Tables[factTableName].Columns[factTableDateColumnName]; 81 | var dateTable = Model.Tables[dateTableName]; 82 | var factTable = Model.Tables[factTableName]; 83 | var dateColumnWithTableNA = Model.Tables[dateTableName].Columns[dateTableDateColumnName]; 84 | var dateColumnWithTable = dateColumnWithTableNA.DaxObjectFullName; 85 | var factDateColumnWithTable = factDateColumnWithTableNA.DaxObjectFullName; 86 | 87 | string dateWithSalesColumnName = "DateWith" + factTable.Name; 88 | string DateWithSalesCalculatedColumnExpression = dateColumnWithTable + " <= MAX ( " + factDateColumnWithTable + ")"; 89 | string ShowValueForDatesMeasureExpression = "VAR LastDateWithData = " + " CALCULATE ( " + " MAX ( " + factDateColumnWithTable + " ), " + " REMOVEFILTERS () " + " )" + "VAR FirstDateVisible = " + " MIN ( " + dateColumnWithTable + " ) " + "VAR Result = " + " FirstDateVisible <= LastDateWithData " + "RETURN " + " Result "; 90 | string ShowADY = "VAR _LSD = " + " MAX ( " + factDateColumnWithTable + " ) " + " VAR _LSDPY = " + " EDATE( " + " _LSD, " + " - 12 " + " )" + " RETURN " + " " + dateColumnWithTable + "<= _LSDPY "; 91 | 92 | if (!Model.Tables[dateTableName].Columns.Contains(ADYname)) { 93 | var ShowValueForDatesADY = dateTable.AddCalculatedColumn(ADYname, ShowADY); 94 | 95 | ShowValueForDatesADY.FormatDax(); 96 | }; 97 | if (!Model.Tables[dateTableName].Columns.Contains(dateWithSalesColumnName)) { 98 | 99 | var AddDateSales = dateTable.AddCalculatedColumn(dateWithSalesColumnName, DateWithSalesCalculatedColumnExpression); 100 | AddDateSales.FormatDax(); 101 | } 102 | if (!Model.Tables[dateTableName].Measures.Contains(ShowValueForDatesMeasureName)) { 103 | var ShowValueForDatesMeasure = dateTable.AddMeasure(ShowValueForDatesMeasureName, ShowValueForDatesMeasureExpression); 104 | 105 | ShowValueForDatesMeasure.FormatDax(); 106 | }; 107 | 108 | string YTDName = " YtD"; 109 | string PYYTDName = " PY YtD"; 110 | string YOYName = " YoY"; 111 | 112 | string YTDExpressionFirstPart = "Calculate( "; 113 | string YTDEXPRENextPart = " , DATESYTD(" + dateColumnWithTable + "))"; 114 | 115 | string PYTEXPFirst = " VAR LastDaySelection = " + " LASTNONBLANK( " + factDateColumnWithTable + ", "; 116 | string firstBracket = "["; 117 | string PYLMEA = "]"; 118 | string FinishPY = ")" + " VAR CurrentRange = " + " DATESBETWEEN( " + dateColumnWithTable + ", " + " MIN( " + factDateColumnWithTable + " ) , " + " LastDaySelection )" + " VAR PreviousRange = " + " SAMEPERIODLASTYEAR( CurrentRange ) " + "RETURN " + " IF(LastDaySelection " + " >= MIN( " + dateColumnWithTable + " )," + " CALCULATE( " + firstBracket; 119 | string FinalPiech = PYLMEA + " , " + " PreviousRange, " + " " + dateTableName + "[ADY] = TRUE ))"; 120 | 121 | foreach(var mdd in Selected.Measures) { 122 | string df = "__" + DisplayFolderName; 123 | var MESN = mdd.Table.AddMeasure( 124 | mdd.Name + YTDName, YTDExpressionFirstPart + mdd.DaxObjectName + YTDEXPRENextPart); 125 | var PYTN = mdd.Table.AddMeasure( 126 | mdd.Name + PYYTDName, PYTEXPFirst + firstBracket + MESN.Name + PYLMEA + FinishPY + mdd.Name + FinalPiech); 127 | var YOYNNN = mdd.Table.AddMeasure( 128 | mdd.Name + YOYName, "Divide((" + MESN.DaxObjectName + " - " + PYTN.DaxObjectName + "), " + PYTN.DaxObjectName + ")"); 129 | MESN.DisplayFolder = df; 130 | PYTN.DisplayFolder = df; 131 | YOYNNN.DisplayFolder = df; 132 | MESN.FormatDax(); 133 | PYTN.FormatDax(); 134 | YOYNNN.FormatDax(); 135 | }; 136 | -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Measure in Calc Item.csx: -------------------------------------------------------------------------------- 1 | #r "Microsoft.VisualBasic" 2 | using Microsoft.VisualBasic; 3 | 4 | // CHANGE LOG: 5 | // '2021-10-20 / B.Agullo / 6 | // '2021-11-22 / B.Agullo / Totally rewrote the script as did not work the way it was 7 | 8 | 9 | // Instructions: 10 | // select the calculation items and and the measure(s) for which the calculation should be shown. 11 | // the first time you will be asked to introduce a name for your calc group and dummy measure 12 | // second time on it will reuse the same group and dummy measure to add new calc items 13 | // the script will create a new calc item for eachs elected measure 14 | // A pop-up will show if the measure has been previously selected and thus already has a calculation item with its name 15 | 16 | 17 | // 18 | // ----- do not modify script below this line ----- 19 | // 20 | 21 | string affectedMeasures = ""; 22 | string selectedCalcItems = ""; 23 | string selectedCalcItemsCalcGroupName = ""; 24 | 25 | CalculationGroup selectedCalculationGroup = null as CalculationGroup; 26 | Column selectedCalcItemsCalcGroupColumn = null as Column; 27 | 28 | if (Selected.Measures.Count == 0) { 29 | 30 | Error("No measures selected"); 31 | return; 32 | 33 | } else if (Selected.CalculationItems.Count == 0) { 34 | 35 | Error("No calculation items selected"); 36 | return; 37 | 38 | } else { 39 | 40 | // create in-line table with selected calc item names 41 | foreach(var ci in Selected.CalculationItems) { 42 | 43 | if(selectedCalcItems == "") { 44 | selectedCalcItems += "{\"" + ci.Name + "\""; 45 | 46 | } else { 47 | 48 | selectedCalcItems += ",\"" + ci.Name + "\""; 49 | 50 | }; 51 | 52 | if(selectedCalcItemsCalcGroupName == "") { 53 | 54 | //selectedCalculationGroup = ci.CalculationGroupTable; 55 | selectedCalcItemsCalcGroupName = ci.CalculationGroupTable.Name; 56 | 57 | }; 58 | 59 | if(selectedCalcItemsCalcGroupColumn == null) { 60 | 61 | //get the only column that is not "ordinal" 62 | selectedCalcItemsCalcGroupColumn = (ci.CalculationGroupTable as Table).Columns.Where(x => x.Name != "Ordinal").First(); 63 | 64 | }; 65 | }; 66 | 67 | selectedCalcItems += "}"; 68 | 69 | }; 70 | 71 | string calcGroupTag = "Dynamic Measure Calculation Group For Arbitrary 2-row Header"; 72 | string dummyMeasureTag = "Dummy Measure for the Dynamic Measure Calculation Group For Arbitrary 2-row Header"; 73 | 74 | //dynamic Measure CG for 2 row header 75 | var DynamicMeasureCGs = Model.Tables.Where(x => x.GetAnnotation("@AgulloBernat") == calcGroupTag); 76 | 77 | var DynamicMeasureCG = null as CalculationGroupTable; 78 | 79 | if (DynamicMeasureCGs.Count() == 1 ) { 80 | 81 | DynamicMeasureCG = DynamicMeasureCGs.First() as CalculationGroupTable; 82 | 83 | } else if (DynamicMeasureCGs.Count() < 1) { 84 | 85 | string calcGroupName = 86 | Interaction.InputBox( 87 | "Provide a name for your Dynamic Measure Calculation Group For Arbitrary 2-row Header", 88 | "Calculation Group Name", "", 740, 400 89 | ); 90 | 91 | if(calcGroupName == "") { 92 | 93 | Error("No name provided"); 94 | return; 95 | 96 | }; 97 | 98 | DynamicMeasureCG = Model.AddCalculationGroup(calcGroupName); 99 | DynamicMeasureCG.Description = 100 | "Under this calc group only certain calculation items of " 101 | + selectedCalcItemsCalcGroupName 102 | + " calculation group will be visible and with a certain measure. See calculation items for details"; 103 | 104 | DynamicMeasureCG.SetAnnotation("@AgulloBernat",calcGroupTag); 105 | Model.Tables[calcGroupName].Columns["Name"].Name = calcGroupName; 106 | 107 | } else { 108 | 109 | //this should never happen -- 110 | DynamicMeasureCG = SelectTable(DynamicMeasureCGs, label:"Select your Dynamic Measure Calculation Group For Arbitrary 2-row Header") as CalculationGroupTable; 111 | 112 | }; 113 | 114 | if (DynamicMeasureCG == null) { return; } // doesn't work in TE3 as cancel button doesn't return null in TE3 115 | 116 | //INIT DUMMY MEASURE (if necessary) 117 | var dummyMeasureCandidates = DynamicMeasureCG.Measures.Where(x => x.GetAnnotation("@AgulloBernat") == dummyMeasureTag); 118 | Measure dummyMeasure = null as Measure; 119 | 120 | if (dummyMeasureCandidates.Count() == 1) { 121 | 122 | dummyMeasure = dummyMeasureCandidates.First() as Measure; 123 | 124 | } else if (dummyMeasureCandidates.Count() < 1) { 125 | 126 | string dummyMeasureName = Interaction.InputBox("Enter a name for the dummy measure", "Measure Name", "", 740, 400); 127 | 128 | if(dummyMeasureName == "") { 129 | 130 | Error("No name provided"); 131 | return; 132 | 133 | }; 134 | 135 | //add dummy measure if not present yet 136 | if(!DynamicMeasureCG.Measures.Where(m => m.Name == dummyMeasureName).Any()) { 137 | 138 | dummyMeasure = DynamicMeasureCG.AddMeasure(dummyMeasureName,"0"); 139 | 140 | } else { 141 | 142 | //get the reference if already exists 143 | dummyMeasure = DynamicMeasureCG.Measures.Where(m => m.Name == dummyMeasureName).First(); 144 | 145 | }; 146 | 147 | //in any case add the annotation so it will be found next time 148 | dummyMeasure.SetAnnotation("@AgulloBernat",dummyMeasureTag); 149 | 150 | } else { 151 | 152 | dummyMeasure = 153 | SelectMeasure( 154 | dummyMeasureCandidates, 155 | label:"Select your Dummy Measure for your Dynamic Measure Calculation Group For Arbitrary 2-row Header" 156 | ) as Measure; 157 | 158 | } ; 159 | 160 | foreach (var m in Selected.Measures) { 161 | 162 | if (!DynamicMeasureCG.CalculationItems.Where(x => x.Name == m.Name).Any()) { 163 | 164 | string newCalcItemName = m.Name; 165 | string newCalcItemExpression = 166 | "IF(" + 167 | " ISSELECTEDMEASURE( [" + dummyMeasure.Name + "] )," + 168 | " VAR currentCalcItem =" + 169 | " SELECTEDVALUE( "+ selectedCalcItemsCalcGroupColumn.DaxObjectFullName +", \"NO SELECTION\" )" + 170 | " RETURN" + 171 | " IF( currentCalcItem IN " + selectedCalcItems + ", [" + m.Name + "] )," + 172 | " SELECTEDMEASURE()" + 173 | ")"; 174 | 175 | CalculationItem newCalcItem = DynamicMeasureCG.AddCalculationItem(newCalcItemName, newCalcItemExpression); 176 | newCalcItem.FormatDax(); 177 | 178 | } else { 179 | 180 | Info("Calculation item " + m.Name + " already present. Please modify manually, or delete it and try again"); 181 | 182 | }; 183 | 184 | }; 185 | 186 | CallDaxFormatter(); 187 | -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Dynamic Measure Calculation Group Script.csx: -------------------------------------------------------------------------------- 1 | #r "Microsoft.VisualBasic" 2 | using Microsoft.VisualBasic; 3 | 4 | // by Johnny Winter 5 | // www.greyskullanalytics.com 6 | // '2021-10-15 / B.Agullo / dynamic parameters by B.Agullo / 7 | // '2022-05-14 / B.Agullo / you can now rerun the script to simply add new measures or update existing ones 8 | // '2022-07-11 / B.Agullo / no check on the number of measures selected so it can be launched just to create the calc group table 9 | 10 | // Instructions: 11 | //select the measures you want to add to your Dynamic Measure and then run this script (or store it as macro) 12 | 13 | // 14 | // ----- do not modify script below this line ----- 15 | // 16 | 17 | string dynamicMeasureCgTag = "@GreyskullPBI"; 18 | string dynamicMeasureCgValue = "Dynamic Measure Calculation Group"; 19 | 20 | string dummyMeasureTag = dynamicMeasureCgTag; 21 | string dummyMeasureValue = "Dummy Measure"; 22 | 23 | string calcGroupName = ""; 24 | string columnName = ""; 25 | string measureName = ""; 26 | string secondaryMeasureName = ""; 27 | string conditionalFormatMeasureName = ""; 28 | 29 | Measure dummyMeasure = null as Measure; 30 | 31 | var dynamicCGs = Model.Tables.Where(x => x.GetAnnotation(dynamicMeasureCgTag) == dynamicMeasureCgValue); 32 | 33 | CalculationGroupTable cgTable = null as CalculationGroupTable; 34 | // CalculationGroup cg = null as CalculationGroup; 35 | 36 | if(dynamicCGs.Count() == 1) 37 | { 38 | //reuse the calc group 39 | cgTable = dynamicCGs.First() as CalculationGroupTable; 40 | 41 | } 42 | else if (dynamicCGs.Count() < 1) 43 | { 44 | //create the calc group 45 | calcGroupName = Interaction.InputBox("Provide a name for your Calc Group", "Calc Group Name", "Dynamic Measure", 740, 400); 46 | if (calcGroupName == "") return; 47 | 48 | columnName = Interaction.InputBox("Calc Group column name", "Column Name", calcGroupName, 740, 400); 49 | if (columnName == "") return; 50 | 51 | //check to see if a table with this name already exists 52 | //if it doesnt exist, create a calculation group with this name 53 | if (!Model.Tables.Contains(calcGroupName)) 54 | { 55 | cgTable = Model.AddCalculationGroup(calcGroupName); 56 | cgTable.Description = "Contains dynamic measures and a column called " + columnName + ". The contents of the dynamic measures can be controlled by selecting values from " + columnName + "."; 57 | }; 58 | 59 | //set variable for the calc group 60 | Table calcGroup = Model.Tables[calcGroupName]; 61 | 62 | //if table already exists, make sure it is a Calculation Group type 63 | if (calcGroup.SourceType.ToString() != "CalculationGroup") 64 | { 65 | Error("Table exists in Model but is not a Calculation Group. Rename the existing table or choose an alternative name for your Calculation Group."); 66 | return; 67 | }; 68 | 69 | //apply the annotation so the user is not asked again 70 | cgTable = calcGroup as CalculationGroupTable; 71 | cgTable.SetAnnotation(dynamicMeasureCgTag, dynamicMeasureCgValue); 72 | 73 | //by default the calc group has a column called Name. If this column is still called Name change this in line with specfied variable 74 | if (cgTable.Columns.Contains("Name")) 75 | { 76 | cgTable.Columns["Name"].Name = columnName; 77 | }; 78 | cgTable.Columns[columnName].Description = "Select value(s) from this column to control the contents of the dynamic measures."; 79 | 80 | } 81 | else 82 | { 83 | //make them choose the calc group -- should not happen! 84 | cgTable = SelectTable(dynamicCGs, label: "Select your Dynamic Measure Calculation Group For Arbitrary 2-row Header") as CalculationGroupTable; 85 | } 86 | 87 | //get the column name in case the calc group was already there 88 | columnName = cgTable.Columns.Where(x => x.Name != "Ordinal").First().Name; 89 | 90 | var dummyMeasures = Model.AllMeasures.Where(x => x.GetAnnotation(dummyMeasureTag) == dummyMeasureValue); 91 | 92 | if (dummyMeasures.Count() == 1) 93 | { 94 | //get the measure 95 | measureName = dummyMeasures.First().Name; 96 | 97 | } 98 | else if (dummyMeasures.Count() < 1) 99 | { 100 | //create the measure 101 | measureName = Interaction.InputBox("Dynamic Measure Name (cannot be named \"" + columnName + "\")", "Measure Name", "Dummy", 740, 400); 102 | if (measureName == "") return; 103 | 104 | } 105 | else 106 | { 107 | //choose measure (should not happen!) 108 | measureName = SelectMeasure(dummyMeasures).Name; 109 | }; 110 | 111 | secondaryMeasureName = measureName + " 2"; 112 | conditionalFormatMeasureName = measureName + " CF"; 113 | 114 | //check to see if dynamic measure has been created, if not create it now 115 | //if a measure with that name alredy exists elsewhere in the model, throw an error 116 | if (!cgTable.Measures.Contains(measureName)) 117 | { 118 | dummyMeasure = cgTable.AddMeasure(measureName, "BLANK()"); 119 | dummyMeasure.Description = "Control the content of this measure by selecting values from " + columnName + "."; 120 | dummyMeasure.SetAnnotation(dummyMeasureTag, dummyMeasureValue); 121 | }; 122 | 123 | if (!cgTable.Measures.Contains(secondaryMeasureName)) 124 | { 125 | dummyMeasure = cgTable.AddMeasure(secondaryMeasureName, "BLANK()"); 126 | dummyMeasure.Description = "Control the content of this measure by selecting values from " + columnName + ". Secondary dynamic measure for complex use cases"; 127 | }; 128 | 129 | if (!cgTable.Measures.Contains(conditionalFormatMeasureName)) 130 | { 131 | dummyMeasure = cgTable.AddMeasure(conditionalFormatMeasureName, "BLANK()"); 132 | dummyMeasure.Description = "Control the content of this measure by selecting values from " + columnName + ". Used this measure for conditional format purposes"; 133 | }; 134 | 135 | 136 | 137 | string isSelectedMeasureString = "[" + measureName + "],[" + secondaryMeasureName + "],[" + conditionalFormatMeasureName + "]"; 138 | 139 | //if no measures were selected that's the end of the story 140 | //(only makes sense if being launched from another script and soon after will be launched against one or more measures) 141 | if (Selected.Measures.Count == 0) 142 | { 143 | return; 144 | } 145 | foreach (var m in Selected.Measures) 146 | { 147 | 148 | //remove calculation item if already exists 149 | if (cgTable.CalculationItems.Contains(m.Name)) { 150 | cgTable.CalculationItems[m.Name].Delete(); 151 | }; 152 | 153 | //if (!cg.CalculationItems.Contains(m.Name)) 154 | //{ 155 | 156 | var newCalcItem = 157 | cgTable.AddCalculationItem( 158 | m.Name, 159 | "IF ( " + "ISSELECTEDMEASURE (" + isSelectedMeasureString + "), " + "[" + m.Name + "], " + "SELECTEDMEASURE() )" 160 | ); 161 | 162 | // '2021-10-15 / B.Agullo / double quotes in format string need to be doubled to be preserved 163 | newCalcItem.FormatStringExpression = "IF ( " + "ISSELECTEDMEASURE (" + isSelectedMeasureString + "),\"" + m.FormatString.Replace("\"", "\"\"") + "\", SELECTEDMEASUREFORMATSTRING() )"; 164 | newCalcItem.FormatDax(); 165 | 166 | 167 | //}; 168 | }; -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Data Problems Button.csx: -------------------------------------------------------------------------------- 1 | #r "Microsoft.VisualBasic" 2 | using Microsoft.VisualBasic; 3 | 4 | 5 | // '2021-05-26 / B.Agullo / 6 | // '2021-10-13 / B.Agullo / dynamic parameters for one-click operation 7 | // '2022-10-18 / B.Agullo / Bug fixes 8 | // by Bernat Agulló 9 | // www.esbrina-ba.com 10 | 11 | // Instructions: 12 | // select the measures that counts the number of "data problems" the model has and then run the script or as macro 13 | // when adding macro select measure context for execution 14 | 15 | // 16 | // ----- do not modify script below this line ----- 17 | // 18 | 19 | 20 | if (Selected.Measures.Count != 1) { 21 | Error("Select one and only one measure"); 22 | return; 23 | }; 24 | 25 | string navigationTableName = Interaction.InputBox("Provide a name for navigation measures table name", "Navigation Table Name", "Navigation", 740, 400); 26 | if(navigationTableName == "") return; 27 | 28 | if(Model.Tables.Any(Table => Table.Name == navigationTableName)) { 29 | Error(navigationTableName + " already exists!"); 30 | return; 31 | }; 32 | 33 | string buttonTextMeasureName = Interaction.InputBox("Name for your button text measure", "Button text measure name", "Button Text", 740, 400); 34 | if(buttonTextMeasureName == "") return; 35 | 36 | string buttonTextPattern = Interaction.InputBox("Provide a pattern for your button text", "Button text pattern (# = no. of problems)", "There are # data problems", 740, 400); 37 | if(buttonTextPattern == "") return; 38 | 39 | string buttonBackgroundMeasureName = Interaction.InputBox("Name your button background measure", "Button Background Measure", "Button Background", 740, 400); 40 | if(buttonBackgroundMeasureName == "") return; 41 | 42 | string buttonNavigationMeasureName = Interaction.InputBox("Name your button navigation measure", "Button Navigation Measure", "Button Navigation", 740, 400); 43 | if(buttonNavigationMeasureName == "") return; 44 | 45 | string thereAreDataProblemsMeasureName = Interaction.InputBox("Name your data problems flag measure", "Data problems Flag Measure", "There are Data Problems", 740, 400); 46 | if(thereAreDataProblemsMeasureName == "") return; 47 | 48 | string dataProblemsSheetName = Interaction.InputBox("Where are the data problems detail?", "Data problems Sheet", "Data Problems", 740, 400); 49 | if(dataProblemsSheetName == "") return; 50 | 51 | 52 | // colors will be created if not present 53 | string buttonColorMeasureNameWhenVisible = Interaction.InputBox("What's the color measure name when the button is visible?", "Visible color measure name", "Warning Color", 740, 400); 54 | if(buttonColorMeasureNameWhenVisible == "") return; 55 | 56 | string buttonColorMeasureValueWhenVisible = Interaction.InputBox("What's the color code of " + buttonColorMeasureNameWhenVisible + "?", "Visible color code", "#D64554", 740, 400); 57 | if(buttonColorMeasureValueWhenVisible == "") return; 58 | buttonColorMeasureValueWhenVisible = "\"" + buttonColorMeasureValueWhenVisible + "\""; 59 | 60 | string buttonColorMeasureNameWhenInvisible = Interaction.InputBox("What's the color measure name when button is invisible?", "Invisible color measure name", "Report Background Color", 740, 400); 61 | if(buttonColorMeasureNameWhenInvisible == "") return; 62 | 63 | string buttonColorMeasureValueWhenInvisible = Interaction.InputBox("What's the color code of " + buttonColorMeasureNameWhenInvisible + "?", "Invisible color measure name", "#FFFFFF", 740, 400); 64 | if(buttonColorMeasureValueWhenInvisible == "") return; 65 | buttonColorMeasureValueWhenInvisible = "\"" + buttonColorMeasureValueWhenInvisible + "\""; 66 | 67 | 68 | // prepare array to iterate on new measure names 69 | string[] newMeasureNames = 70 | { 71 | buttonTextMeasureName, 72 | buttonBackgroundMeasureName, 73 | buttonNavigationMeasureName, 74 | thereAreDataProblemsMeasureName 75 | }; 76 | 77 | 78 | // check none of the new measure names already exist as such 79 | foreach(string measureName in newMeasureNames) { 80 | if(Model.AllMeasures.Any(Measure => Measure.Name == measureName)) { 81 | Error(measureName + " already exists!"); 82 | return; 83 | }; 84 | }; 85 | 86 | var dataProblemsMeasure = Selected.Measure; 87 | 88 | string navigationTableExpression = 89 | "FILTER({1},[Value] = 0)"; 90 | 91 | var navigationTable = 92 | Model.AddCalculatedTable(navigationTableName,navigationTableExpression); 93 | 94 | navigationTable.FormatDax(); 95 | navigationTable.Description = 96 | "Table to store the measures for the dynamic button that leads to the data problems sheet"; 97 | 98 | navigationTable.IsHidden = true; 99 | 100 | if(!Model.AllMeasures.Any(Measure => Measure.Name == buttonColorMeasureNameWhenVisible)) { 101 | navigationTable.AddMeasure(buttonColorMeasureNameWhenVisible,buttonColorMeasureValueWhenVisible); 102 | }; 103 | 104 | if(!Model.AllMeasures.Any(Measure => Measure.Name == buttonColorMeasureNameWhenInvisible)) { 105 | navigationTable.AddMeasure(buttonColorMeasureNameWhenInvisible,"\"#FFFFFF00\""); 106 | }; 107 | 108 | string thereAreDataProblemsMeasureExpression = 109 | "[" + dataProblemsMeasure.Name + "]>0"; 110 | 111 | var thereAreDataProblemsMeasure = 112 | navigationTable.AddMeasure( 113 | thereAreDataProblemsMeasureName, 114 | thereAreDataProblemsMeasureExpression 115 | ); 116 | 117 | thereAreDataProblemsMeasure.FormatDax(); 118 | thereAreDataProblemsMeasure.Description = "Boolean measure, if true, the button leading to data problems sheet should show (internal use only)" ; 119 | 120 | string buttonBackgroundMeasureExpression = 121 | "VAR colorCode = " + 122 | " IF(" + 123 | " [" + thereAreDataProblemsMeasureName + "]," + 124 | " [" + buttonColorMeasureNameWhenVisible + "]," + 125 | " [" + buttonColorMeasureNameWhenInvisible + "]" + 126 | " )" + 127 | "RETURN " + 128 | " FORMAT(colorCode,\"@\")"; 129 | 130 | var buttonBackgroundMeasure = 131 | navigationTable.AddMeasure( 132 | buttonBackgroundMeasureName, 133 | buttonBackgroundMeasureExpression 134 | ); 135 | 136 | buttonBackgroundMeasure.FormatDax(); 137 | buttonBackgroundMeasure.Description = "Use this measure for conditional formatting of button background"; 138 | 139 | string buttonNavigationMeasureExpression = 140 | "IF(" + 141 | " [" + thereAreDataProblemsMeasureName + "]," + 142 | " \"" + dataProblemsSheetName + "\"," + 143 | " \"\"" + 144 | ")"; 145 | 146 | var buttonNavigationMeasure = 147 | navigationTable.AddMeasure( 148 | buttonNavigationMeasureName, 149 | buttonNavigationMeasureExpression 150 | ); 151 | 152 | buttonNavigationMeasure.FormatDax(); 153 | buttonNavigationMeasure.Description = "Use this measure for conditional page navigation"; 154 | 155 | string buttonTextMeasureExpression = 156 | "IF(" + 157 | " [" + thereAreDataProblemsMeasureName + "]," + 158 | " SUBSTITUTE(\"" + buttonTextPattern + "\",\"#\",FORMAT([" + dataProblemsMeasure.Name + "],0))," + 159 | " \"\"" + 160 | ")"; 161 | 162 | var buttonTextMeasure = 163 | navigationTable.AddMeasure( 164 | buttonTextMeasureName, 165 | buttonTextMeasureExpression 166 | ); 167 | 168 | buttonTextMeasure.FormatDax(); 169 | buttonTextMeasure.Description = "Use this measure for dynamic button text"; 170 | 171 | //dataProblemsMeasure.MoveTo(navigationTable); 172 | 173 | -------------------------------------------------------------------------------- /Intermediate/Create Explicit Measures.csx: -------------------------------------------------------------------------------- 1 | // Title: Auto-create explicit measures from all columns in all tables that have qualifying aggregation functions assigned 2 | // 3 | // Author: Tom Martens, twitter.com/tommartens68 4 | // 5 | // This script, when executed, will loop through all the tables and creates explicit measure for all the columns with qualifying 6 | // aggregation functions. 7 | // The qualifying aggregation functions are SUM, COUNT, MIN, MAX, AVERAGE. 8 | // This script can create a lot of measures, as by default the aggregation function for columns with a numeric data type is SUM. 9 | // So, it is a good idea to check all columns for the proper aggregation type, e.g. the aggregation type of id columns 10 | // should be set to None, as it does not make any sense to aggregate id columns. 11 | // An annotation:CreatedThrough is created with a value:CreateExplicitMeasures this will help to identify the measures created 12 | // using this script. 13 | // What is missing, the list below shows what might be coming in subsequent iterations of the script: 14 | // - the base column property hidden is not set to true 15 | // - no black list is used to prevent the creation of unwanted measures 16 | 17 | // *************************************************************************************************************** 18 | //the following variables are allowing controling the script 19 | var overwriteExistingMeasures = 0; // 1 overwrites existing measures, 0 preserves existing measures 20 | 21 | var measureNameTemplate = "{0} ({1}) - {2}"; // String.Format is used to create the measure name. 22 | //{0} will be replaced with the columnname (c.Name), {1} will be replaced with the aggregation function, and last but not least 23 | //{2} will be replaced with the tablename (t.Name). Using t.Name is necessary to create a distinction between measure names if 24 | //columns with the same name exist in different tables. 25 | //Assuming the column name inside the table "Fact Sale" is "Sales revenue" and the aggregation function is SUM 26 | //the measure name will be: "Sales revenue (Sum) - Fact Sale" 27 | 28 | //store aggregation function that qualify for measure creation to the hashset aggFunctions 29 | var aggFunctions = new HashSet{ 30 | AggregateFunction.Default, //remove this line, if you do not want to mess up your measures list by automatically created measures for all the columns that have the Default AggregateFunction assigned 31 | AggregateFunction.Sum, 32 | AggregateFunction.Count, 33 | AggregateFunction.Min, 34 | AggregateFunction.Max, 35 | AggregateFunction.Average 36 | }; 37 | 38 | //You have to be aware that by default this script will just create measures using the aggregate functions "Sum" or "Count" if 39 | //the column has the aggregate function AggregateFunction.Default assigned, this is checked further down below. 40 | //Also, if a column has the Default AggregateFunction assigned and is of the DataType 41 | //DataType.Automatic, DataType.Unknown, or DataType.Variant, no measure is created automatically, this is checked further down below. 42 | //dictDataTypeAggregateFunction = new Dictionary(); 43 | //see this article for all the available data types: https://docs.microsoft.com/en-us/dotnet/api/microsoft.analysisservices.tabular.datatype?view=analysisservices-dotnet 44 | //Of course you can change the aggregation function that will be used for different data types, 45 | //as long as you are using "Sum" and "Count" 46 | //Please be careful, if you change the aggregation function you might end up with multiplemeasures 47 | var dictDataTypeAggregateFunction = new Dictionary(); 48 | dictDataTypeAggregateFunction.Add( DataType.Binary , AggregateFunction.Count ); //adding a key/value pair(s) to the dictionary using the Add() method 49 | dictDataTypeAggregateFunction.Add( DataType.Boolean , AggregateFunction.Count ); 50 | dictDataTypeAggregateFunction.Add( DataType.DateTime , AggregateFunction.Count ); 51 | dictDataTypeAggregateFunction.Add( DataType.Decimal , AggregateFunction.Sum ); 52 | dictDataTypeAggregateFunction.Add( DataType.Double , AggregateFunction.Sum ); 53 | dictDataTypeAggregateFunction.Add( DataType.Int64 , AggregateFunction.Sum ); 54 | dictDataTypeAggregateFunction.Add( DataType.String , AggregateFunction.Count ); 55 | 56 | // *************************************************************************************************************** 57 | //all the stuff below this line should not be altered 58 | //of course this is not valid if you have to fix my errors, make the code more efficient, 59 | //or you have a thorough understanding of what you are doing 60 | 61 | //store all the existing measures to the list listOfMeasures 62 | var listOfMeasures = new List(); 63 | foreach( var m in Model.AllMeasures ) { 64 | listOfMeasures.Add( m.Name ); 65 | } 66 | 67 | //loop across all tables 68 | foreach( var t in Model.Tables ) { 69 | 70 | //loop across all columns of the current table t 71 | foreach( var c in t.Columns ) { 72 | 73 | var currAggFunction = c.SummarizeBy; //cache the aggregation function of the current column c 74 | var useAggFunction = AggregateFunction.Sum; 75 | var theMeasureName = ""; // Name of the new Measure 76 | var posInListOfMeasures = 0; //check if the new measure already exists <> -1 77 | 78 | if( aggFunctions.Contains(currAggFunction) ) //check if the current aggregation function qualifies for measure aggregation 79 | { 80 | //check if the current aggregation function is Default 81 | if( currAggFunction == AggregateFunction.Default ) 82 | { 83 | // check if the datatype of the column is considered for measure creation 84 | if( dictDataTypeAggregateFunction.ContainsKey( c.DataType ) ) 85 | { 86 | 87 | //some kind of sanity check 88 | if( c.DataType == DataType.Automatic || c.DataType == DataType.Unknown || c.DataType == DataType.Variant ) 89 | { 90 | Output("No measure will be created for columns with the data type: " + c.DataType.ToString() + " (" + c.DaxObjectFullName + ")"); 91 | continue; //moves to the next item in the foreach loop, the next colum in the current table 92 | } 93 | 94 | //cache the aggregation function from the dictDataTypeAggregateFunction 95 | useAggFunction = dictDataTypeAggregateFunction[ c.DataType ]; 96 | 97 | //some kind of sanity check 98 | if( useAggFunction != AggregateFunction.Count && useAggFunction != AggregateFunction.Sum ) 99 | { 100 | Output("No measure will be created for the column: " + c.DaxObjectFullName); 101 | continue; //moves to the next item in the foreach loop, the next colum in the current table 102 | } 103 | theMeasureName = String.Format( measureNameTemplate , c.Name , useAggFunction.ToString() , t.Name ); // Name of the new Measure 104 | posInListOfMeasures = listOfMeasures.IndexOf( theMeasureName ); //check if the new measure already exists <> -1 105 | 106 | } else { 107 | 108 | continue; //moves to the next item in the foreach loop, the next colum in the current table 109 | } 110 | 111 | } else { 112 | 113 | useAggFunction = currAggFunction; 114 | theMeasureName = String.Format( measureNameTemplate , c.Name , useAggFunction.ToString() , t.Name ); // Name of the new Measure 115 | posInListOfMeasures = listOfMeasures.IndexOf( theMeasureName ); //check if the new measure already exists <> -1 116 | 117 | } 118 | 119 | //sanity check 120 | if(theMeasureName == "") 121 | { 122 | continue; //moves to the next item in the foreach loop, the next colum in the current table 123 | } 124 | 125 | // create the measure 126 | if( ( posInListOfMeasures == -1 || overwriteExistingMeasures == 1 )) 127 | { 128 | if( overwriteExistingMeasures == 1 ) 129 | { 130 | foreach( var m in Model.AllMeasures.Where( m => m.Name == theMeasureName ).ToList() ) 131 | { 132 | m.Delete(); 133 | } 134 | } 135 | 136 | var newMeasure = t.AddMeasure 137 | ( 138 | theMeasureName // Name of the new Measure 139 | , "" + useAggFunction.ToString().ToUpper() + "(" + c.DaxObjectFullName + ")" // DAX expression 140 | ); 141 | 142 | newMeasure.SetAnnotation( "CreatedThrough" , "CreateExplicitMeasures" ); // flag the measures created through this script 143 | 144 | } 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /Basic/Extended Properties Preview/ep_Model Extended Properties Generator.csx: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Author: Tommy Puglia 4 | // Website: https://pugliabi.com 5 | // Check out the Explicit Measures Podcast on Apple, spotify, & YouTube! 6 | // https://www.youtube.com/playlist?list=PLn1m_aBmgsbHr83c1P6uqaWF5PLdFzOjj 7 | // 8 | // Global Model Extended Properties Template Generator 9 | // 10 | // Tied to an example of the Group by Global Measure Script in this same repo 11 | 12 | // This Script is a preview of how easy it can be to create standard Extended Properties based on a model (such as a Date Table, referencing the fact table's date column, etc.) 13 | // 14 | // Why? 15 | // Creating Global Extended Properties with the same keys across Models would open up endless possibilities for utilizing scripts in Tabular Editor to automate tasks based on the Extended Properties. 16 | // 17 | // Take the example this script does. It looks at the current model if the standard Extended Properties are available, if not will prompt you to choose the columns and it auto-create the Extended Properties. 18 | // Then, any other macro can simply call the key value and use it for Time Intelligence, Rolling Calcs, Calculation Groups, or any other use case. 19 | // Rather than having to always select the objects per model, or worse hard-coding the value in a macro script that changes per model, having global Extended Properties would allow for a more dynamic approach. 20 | 21 | // Instructions 22 | // Simply run the script on the model, and it will prompt you to choose the following: 23 | // Date Table, Date column, and Date Year Column (exp 2022) 24 | // Fact Table, and the date column in the fact table 25 | // A Global Measure (for default calculations) 26 | 27 | // More can be added! 28 | 29 | #r "C:\Program Files\Tabular Editor 3\TabularEditor3.exe" // *** Needed for C# scripting, remove in TE3 *** 30 | #r "C:\Program Files (x86)\Tabular Editor 3\TabularEditor3.exe" // *** Needed for C# scripting, remove in TE3 *** 31 | #r "System.IO" 32 | #r "Microsoft.VisualBasic" 33 | 34 | using TabularEditor.TOMWrapper; // *** Needed for C# scripting, remove in TE3 *** 35 | using TabularEditor.Scripting; // *** Needed for C# scripting, remove in TE3 *** 36 | using System.IO; 37 | using Microsoft.VisualBasic; 38 | using Newtonsoft.Json; 39 | using Newtonsoft.Json.Linq; 40 | using System.Windows.Forms; 41 | 42 | Model Model; // *** Needed for C# scripting, remove in TE3 *** 43 | TabularEditor.Shared.Interaction.Selection Selected; // *** Needed for C# scripting, remove in TE3 *** 44 | 45 | #r "C:\Program Files\Tabular Editor 3\TabularEditor3.exe" // *** Needed for C# scripting, remove in TE3 *** 46 | #r "C:\Program Files (x86)\Tabular Editor 3\TabularEditor3.exe" // *** Needed for C# scripting, remove in TE3 *** 47 | #r "System.IO" 48 | #r "Microsoft.VisualBasic" 49 | 50 | using TabularEditor.TOMWrapper; // *** Needed for C# scripting, remove in TE3 *** 51 | using TabularEditor.Scripting; // *** Needed for C# scripting, remove in TE3 *** 52 | using System.IO; 53 | using Microsoft.VisualBasic; 54 | using Newtonsoft.Json; 55 | using Newtonsoft.Json.Linq; 56 | using System.Windows.Forms; 57 | 58 | 59 | Func, string, string> SelectString = (IList listText, string titleText) => 60 | { 61 | 62 | var listboxText = new ListBox() 63 | { 64 | Dock = DockStyle.Fill 65 | }; 66 | 67 | var panelButtons = new Panel() 68 | { 69 | Height = 22, 70 | Dock = DockStyle.Bottom 71 | }; 72 | 73 | var buttonOK = new Button() 74 | { 75 | Text = "OK", 76 | DialogResult = DialogResult.OK, 77 | Left = 120 78 | }; 79 | 80 | var buttonCancel = new Button() 81 | { 82 | Text = "Cancel", 83 | DialogResult = DialogResult.Cancel, 84 | Left = 204 85 | }; 86 | 87 | var formInputBox = new Form() 88 | { 89 | Text = titleText, 90 | Padding = new System.Windows.Forms.Padding(8), 91 | FormBorderStyle = FormBorderStyle.FixedDialog, 92 | MinimizeBox = false, 93 | MaximizeBox = false, 94 | StartPosition = FormStartPosition.CenterScreen, 95 | AcceptButton = buttonOK, 96 | CancelButton = buttonCancel 97 | }; 98 | listboxText.Items.AddRange(listText.ToArray()); 99 | listboxText.SelectedItem = listText[0]; 100 | formInputBox.Controls.AddRange(new Control[] { listboxText, panelButtons }); 101 | panelButtons.Controls.AddRange(new Control[] { buttonOK, buttonCancel }); 102 | return formInputBox.ShowDialog() == DialogResult.OK ? listboxText.SelectedItem.ToString() : null; 103 | 104 | }; 105 | // Get Extended Properties & Template 106 | var mExtProp = Model.GetExtendedProperties(); 107 | var TemplateExtendedProperties = new string[] { 108 | "DateTable", 109 | "DateColumn", 110 | "DateYearColumn", 111 | "FactTable", 112 | "FactDateColumn", 113 | "GlobalMeasure", 114 | "DefaultRollingDays" 115 | }; 116 | var ChooseInput = (new string[] {"Yes","No"}); 117 | 118 | 119 | // If Copying 120 | var stepDateTable = "DateTable"; 121 | var stepDateColumn = "DateColumn"; 122 | var stepDateYearColumn = "DateYearColumn"; 123 | var stepFactTable = "FactTable"; 124 | var stepFactDateColumn = "FactDateColumn"; 125 | var stepGlobalMeasure = "GlobalMeasure"; 126 | var stepDefaultDays = "DefaultRollingDays"; 127 | 128 | var sb = new System.Text.StringBuilder(); 129 | var createdb = new System.Text.StringBuilder(); 130 | string newline = Environment.NewLine; 131 | sb.Append("ExtenPropertyName" + '\t' + "ExtenPropertyValue" + '\t' + "Exisits" + newline); 132 | createdb.Append("{" + newline ); 133 | 134 | 135 | // Add DateTable Extended Property 136 | var epDateTable = ""; 137 | var epDateColumn = ""; 138 | var epDateYearColumn = ""; 139 | var quotes = "_"; 140 | if(Model.GetExtendedProperty(stepDateTable) == null) 141 | { 142 | 143 | var DateTable = SelectTable(label: "Select your Date Table"); 144 | var DateColumn = SelectColumn(DateTable.Columns, label: "Select your Date Column"); 145 | var DateYearColumn = SelectColumn(DateTable.Columns, label: "Select your Date Year Column"); 146 | string epName = DateTable.Name; 147 | Model.SetExtendedProperty(stepDateTable, epName, ExtendedPropertyType.String); 148 | Model.SetExtendedProperty(stepDateColumn, DateColumn.DaxObjectFullName, ExtendedPropertyType.String); 149 | Model.SetExtendedProperty(stepDateYearColumn, DateYearColumn.DaxObjectFullName, ExtendedPropertyType.String); 150 | } 151 | else 152 | { 153 | var UpdateDates = SelectString(new string[] {"Yes", "No"}, "Update Date Extended Properties?"); 154 | if (UpdateDates == "Yes") 155 | { 156 | var DateTable = SelectTable(label: "Select your Date Table"); 157 | var DateColumn = SelectColumn(DateTable.Columns, label: "Select your Date Column"); 158 | var DateYearColumn = SelectColumn(DateTable.Columns, label: "Select your Date Year Column"); 159 | string epName = DateTable.Name; 160 | Model.SetExtendedProperty(stepDateTable, epName, ExtendedPropertyType.String); 161 | Model.SetExtendedProperty(stepDateColumn, DateColumn.DaxObjectFullName, ExtendedPropertyType.String); 162 | Model.SetExtendedProperty(stepDateYearColumn, DateYearColumn.DaxObjectFullName, ExtendedPropertyType.String); 163 | } 164 | } 165 | if(Model.GetExtendedProperty(stepFactTable) == null) 166 | { 167 | var FactTable = SelectTable(label: "Select your fact Table"); 168 | var FactDateColumn = SelectColumn(FactTable.Columns, label: "Select your Fact Date Column"); 169 | Model.SetExtendedProperty(stepFactTable, FactTable.Name, ExtendedPropertyType.String); 170 | Model.SetExtendedProperty(stepFactDateColumn, FactDateColumn.DaxObjectFullName, ExtendedPropertyType.String); 171 | } 172 | else 173 | { 174 | var UpdateFact = SelectString(new string[] {"Yes", "No"}, "Update Fact Extended Properties?"); 175 | if (UpdateFact == "Yes") 176 | { 177 | var FactTable = SelectTable(label: "Select your fact Table"); 178 | var FactDateColumn = SelectColumn(FactTable.Columns, label: "Select your Fact Date Column"); 179 | Model.SetExtendedProperty(stepFactTable, FactTable.Name, ExtendedPropertyType.String); 180 | Model.SetExtendedProperty(stepFactDateColumn, FactDateColumn.DaxObjectFullName, ExtendedPropertyType.String); 181 | } 182 | } 183 | if (Model.GetExtendedProperty(stepGlobalMeasure) == null) 184 | { 185 | var GlobalMeasures = SelectMeasure(label: "Select your Global Measure"); 186 | Model.SetExtendedProperty(stepGlobalMeasure, GlobalMeasures.DaxObjectFullName, ExtendedPropertyType.String); 187 | } 188 | else 189 | { 190 | var UpdateGlobal = SelectString(new string[] {"Yes", "No"}, "Update Global Measure?"); 191 | if (UpdateGlobal == "Yes") 192 | { 193 | var GlobalMeasures = SelectMeasure(label: "Select your Global Measure"); 194 | Model.SetExtendedProperty(stepGlobalMeasure, GlobalMeasures.DaxObjectFullName, ExtendedPropertyType.String); 195 | } 196 | } 197 | if (Model.GetExtendedProperty(stepDefaultDays) == null) 198 | { 199 | var DefaultDays = Interaction.InputBox("How many Default Days for Rolling?","Rolling Days", "Labels", 740, 400); 200 | Model.SetExtendedProperty(stepDefaultDays, DefaultDays, ExtendedPropertyType.String); 201 | } 202 | else 203 | { 204 | var DefaultDays = Interaction.InputBox("How many Default Days for Rolling?","Rolling Days", Model.GetExtendedProperty(stepDefaultDays), 740, 400); 205 | Model.SetExtendedProperty(stepDefaultDays, DefaultDays, ExtendedPropertyType.String); 206 | } 207 | 208 | -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Alert Summary Measures.csx: -------------------------------------------------------------------------------- 1 | /* 2 | //2022-06-29 B.Agullo 3 | // 4 | // BLOG POST on the actual use case and how to use it 5 | // https://www.esbrina-ba.com/data-validation-with-power-bi/ 6 | // 7 | //Selecting a table will select the table for the overall summary measures. 8 | //The script will go through all the tables of the model and 9 | //for each table if will scan the measures contained in the display folder that starts with "Checks " 10 | // and will generate a new display folder in the same table 11 | //called "Titles " followed by the original display folder name 12 | //where all measures original measures will have an equivalent measure 13 | //with the expression [original measure] & Original measure name 14 | //also another displayfolder called "summary measures" 15 | //will contain 2 measures. One is the sum of all selected measures 16 | //the other one is the concatenation of all the titles measures 17 | //Useful when setting alarms when one measure is greater than 0 18 | //on the original selected table it will store the sum of all the summary measures of the model 19 | //read the blog post for a more clear idea of how to use it. 20 | */ 21 | 22 | /*customize if needed*/ 23 | string dynamicMeasureCustomActionName = "Dynamic Measure"; 24 | 25 | string alertDisplayFolderPrefix = "Checks "; 26 | string allMeasuresPrefix = "Alerts Value "; 27 | string allTitleMeasuresPrefix = "Alerts Descriptions "; 28 | string allMeasureCountPrefix = "Alert Count "; 29 | 30 | string summaryMeasuresDisplayFolder = "Summary Measures"; 31 | string titleMeasuresPrefix = "Title "; 32 | string titleMeasuresDisplayFolderPrefix = "Titles of "; 33 | 34 | 35 | 36 | string overallAlertValueMeasureName = "Total Alerts Value"; 37 | string overallAlertDescMeasureName = "Total Alerts Description"; 38 | string overallAlertCountMeasureName = "Total Alert Count"; 39 | 40 | 41 | /*do not modify below this line*/ 42 | 43 | 44 | 45 | string annotationKey = "@AgulloBernat"; 46 | string annotationValue = "Alert Summary Sum Measure"; 47 | string annotationValueDesc = "Alert Description Concat Measure"; 48 | string annotationValueCount = "Alert Count Measure"; 49 | 50 | 51 | if(Selected.Tables.Count() != 1) 52 | { 53 | ScriptHelper.Error("Select a table for overall alert measures and try again."); 54 | return; 55 | } 56 | 57 | //create calculation group without any calc items 58 | Model.CustomAction(dynamicMeasureCustomActionName); 59 | 60 | /* go through each table ... */ 61 | foreach (Table t in Model.Tables) 62 | { 63 | 64 | string displayFolderName = ""; 65 | string titleMeasuresDisplayFolderName = ""; 66 | string allMeasureExpression = ""; 67 | string allMeasureName = ""; 68 | string allTitleMeasureExpression = "\"\""; 69 | string allTitleMeasureName = ""; 70 | string allMeasureCountExpression = ""; 71 | string allMeasureCountName = ""; 72 | 73 | 74 | /*check if there's any measure to process*/ 75 | List alertMeasures = new List(); 76 | foreach (Measure m in t.Measures) 77 | { 78 | if (m.DisplayFolder.Length >= alertDisplayFolderPrefix.Length) 79 | { 80 | if( m.DisplayFolder.Substring(0, alertDisplayFolderPrefix.Length) == alertDisplayFolderPrefix) 81 | { 82 | 83 | alertMeasures.Add(m); 84 | } 85 | } 86 | } 87 | 88 | /* if any was found */ 89 | if (alertMeasures.Count() > 0) { 90 | 91 | /*for each measure found, process it */ 92 | foreach (Measure m in alertMeasures) { 93 | 94 | if (displayFolderName == "") 95 | { 96 | displayFolderName = m.DisplayFolder; 97 | titleMeasuresDisplayFolderName = titleMeasuresDisplayFolderPrefix + displayFolderName; 98 | }; 99 | 100 | string titleMeasureName = titleMeasuresPrefix + m.Name; 101 | string titleMeasureExpression = m.DaxObjectName + " & \" " + m.Name + "\""; 102 | 103 | foreach (Measure delM in Model.AllMeasures.Where(x => x.Name == titleMeasureName).ToList()) 104 | { 105 | delM.Delete(); 106 | }; 107 | 108 | Measure titleM = m.Table.AddMeasure( 109 | name: titleMeasureName, 110 | expression: titleMeasureExpression, 111 | displayFolder: titleMeasuresDisplayFolderName 112 | ); 113 | 114 | titleM.FormatDax(); 115 | 116 | allMeasureExpression = allMeasureExpression + Environment.NewLine + " + " + m.DaxObjectName; 117 | 118 | allTitleMeasureExpression = 119 | allTitleMeasureExpression 120 | + " & IF(" + m.DaxObjectFullName + "> 0," 121 | + titleM.DaxObjectFullName + " & UNICHAR(10))"; 122 | 123 | //m.CustomAction("Dynamic Measure"); 124 | 125 | allMeasureCountExpression = 126 | allMeasureCountExpression 127 | + Environment.NewLine + " + IF(" + m.DaxObjectFullName + "> 0, 1)"; 128 | 129 | }; 130 | 131 | /*now create the summary measures for that table*/ 132 | allMeasureName = allMeasuresPrefix + t.Name; 133 | allTitleMeasureName = allTitleMeasuresPrefix + t.Name; 134 | allMeasureCountName = allMeasureCountPrefix + t.Name; 135 | 136 | foreach (Measure delM in Model.AllMeasures.Where(x => x.Name == allMeasureName).ToList()) 137 | { 138 | delM.Delete(); 139 | }; 140 | 141 | foreach (Measure delM in Model.AllMeasures.Where(x => x.Name == allTitleMeasureName).ToList()) 142 | { 143 | delM.Delete(); 144 | }; 145 | 146 | foreach (Measure delM in Model.AllMeasures.Where(x => x.Name == allMeasureCountName).ToList()) 147 | { 148 | delM.Delete(); 149 | }; 150 | 151 | Measure measure = 152 | t.AddMeasure( 153 | name: allMeasureName, 154 | expression: allMeasureExpression, 155 | displayFolder: summaryMeasuresDisplayFolder); 156 | 157 | measure.SetAnnotation(annotationKey, annotationValue); 158 | measure.FormatString = "#,##0"; 159 | measure.FormatDax(); 160 | measure.CustomAction(dynamicMeasureCustomActionName); 161 | 162 | Measure titleMeasure = 163 | t.AddMeasure( 164 | name: allTitleMeasureName, 165 | expression: allTitleMeasureExpression, 166 | displayFolder: summaryMeasuresDisplayFolder); 167 | 168 | titleMeasure.SetAnnotation(annotationKey, annotationValueDesc); 169 | titleMeasure.FormatDax(); 170 | titleMeasure.CustomAction(dynamicMeasureCustomActionName); 171 | 172 | Measure countMeasure = 173 | t.AddMeasure( 174 | name: allMeasureCountName, 175 | expression: allMeasureCountExpression, 176 | displayFolder: summaryMeasuresDisplayFolder); 177 | 178 | countMeasure.SetAnnotation(annotationKey, annotationValueCount); 179 | countMeasure.FormatDax(); 180 | countMeasure.CustomAction(dynamicMeasureCustomActionName); 181 | 182 | } 183 | } 184 | 185 | 186 | /*once we processed all tables, time to create overall summary measures*/ 187 | /*clean up*/ 188 | if (Model.AllMeasures.Any(x => x.Name == overallAlertValueMeasureName)) 189 | { 190 | Model.AllMeasures.Where( 191 | x => x.Name == overallAlertValueMeasureName 192 | ).First().Delete(); 193 | 194 | } 195 | 196 | if (Model.AllMeasures.Any( 197 | x => x.Name == overallAlertDescMeasureName)) 198 | { 199 | Model.AllMeasures.Where( 200 | x => x.Name == overallAlertDescMeasureName 201 | ).First().Delete(); 202 | 203 | } 204 | 205 | if (Model.AllMeasures.Any( 206 | x => x.Name == overallAlertCountMeasureName)) 207 | { 208 | Model.AllMeasures.Where( 209 | x => x.Name == overallAlertCountMeasureName 210 | ).First().Delete(); 211 | 212 | } 213 | 214 | 215 | /*regenerate if necessary*/ 216 | if (Model.AllMeasures.Any( 217 | x => x.DisplayFolder.IndexOf(summaryMeasuresDisplayFolder) == 0 218 | & x.Name.IndexOf(allMeasuresPrefix) == 0)) 219 | { 220 | 221 | string overallAlertValueMeasureExpression = ""; 222 | string overallAlertDescMeasureExpression = "\"\""; 223 | string overallAlertCountMeasureExpression = ""; 224 | 225 | foreach (Measure m in 226 | Model.AllMeasures.Where( 227 | x => x.DisplayFolder.IndexOf(summaryMeasuresDisplayFolder) == 0 228 | & x.Name.IndexOf(allMeasuresPrefix) == 0)) 229 | { 230 | 231 | 232 | overallAlertValueMeasureExpression = 233 | overallAlertValueMeasureExpression 234 | + " + " + m.DaxObjectFullName; 235 | 236 | } 237 | 238 | foreach (Measure m in 239 | Model.AllMeasures.Where( 240 | x => x.DisplayFolder.IndexOf(summaryMeasuresDisplayFolder) == 0 241 | & x.Name.IndexOf(allTitleMeasuresPrefix) == 0)) 242 | { 243 | overallAlertDescMeasureExpression = 244 | overallAlertDescMeasureExpression 245 | + " & IF(LEN(" +m.DaxObjectFullName +")>0, UNICHAR(10) & UNICHAR(10) & \"********** " 246 | + m.Table.Name + "*********\" & UNICHAR(10) & " + m.DaxObjectFullName + ")"; 247 | 248 | } 249 | 250 | 251 | 252 | foreach (Measure m in 253 | Model.AllMeasures.Where( 254 | x => x.DisplayFolder.IndexOf(summaryMeasuresDisplayFolder) == 0 255 | & x.Name.IndexOf(allMeasureCountPrefix) == 0)) 256 | { 257 | overallAlertCountMeasureExpression = 258 | overallAlertCountMeasureExpression 259 | + " + " + m.DaxObjectFullName; 260 | 261 | } 262 | 263 | 264 | Measure alertValueMeasure = Selected.Table.AddMeasure(overallAlertValueMeasureName, overallAlertValueMeasureExpression); 265 | alertValueMeasure.FormatDax(); 266 | alertValueMeasure.FormatString ="#,##0"; 267 | 268 | Measure alertDescMeasure = Selected.Table.AddMeasure(overallAlertDescMeasureName, overallAlertDescMeasureExpression); 269 | alertDescMeasure.FormatDax(); 270 | 271 | Measure alertCountMeasure = Selected.Table.AddMeasure(overallAlertCountMeasureName, overallAlertCountMeasureExpression); 272 | alertCountMeasure.FormatDax(); 273 | alertCountMeasure.FormatString ="#,##0"; 274 | 275 | 276 | }; 277 | -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Create measures with a calculation group.csx: -------------------------------------------------------------------------------- 1 | #r "Microsoft.VisualBasic" 2 | using Microsoft.VisualBasic; 3 | using System.Windows.Forms; 4 | 5 | /* '2022-06-13 / B.Agullo / */ 6 | /* '2022-09-17 / B.Agullo / possibility to create a Field Parameter with a column for the base measure & calc Item 7 | /* CREATE MEASURES WITH BASE MEASURES AND A CALCULATION GROUP */ 8 | 9 | /* See: https://www.esbrina-ba.com/creating-well-formatted-measures-based-on-a-calculation-group/ */ 10 | /* See: https://www.esbrina-ba.com/a-dynamic-legend-for-a-dynamic-measure-time-intel-chart/ 11 | /* select measures and execute, you will need to run it twice */ 12 | /* first time to create aux calc group, second time to actually create measuree*/ 13 | /* remove aux calc group before going to production, do the right thing */ 14 | 15 | string auxCgTag = "@AgulloBernat"; 16 | string auxCgTagValue = "CG to extract format strings"; 17 | 18 | string auxCalcGroupName = "DELETE AUX CALC GROUP"; 19 | string auxCalcItemName = "Get Format String"; 20 | 21 | string baseMeasureAnnotationName = "Base Measure"; 22 | string calcItemAnnotationName = "Calc Item"; 23 | string calcItemSortOrderName = "Sort Order"; 24 | string calcItemSortOrderValue = String.Empty; 25 | 26 | string scriptAnnotationName = "Script"; 27 | string scriptAnnotationValue = "Create Measures with a Calculation Group"; 28 | 29 | bool generateFieldParameter; 30 | 31 | DialogResult dialogResult = MessageBox.Show("Generate Field Parameter?", "Field Parameter", MessageBoxButtons.YesNo); 32 | generateFieldParameter = (dialogResult == DialogResult.Yes); 33 | 34 | 35 | /*find any regular CGs (excluding the one we might have created)*/ 36 | var regularCGs = Model.Tables.Where( 37 | x => x.ObjectType == ObjectType.CalculationGroupTable 38 | & x.GetAnnotation(auxCgTag) != auxCgTagValue); 39 | 40 | if (regularCGs.Count() == 0) 41 | { 42 | Error("No Calculation Groups Found"); 43 | return; 44 | }; 45 | 46 | /*check if we already created the auxiliary calculation group*/ 47 | var auxCgs = Model.Tables.Where(x => x.GetAnnotation(auxCgTag) == auxCgTagValue); 48 | 49 | CalculationGroupTable auxCg = null as CalculationGroupTable; 50 | 51 | /*if there are more than one for some reason we'll just use the first one*/ 52 | if(auxCgs.Count() >= 1) 53 | { 54 | auxCg = auxCgs.First() as CalculationGroupTable; 55 | } else 56 | { 57 | /*create the aux calc group and ask for a refresh since it cannot be used in a query before that*/ 58 | auxCg = Model.AddCalculationGroup(name: auxCalcGroupName); 59 | auxCg.AddCalculationItem(name: auxCalcItemName, expression: "SELECTEDMEASUREFORMATSTRING()"); 60 | auxCg.SetAnnotation(auxCgTag, auxCgTagValue); 61 | 62 | /*better hidden in case someone forgets to delete it*/ 63 | auxCg.IsHidden = true; 64 | int maxPrecedence = 0; 65 | 66 | /*check for the max precedence of other calc groups*/ 67 | foreach (CalculationGroupTable cg in regularCGs) 68 | { 69 | if (cg.CalculationGroupPrecedence > maxPrecedence) 70 | { 71 | maxPrecedence = cg.CalculationGroupPrecedence; 72 | }; 73 | }; 74 | 75 | /*assign the highest precedence and some margin*/ 76 | auxCg.CalculationGroupPrecedence = maxPrecedence + 10; 77 | 78 | Info("Save changes to the model, recalculate the model, and launch the script again."); 79 | return; 80 | 81 | }; 82 | 83 | 84 | 85 | 86 | /*check if any measures are selected*/ 87 | if (Selected.Measures.Count == 0) 88 | { 89 | Error("No measures selected"); 90 | return; 91 | } 92 | 93 | CalculationGroupTable regularCg = null as CalculationGroupTable; 94 | 95 | /*allow user to select calculation group if more than one is found*/ 96 | if (regularCGs.Count() > 1) 97 | { 98 | regularCg = SelectTable(regularCGs) as CalculationGroupTable; 99 | } 100 | /*otherwise just pick the first (and only)*/ 101 | else 102 | { 103 | regularCg = regularCGs.First() as CalculationGroupTable; 104 | } 105 | 106 | /*check if no selection was made*/ 107 | if(regularCg == null) 108 | { 109 | Error("No Target Calculation Group selected"); 110 | return; 111 | }; 112 | 113 | string name = String.Empty; 114 | if(generateFieldParameter) { 115 | name = Interaction.InputBox("Provide a name for the field parameter", "Field Parameter", regularCg.Name + " Measures", 740, 400); 116 | if(name == "") {Error("Execution Aborted"); return;}; 117 | }; 118 | 119 | 120 | MeasureCollection measures; 121 | 122 | /*iterates through each selected measure*/ 123 | foreach (Measure m in Selected.Measures) 124 | { 125 | /*check that base measure has a proper format string*/ 126 | if(m.FormatString == "") { 127 | Error("Define FormatString for " + m.Name + " and try again"); 128 | return; 129 | }; 130 | 131 | /*prepares a displayfolder to store all new measures*/ 132 | string displayFolderName = m.Name + " Measures"; 133 | 134 | /*iterates thorough all calculation items of the selected calc group*/ 135 | foreach (CalculationItem calcItem in regularCg.CalculationItems) 136 | { 137 | /*measure name*/ 138 | string measureName = m.Name + " " + calcItem.Name; 139 | 140 | //only if the measure is not yet there (think of reruns) 141 | if(!Model.AllMeasures.Any(x => x.Name == measureName)){ 142 | 143 | /*prepares a query to calculate the resulting format when applying the calculation item on the measure*/ 144 | string query = string.Format( 145 | "EVALUATE {{CALCULATE({0},{1},{2})}}", 146 | m.DaxObjectFullName, 147 | string.Format( 148 | "{0}=\"{1}\"", 149 | regularCg.Columns[0].DaxObjectFullName, 150 | calcItem.Name), 151 | string.Format( 152 | "{0}=\"{1}\"", 153 | auxCg.Columns[0].DaxObjectFullName, 154 | auxCalcItemName) 155 | ); 156 | 157 | /*executes the query*/ 158 | using (var reader = Model.Database.ExecuteReader(query)) 159 | { 160 | // resultset should contain just one row, with the format string 161 | while (reader.Read()) 162 | { 163 | 164 | 165 | /*retrive the formatstring from the query*/ 166 | string formatString = reader.GetValue(0).ToString(); 167 | 168 | /*build the expression of the measure*/ 169 | string measureExpression = string.Format( 170 | "CALCULATE({0},{1}=\"{2}\")", 171 | m.DaxObjectName, 172 | regularCg.Columns[0].DaxObjectFullName, 173 | calcItem.Name); 174 | 175 | 176 | 177 | /*actually build the measure*/ 178 | Measure newMeasure = 179 | m.Table.AddMeasure( 180 | name: measureName, 181 | expression: measureExpression); 182 | 183 | 184 | /*the all important format string!*/ 185 | newMeasure.FormatString = formatString; 186 | 187 | /*final polish*/ 188 | newMeasure.DisplayFolder = displayFolderName; 189 | newMeasure.FormatDax(); 190 | 191 | /*add annotations for the creation of the field parameter*/ 192 | newMeasure.SetAnnotation(baseMeasureAnnotationName,m.Name); 193 | newMeasure.SetAnnotation(calcItemAnnotationName,calcItem.Name); 194 | newMeasure.SetAnnotation(scriptAnnotationName,scriptAnnotationValue); 195 | newMeasure.SetAnnotation(calcItemSortOrderName,calcItem.Ordinal.ToString("000")); 196 | 197 | } 198 | } 199 | } 200 | } 201 | } 202 | 203 | 204 | if(!generateFieldParameter) { 205 | //end of execution 206 | return; 207 | }; 208 | 209 | 210 | // Before running the script, select the measures or columns that you 211 | // would like to use as field parameters (hold down CTRL to select multiple 212 | // objects). Also, you may change the name of the field parameter table 213 | // below. NOTE: If used against Power BI Desktop, you must enable unsupported 214 | // features under File > Preferences (TE2) or Tools > Preferences (TE3). 215 | 216 | 217 | if(Selected.Columns.Count == 0 && Selected.Measures.Count == 0) throw new Exception("No columns or measures selected!"); 218 | 219 | // Construct the DAX for the calculated table based on the measures created previously by the script 220 | var objects = Model.AllMeasures 221 | .Where(x => x.GetAnnotation(scriptAnnotationName) == scriptAnnotationValue) 222 | .OrderBy(x => x.GetAnnotation(baseMeasureAnnotationName) + x.GetAnnotation(calcItemSortOrderName)); 223 | 224 | var dax = "{\n " + string.Join(",\n ", objects.Select((c,i) => string.Format("(\"{0}\", NAMEOF('{1}'[{0}]), {2},\"{3}\",\"{4}\")", 225 | c.Name, c.Table.Name, i, 226 | Model.Tables[c.Table.Name].Measures[c.Name].GetAnnotation("Base Measure"), 227 | Model.Tables[c.Table.Name].Measures[c.Name].GetAnnotation("Calc Item")))) + "\n}"; 228 | 229 | // Add the calculated table to the model: 230 | var table = Model.AddCalculatedTable(name, dax); 231 | 232 | // In TE2 columns are not created automatically from a DAX expression, so 233 | // we will have to add them manually: 234 | var te2 = table.Columns.Count == 0; 235 | var nameColumn = te2 ? table.AddCalculatedTableColumn(name, "[Value1]") : table.Columns["Value1"] as CalculatedTableColumn; 236 | var fieldColumn = te2 ? table.AddCalculatedTableColumn(name + " Fields", "[Value2]") : table.Columns["Value2"] as CalculatedTableColumn; 237 | var orderColumn = te2 ? table.AddCalculatedTableColumn(name + " Order", "[Value3]") : table.Columns["Value3"] as CalculatedTableColumn; 238 | 239 | if(!te2) { 240 | // Rename the columns that were added automatically in TE3: 241 | nameColumn.IsNameInferred = false; 242 | nameColumn.Name = name; 243 | fieldColumn.IsNameInferred = false; 244 | fieldColumn.Name = name + " Fields"; 245 | orderColumn.IsNameInferred = false; 246 | orderColumn.Name = name + " Order"; 247 | } 248 | // Set remaining properties for field parameters to work 249 | // See: https://twitter.com/markbdi/status/1526558841172893696 250 | nameColumn.SortByColumn = orderColumn; 251 | nameColumn.GroupByColumns.Add(fieldColumn); 252 | fieldColumn.SortByColumn = orderColumn; 253 | fieldColumn.SetExtendedProperty("ParameterMetadata", "{\"version\":3,\"kind\":2}", ExtendedPropertyType.Json); 254 | fieldColumn.IsHidden = true; 255 | orderColumn.IsHidden = true; -------------------------------------------------------------------------------- /Advanced/TopN_PlusOthers.csx: -------------------------------------------------------------------------------- 1 | #r "Microsoft.AnalysisServices.Core.dll" 2 | #r "Microsoft.VisualBasic" 3 | using Microsoft.VisualBasic; 4 | 5 | /* 6 | * Title: TopN + Others from SQL BI 7 | * 8 | * Author: George 9 | * 10 | * This script was inspired by the article from SQLBI https://sql.bi/695263 11 | * Code example for checking connection to Power BI Desktop was provided by Daniel Otykier 12 | * 13 | * For more detail see the this link: https://github.com/TabularEditor/TabularEditor/issues/1010 14 | * 15 | * Requirements: Create What-If Parameter before running this code! 16 | */ 17 | 18 | 19 | var ListOfTableAnnotations = new List(); 20 | string RexExpPattern = "TopNScript_(.+?)_ShiyanovG"; 21 | foreach (Table t in Model.Tables) 22 | { 23 | foreach (var a in t.GetAnnotations()) 24 | { 25 | // a.Output(); 26 | bool IsMatched = 27 | a.IndexOf("TopNScript_", StringComparison.OrdinalIgnoreCase) >= 0 28 | && a.IndexOf("_ShiyanovG", StringComparison.OrdinalIgnoreCase) >= 0 29 | ; 30 | if (IsMatched) 31 | { 32 | ListOfTableAnnotations.Add(a); 33 | } 34 | } 35 | } 36 | 37 | bool HasScriptAnnotations = ListOfTableAnnotations.Count() > 0; 38 | 39 | if (HasScriptAnnotations == true) 40 | { 41 | Info("TopN Script already implemented"); 42 | return; 43 | } 44 | 45 | 46 | var server = Model.Database.TOMDatabase.Server as Microsoft.AnalysisServices.Tabular.Server; 47 | var isLoadedFromFile = server == null; 48 | var isPbiDesktop = server != null 49 | && server.ServerLocation == Microsoft.AnalysisServices.ServerLocation.OnPremise 50 | && server.CompatibilityMode == Microsoft.AnalysisServices.CompatibilityMode.PowerBI; 51 | 52 | if (isLoadedFromFile) 53 | { 54 | Info("Metadata loaded from file"); 55 | } 56 | else if (isPbiDesktop) 57 | { 58 | // Info("Connected to PBI Desktop"); 59 | //Info("Select TopN What-If Parameter"); 60 | 61 | 62 | 63 | string ConfigInpuBoxText = 64 | "1 - Measures" 65 | + "\n" 66 | + "2 - Calculation Group" 67 | ; 68 | int ConfigDefaultInput = 1; 69 | var ConfigAnswer = Interaction.InputBox( 70 | ConfigInpuBoxText, 71 | "Choose configuration of execution", 72 | ConfigDefaultInput.ToString(), 73 | 740, 74 | 400); 75 | 76 | bool RunCode = 77 | ConfigAnswer == '1'.ToString() 78 | || ConfigAnswer == '2'.ToString(); 79 | string ConfigErrorMessage = @"Please choose correct configuration!"; 80 | if (RunCode == false) 81 | { 82 | // Force an Error if configuration is wrong 83 | Error(ConfigErrorMessage); 84 | // Do nothing 85 | return; 86 | } 87 | 88 | Table StartTableTopN = Model.SelectTable(null, "Select TopN What-If Parameter"); 89 | bool CheckForTopN = StartTableTopN == null; 90 | if (CheckForTopN) 91 | { 92 | Error("No TopN What-If"); 93 | return; 94 | } 95 | else 96 | { 97 | Info("OK"); 98 | 99 | var ListOfFactTables = new List(); 100 | var ListOfDimentionTables = new List
(); 101 | 102 | foreach (var r in Model.Relationships) 103 | { 104 | Table ManyColTable = r.FromColumn.Table; 105 | // Table ManyColTable = r.FromColumn.Table; 106 | bool notInListFact = ListOfFactTables.Contains(ManyColTable); 107 | if (notInListFact == false) 108 | { 109 | ListOfFactTables.Add(ManyColTable); 110 | } 111 | } 112 | 113 | foreach (var r in Model.Relationships) 114 | { 115 | Table OneColTable = r.ToColumn.Table; 116 | bool NotDate = r.ToColumn.DataType == DataType.DateTime; 117 | bool notInListDim = ListOfDimentionTables.Contains(OneColTable); 118 | if (notInListDim == false && NotDate == false) 119 | { 120 | ListOfDimentionTables.Add(OneColTable); 121 | } 122 | } 123 | 124 | // Set Annotation for object StartTable 125 | StartTableTopN.SetAnnotation( 126 | "TopNScript_StartTableTopN_ShiyanovG" 127 | , "TopNScript_StartTableTopN_ShiyanovG" 128 | ); 129 | Measure MeasureTopN = StartTableTopN.SelectMeasure(null, "Select TopN measure"); 130 | var MeasureTopNReference = MeasureTopN.DaxObjectName; 131 | Table StartTable = ListOfDimentionTables.SelectTable(null, "Select the table to implement TopN + Others for"); 132 | string StartTableName = StartTable.DaxObjectFullName; 133 | // Set Annotation for object StartTable 134 | StartTable.SetAnnotation( 135 | "TopNScript_StartTable_ShiyanovG" 136 | , "TopNScript_StartTable_ShiyanovG" 137 | ); 138 | 139 | Column StartColumn = 140 | StartTable.Columns 141 | .Where(c => c.UsedInRelationships.Any() == false) 142 | .SelectColumn(null, "Select the column to use for TopN + Others"); 143 | 144 | 145 | Table FactTable = ListOfFactTables.SelectTable(null, "Where is your main Fact Table"); 146 | Measure RankingMeasureReference = FactTable.SelectMeasure(null, "What is you base measure for the pattern"); 147 | string RankingMeasureReferenceName = RankingMeasureReference.DaxObjectFullName; 148 | 149 | string TopNTableExpression = 150 | "UNION( " 151 | + "ALLNOBLANKROW( " + StartColumn.DaxObjectFullName + ")," 152 | + "\n" 153 | + "{ \"Others\" }" 154 | + "\n )"; 155 | 156 | string TopNTableName = StartTable.Name + " Names"; 157 | string FullPowerBITableExpression = 158 | TopNTableName + " = " 159 | + "\n" 160 | + TopNTableExpression; 161 | 162 | Info("Copy to Clipboard the following code and create new Calucated Table"); 163 | FullPowerBITableExpression.Output(); 164 | 165 | Table ReferenceTable = Model.Tables[TopNTableName]; 166 | ReferenceTable.SetAnnotation( 167 | "TopNScript_ReferenceTable_ShiyanovG" 168 | , "TopNScript_ReferenceTable_ShiyanovG" 169 | ); 170 | 171 | string ReferenceTableName = ReferenceTable.DaxObjectFullName; 172 | string ReferenceColumnName = ReferenceTable.Columns.First().DaxObjectFullName; 173 | string RankingMeasureName = "Ranking"; 174 | string Others = " \"Others\" "; 175 | string RankingMeasureDax = 176 | @" 177 | IF ( 178 | ISINSCOPE ( {1} ), 179 | VAR ProductsToRank = [TopN Value] 180 | VAR SalesAmount = {3} 181 | VAR IsOtherSelected = 182 | SELECTEDVALUE ( {1} ) = {0} 183 | RETURN 184 | IF( 185 | IsOtherSelected, 186 | -- Rank for Others 187 | ProductsToRank + 1, 188 | -- Rank for regular products 189 | IF ( 190 | SalesAmount > 0, 191 | VAR VisibleProducts = 192 | CALCULATETABLE(VALUES({1}), ALLSELECTED({2})) 193 | VAR Ranking = 194 | RANKX(VisibleProducts, {3}, SalesAmount) 195 | RETURN 196 | IF (Ranking > 0 && Ranking <= ProductsToRank, Ranking ) 197 | ) 198 | ) 199 | ) 200 | "; 201 | string RankingMeasureDaxFormatted = string.Format( 202 | RankingMeasureDax, Others, ReferenceColumnName, 203 | ReferenceTableName, RankingMeasureReferenceName 204 | ); 205 | string VisibleRowMeasureDax = 206 | @" 207 | VAR Ranking = [Ranking] 208 | VAR TopNValue = [TopN Value] 209 | VAR Result = 210 | IF( 211 | NOT ISBLANK(Ranking), 212 | (Ranking <= TopNValue) - (Ranking = TopNValue + 1) 213 | ) 214 | RETURN Result "; 215 | 216 | // Generate code for desired measure (for example Sales Amount) 217 | string AddColsColumn = "@SalesAmount"; 218 | string q = "\""; 219 | string AmountNAMeasureDax = @" 220 | VAR SalesOfAll = 221 | CALCULATE ( {6}, REMOVEFILTERS ( {5} ) ) 222 | RETURN 223 | IF ( 224 | NOT ISINSCOPE ( {3} ), 225 | -- Calculation for a group of products 226 | SalesOfAll, 227 | -- Calculation for one product name 228 | VAR ProductsToRank = [TopN Value] 229 | VAR SalesOfCurrentProduct = {6} 230 | VAR IsOtherSelected = 231 | SELECTEDVALUE ( {3} ) = {0} 232 | RETURN 233 | IF( 234 | NOT IsOtherSelected, 235 | -- Calculation for a regular product 236 | SalesOfCurrentProduct, 237 | -- Calculation for Others 238 | VAR VisibleProducts = 239 | CALCULATETABLE( 240 | VALUES({4}), 241 | REMOVEFILTERS({3}) 242 | ) 243 | VAR ProductsWithSales = 244 | ADDCOLUMNS(VisibleProducts, {1}{2}{1} , [Sales Amount]) 245 | VAR FilterTopProducts = 246 | TOPN(ProductsToRank, ProductsWithSales, [{2}]) 247 | VAR FilterOthers = 248 | EXCEPT(ProductsWithSales, FilterTopProducts) 249 | VAR SalesOthers = 250 | CALCULATE( 251 | {6}, 252 | FilterOthers, 253 | REMOVEFILTERS ( {3} ) 254 | ) 255 | RETURN 256 | SalesOthers 257 | ) 258 | )" 259 | ; 260 | string AmountNAMeasureDaxFormatted = 261 | string.Format( 262 | AmountNAMeasureDax 263 | , Others // 0 264 | , q // 1 265 | , AddColsColumn // 2 266 | , ReferenceColumnName // 3 267 | , StartTableName // 4 268 | , ReferenceTableName // 5 269 | , RankingMeasureReferenceName // 6 270 | ); 271 | string AmountNAMeasureName = RankingMeasureReference.Name + " NA"; 272 | 273 | if (ConfigAnswer == "1") 274 | { 275 | // Add Ranking measure to the table with Formatted code 276 | ReferenceTable.AddMeasure(RankingMeasureName, RankingMeasureDaxFormatted, null); 277 | // Add Visible Row measure to the table 278 | ReferenceTable.AddMeasure("Visible Row", VisibleRowMeasureDax, null); 279 | // Add AmountNA measure to the table 280 | ReferenceTable.AddMeasure( 281 | AmountNAMeasureName 282 | , AmountNAMeasureDaxFormatted 283 | , null 284 | ); 285 | // Format all created measures 286 | ReferenceTable.Measures.FormatDax(); 287 | } 288 | else if (ConfigAnswer=="2") 289 | { 290 | string Config2Text = @" 291 | Corrently code doesn't support Calc Group implementation. 292 | Press OK to create measures."; 293 | Info(Config2Text); 294 | 295 | // Add Ranking measure to the table with Formatted code 296 | ReferenceTable.AddMeasure(RankingMeasureName, RankingMeasureDaxFormatted, null); 297 | // Add Visible Row measure to the table 298 | ReferenceTable.AddMeasure("Visible Row", VisibleRowMeasureDax, null); 299 | // Add AmountNA measure to the table 300 | ReferenceTable.AddMeasure( 301 | AmountNAMeasureName 302 | , AmountNAMeasureDaxFormatted 303 | , null 304 | ); 305 | // Format all created measures 306 | ReferenceTable.Measures.FormatDax(); 307 | } 308 | } 309 | 310 | } 311 | else 312 | { 313 | Info("Not connected to PBI Desktop"); 314 | } 315 | 316 | 317 | -------------------------------------------------------------------------------- /Advanced/One-Click Macros/Time Intelligence Calculation Group Creation.csx: -------------------------------------------------------------------------------- 1 | #r "Microsoft.VisualBasic" 2 | using Microsoft.VisualBasic; 3 | // 4 | // CHANGELOG: 5 | // '2021-05-01 / B.Agullo / 6 | // '2021-05-17 / B.Agullo / added affected measure table 7 | // '2021-06-19 / B.Agullo / data label measures 8 | // '2021-07-10 / B.Agullo / added flag expression to avoid breaking already special format strings 9 | // '2021-09-23 / B.Agullo / added code to prompt for parameters (code credit to Daniel Otykier) 10 | // '2021-09-27 / B.Agullo / added code for general name 11 | // '2022-10-11 / B.Agullo / added MMT and MWT calc item groups 12 | // 13 | // by Bernat Agulló 14 | // twitter: @AgulloBernat 15 | // www.esbrina-ba.com/blog 16 | // 17 | // REFERENCE: 18 | // Check out https://www.esbrina-ba.com/time-intelligence-the-smart-way/ where this script is introduced 19 | // 20 | // FEATURED: 21 | // this script featured in GuyInACube https://youtu.be/_j0iTUo2HT0 22 | // 23 | // THANKS: 24 | // shout out to Johnny Winter for the base script and SQLBI for daxpatterns.com 25 | 26 | //select the measures that you want to be affected by the calculation group 27 | //before running the script. 28 | //measure names can also be included in the following array (no need to select them) 29 | string[] preSelectedMeasures = {}; //include measure names in double quotes, like: {"Profit","Total Cost"}; 30 | 31 | //AT LEAST ONE MEASURE HAS TO BE AFFECTED!, 32 | //either by selecting it or typing its name in the preSelectedMeasures Variable 33 | 34 | 35 | 36 | // 37 | // ----- do not modify script below this line ----- 38 | // 39 | 40 | 41 | string affectedMeasures = "{"; 42 | 43 | int i = 0; 44 | 45 | for (i=0;i cg.GetAnnotation("@AgulloBernat") == "Time Intel Calc Group")) { 77 | calcGroupName = Model.CalculationGroups.Where(cg => cg.GetAnnotation("@AgulloBernat") == "Time Intel Calc Group").First().Name; 78 | 79 | }else { 80 | calcGroupName = Interaction.InputBox("Provide a name for your Calc Group", "Calc Group Name", "Time Intelligence", 740, 400); 81 | }; 82 | 83 | if(calcGroupName == "") return; 84 | 85 | 86 | if(Model.CalculationGroups.Any(cg => cg.GetAnnotation("@AgulloBernat") == "Time Intel Calc Group")) { 87 | columnName = Model.Tables.Where(cg => cg.GetAnnotation("@AgulloBernat") == "Time Intel Calc Group").First().Columns.First().Name; 88 | 89 | }else { 90 | columnName = Interaction.InputBox("Provide a name for your Calc Group Column", "Calc Group Column Name", calcGroupName, 740, 400); 91 | }; 92 | 93 | if(columnName == "") return; 94 | 95 | string affectedMeasuresTableName; 96 | 97 | if(Model.Tables.Any(t => t.GetAnnotation("@AgulloBernat") == "Time Intel Affected Measures Table")) { 98 | affectedMeasuresTableName = Model.Tables.Where(t => t.GetAnnotation("@AgulloBernat") == "Time Intel Affected Measures Table").First().Name; 99 | 100 | } else { 101 | affectedMeasuresTableName = Interaction.InputBox("Provide a name for affected measures table", "Affected Measures Table Name", calcGroupName + " Affected Measures", 740, 400); 102 | 103 | }; 104 | 105 | if(affectedMeasuresTableName == "") return; 106 | 107 | 108 | if(Model.Tables.Any(t => t.GetAnnotation("@AgulloBernat") == "Time Intel Affected Measures Table")) { 109 | affectedMeasuresColumnName = Model.Tables.Where(t => t.GetAnnotation("@AgulloBernat") == "Time Intel Affected Measures Table").First().Columns.First().Name; 110 | 111 | } else { 112 | affectedMeasuresColumnName = Interaction.InputBox("Provide a name for affected measures column", "Affected Measures Table Column Name", "Measure", 740, 400); 113 | 114 | }; 115 | 116 | 117 | 118 | string affectedMeasuresColumnName = Interaction.InputBox("Provide a name for affected measures table column name", "Affected Measures Table Column Name", "Measure", 740, 400); 119 | 120 | if(affectedMeasuresColumnName == "") return; 121 | //string affectedMeasuresColumnName = "Measure"; 122 | 123 | string labelAsValueMeasureName = "Label as Value Measure"; 124 | string labelAsFormatStringMeasureName = "Label as format string"; 125 | 126 | 127 | // '2021-09-24 / B.Agullo / model object selection prompts! 128 | var factTable = SelectTable(label: "Select your fact table"); 129 | if(factTable == null) return; 130 | 131 | var factTableDateColumn = SelectColumn(factTable.Columns, label: "Select the main date column"); 132 | if(factTableDateColumn == null) return; 133 | 134 | Table dateTableCandidate = null; 135 | 136 | if(Model.Tables.Any 137 | (x => x.GetAnnotation("@AgulloBernat") == "Time Intel Date Table" 138 | || x.Name == "Date" 139 | || x.Name == "Calendar")){ 140 | dateTableCandidate = Model.Tables.Where 141 | (x => x.GetAnnotation("@AgulloBernat") == "Time Intel Date Table" 142 | || x.Name == "Date" 143 | || x.Name == "Calendar").First(); 144 | 145 | }; 146 | 147 | var dateTable = 148 | SelectTable( 149 | label: "Select your date table", 150 | preselect:dateTableCandidate); 151 | 152 | if(dateTable == null) { 153 | Error("You just aborted the script"); 154 | return; 155 | } else { 156 | dateTable.SetAnnotation("@AgulloBernat","Time Intel Date Table"); 157 | }; 158 | 159 | 160 | Column dateTableDateColumnCandidate = null; 161 | 162 | if(dateTable.Columns.Any 163 | (x => x.GetAnnotation("@AgulloBernat") == "Time Intel Date Table Date Column" || x.Name == "Date")){ 164 | dateTableDateColumnCandidate = dateTable.Columns.Where 165 | (x => x.GetAnnotation("@AgulloBernat") == "Time Intel Date Table Date Column" || x.Name == "Date").First(); 166 | }; 167 | 168 | var dateTableDateColumn = 169 | SelectColumn( 170 | dateTable.Columns, 171 | label: "Select the date column", 172 | preselect: dateTableDateColumnCandidate); 173 | 174 | if(dateTableDateColumn == null) { 175 | Error("You just aborted the script"); 176 | return; 177 | } else { 178 | dateTableDateColumn.SetAnnotation("@AgulloBernat","Time Intel Date Table Date Column"); 179 | }; 180 | 181 | Column dateTableYearColumnCandidate = null; 182 | if(dateTable.Columns.Any(x => x.GetAnnotation("@AgulloBernat") == "Time Intel Date Table Year Column" || x.Name == "Year")){ 183 | dateTable.Columns.Where 184 | (x => x.GetAnnotation("@AgulloBernat") == "Time Intel Date Table Year Column" || x.Name == "Year").First(); 185 | }; 186 | 187 | var dateTableYearColumn = 188 | SelectColumn( 189 | dateTable.Columns, 190 | label: "Select the year column", 191 | preselect:dateTableYearColumnCandidate); 192 | 193 | if(dateTableYearColumn == null) { 194 | Error("You just abourted the script"); 195 | return; 196 | } else { 197 | dateTableYearColumn.SetAnnotation("@AgulloBernat","Time Intel Date Table Year Column"); 198 | }; 199 | 200 | 201 | //these names are for internal use only, so no need to be super-fancy, better stick to datpatterns.com model 202 | string ShowValueForDatesMeasureName = "ShowValueForDates"; 203 | string dateWithSalesColumnName = "DateWith" + factTable.Name; 204 | 205 | // '2021-09-24 / B.Agullo / I put the names back to variables so I don't have to tough the script 206 | string factTableName = factTable.Name; 207 | string factTableDateColumnName = factTableDateColumn.Name; 208 | string dateTableName = dateTable.Name; 209 | string dateTableDateColumnName = dateTableDateColumn.Name; 210 | string dateTableYearColumnName = dateTableYearColumn.Name; 211 | 212 | // '2021-09-24 / B.Agullo / this is for internal use only so better leave it as is 213 | string flagExpression = "UNICHAR( 8204 )"; 214 | 215 | string calcItemProtection = ""; //default value if user has selected no measures 216 | string calcItemFormatProtection = ""; //default value if user has selected no measures 217 | 218 | // check if there's already an affected measure table 219 | if(Model.Tables.Any(t => t.GetAnnotation("@AgulloBernat") == "Time Intel Affected Measures Table")) { 220 | //modifying an existing calculated table is not risk-free 221 | Info("Make sure to include measure names to the table " + affectedMeasuresTableName); 222 | } else { 223 | // create calculated table containing all names of affected measures 224 | // this is why you need to enable 225 | if(affectedMeasures != "{") { 226 | 227 | affectedMeasures = affectedMeasures + "}"; 228 | 229 | string affectedMeasureTableExpression = 230 | "SELECTCOLUMNS(" + affectedMeasures + ",\"" + affectedMeasuresColumnName + "\",[Value])"; 231 | 232 | var affectedMeasureTable = 233 | Model.AddCalculatedTable(affectedMeasuresTableName,affectedMeasureTableExpression); 234 | 235 | affectedMeasureTable.FormatDax(); 236 | affectedMeasureTable.Description = 237 | "Measures affected by " + calcGroupName + " calculation group." ; 238 | 239 | affectedMeasureTable.SetAnnotation("@AgulloBernat","Time Intel Affected Measures Table"); 240 | 241 | // this causes error 242 | // affectedMeasureTable.Columns[affectedMeasuresColumnName].SetAnnotation("@AgulloBernat","Time Intel Affected Measures Table Column"); 243 | 244 | affectedMeasureTable.IsHidden = true; 245 | 246 | }; 247 | }; 248 | 249 | //if there where selected or preselected measures, prepare protection code for expresion and formatstring 250 | string affectedMeasuresValues = "VALUES('" + affectedMeasuresTableName + "'[" + affectedMeasuresColumnName + "])"; 251 | 252 | calcItemProtection = 253 | "SWITCH(" + 254 | " TRUE()," + 255 | " SELECTEDMEASURENAME() IN " + affectedMeasuresValues + "," + 256 | " ," + 257 | " ISSELECTEDMEASURE([" + labelAsValueMeasureName + "])," + 258 | " ," + 259 | " SELECTEDMEASURE() " + 260 | ")"; 261 | 262 | 263 | calcItemFormatProtection = 264 | "SWITCH(" + 265 | " TRUE() ," + 266 | " SELECTEDMEASURENAME() IN " + affectedMeasuresValues + "," + 267 | " ," + 268 | " ISSELECTEDMEASURE([" + labelAsFormatStringMeasureName + "])," + 269 | " ," + 270 | " SELECTEDMEASUREFORMATSTRING() " + 271 | ")"; 272 | 273 | 274 | string dateColumnWithTable = "'" + dateTableName + "'[" + dateTableDateColumnName + "]"; 275 | string yearColumnWithTable = "'" + dateTableName + "'[" + dateTableYearColumnName + "]"; 276 | string factDateColumnWithTable = "'" + factTableName + "'[" + factTableDateColumnName + "]"; 277 | string dateWithSalesWithTable = "'" + dateTableName + "'[" + dateWithSalesColumnName + "]"; 278 | string calcGroupColumnWithTable = "'" + calcGroupName + "'[" + columnName + "]"; 279 | 280 | //check to see if a table with this name already exists 281 | //if it doesnt exist, create a calculation group with this name 282 | if (!Model.Tables.Contains(calcGroupName)) { 283 | var cg = Model.AddCalculationGroup(calcGroupName); 284 | cg.Description = "Calculation group for time intelligence. Availability of data is taken from " + factTableName + "."; 285 | cg.SetAnnotation("@AgulloBernat","Time Intel Calc Group"); 286 | }; 287 | 288 | //set variable for the calc group 289 | Table calcGroup = Model.Tables[calcGroupName]; 290 | 291 | //if table already exists, make sure it is a Calculation Group type 292 | if (calcGroup.SourceType.ToString() != "CalculationGroup") { 293 | Error("Table exists in Model but is not a Calculation Group. Rename the existing table or choose an alternative name for your Calculation Group."); 294 | return; 295 | }; 296 | 297 | //adds the two measures that will be used for label as value, label as format string 298 | var labelAsValueMeasure = calcGroup.AddMeasure(labelAsValueMeasureName,""); 299 | labelAsValueMeasure.Description = "Use this measure to show the year evaluated in tables"; 300 | 301 | var labelAsFormatStringMeasure = calcGroup.AddMeasure(labelAsFormatStringMeasureName,"0"); 302 | labelAsFormatStringMeasure.Description = "Use this measure to show the year evaluated in charts"; 303 | 304 | //by default the calc group has a column called Name. If this column is still called Name change this in line with specfied variable 305 | if (calcGroup.Columns.Contains("Name")) { 306 | calcGroup.Columns["Name"].Name = columnName; 307 | 308 | }; 309 | 310 | calcGroup.Columns[columnName].Description = "Select value(s) from this column to apply time intelligence calculations."; 311 | calcGroup.Columns[columnName].SetAnnotation("@AgulloBernat","Time Intel Calc Group Column"); 312 | 313 | 314 | //Only create them if not in place yet (reruns) 315 | if(!Model.Tables[dateTableName].Columns.Any(C => C.GetAnnotation("@AgulloBernat") == "Date with Data Column")){ 316 | string DateWithSalesCalculatedColumnExpression = 317 | dateColumnWithTable + " <= MAX ( " + factDateColumnWithTable + ")"; 318 | 319 | Column dateWithDataColumn = dateTable.AddCalculatedColumn(dateWithSalesColumnName,DateWithSalesCalculatedColumnExpression); 320 | dateWithDataColumn.SetAnnotation("@AgulloBernat","Date with Data Column"); 321 | }; 322 | 323 | if(!Model.Tables[dateTableName].Measures.Any(M => M.Name == ShowValueForDatesMeasureName)) { 324 | string ShowValueForDatesMeasureExpression = 325 | "VAR LastDateWithData = " + 326 | " CALCULATE ( " + 327 | " MAX ( " + factDateColumnWithTable + " ), " + 328 | " REMOVEFILTERS () " + 329 | " )" + 330 | "VAR FirstDateVisible = " + 331 | " MIN ( " + dateColumnWithTable + " ) " + 332 | "VAR Result = " + 333 | " FirstDateVisible <= LastDateWithData " + 334 | "RETURN " + 335 | " Result "; 336 | 337 | var ShowValueForDatesMeasure = dateTable.AddMeasure(ShowValueForDatesMeasureName,ShowValueForDatesMeasureExpression); 338 | 339 | ShowValueForDatesMeasure.FormatDax(); 340 | }; 341 | 342 | 343 | 344 | //defining expressions and formatstring for each calc item 345 | string CY = 346 | "/*CY*/ " + 347 | "SELECTEDMEASURE()"; 348 | 349 | string CYlabel = 350 | "SELECTEDVALUE(" + yearColumnWithTable + ")"; 351 | 352 | 353 | string PY = 354 | "/*PY*/ " + 355 | "IF (" + 356 | " [" + ShowValueForDatesMeasureName + "], " + 357 | " CALCULATE ( " + 358 | " "+ CY + ", " + 359 | " CALCULATETABLE ( " + 360 | " DATEADD ( " + dateColumnWithTable + " , -1, YEAR ), " + 361 | " " + dateWithSalesWithTable + " = TRUE " + 362 | " ) " + 363 | " ) " + 364 | ") "; 365 | 366 | 367 | string PYlabel = 368 | "/*PY*/ " + 369 | "IF (" + 370 | " [" + ShowValueForDatesMeasureName + "], " + 371 | " CALCULATE ( " + 372 | " "+ CYlabel + ", " + 373 | " CALCULATETABLE ( " + 374 | " DATEADD ( " + dateColumnWithTable + " , -1, YEAR ), " + 375 | " " + dateWithSalesWithTable + " = TRUE " + 376 | " ) " + 377 | " ) " + 378 | ") "; 379 | 380 | 381 | string YOY = 382 | "/*YOY*/ " + 383 | "VAR ValueCurrentPeriod = " + CY + " " + 384 | "VAR ValuePreviousPeriod = " + PY + " " + 385 | "VAR Result = " + 386 | "IF ( " + 387 | " NOT ISBLANK ( ValueCurrentPeriod ) && NOT ISBLANK ( ValuePreviousPeriod ), " + 388 | " ValueCurrentPeriod - ValuePreviousPeriod" + 389 | " ) " + 390 | "RETURN " + 391 | " Result "; 392 | 393 | string YOYlabel = 394 | "/*YOY*/ " + 395 | "VAR ValueCurrentPeriod = " + CYlabel + " " + 396 | "VAR ValuePreviousPeriod = " + PYlabel + " " + 397 | "VAR Result = " + 398 | "IF ( " + 399 | " NOT ISBLANK ( ValueCurrentPeriod ) && NOT ISBLANK ( ValuePreviousPeriod ), " + 400 | " ValueCurrentPeriod & \" vs \" & ValuePreviousPeriod" + 401 | " ) " + 402 | "RETURN " + 403 | " Result "; 404 | 405 | string YOYpct = 406 | "/*YOY%*/ " + 407 | "VAR ValueCurrentPeriod = " + CY + " " + 408 | "VAR ValuePreviousPeriod = " + PY + " " + 409 | "VAR CurrentMinusPreviousPeriod = " + 410 | "IF ( " + 411 | " NOT ISBLANK ( ValueCurrentPeriod ) && NOT ISBLANK ( ValuePreviousPeriod ), " + 412 | " ValueCurrentPeriod - ValuePreviousPeriod" + 413 | " ) " + 414 | "VAR Result = " + 415 | "DIVIDE ( " + 416 | " CurrentMinusPreviousPeriod," + 417 | " ValuePreviousPeriod" + 418 | ") " + 419 | "RETURN " + 420 | " Result"; 421 | 422 | string YOYpctLabel = 423 | "/*YOY%*/ " + 424 | "VAR ValueCurrentPeriod = " + CYlabel + " " + 425 | "VAR ValuePreviousPeriod = " + PYlabel + " " + 426 | "VAR Result = " + 427 | "IF ( " + 428 | " NOT ISBLANK ( ValueCurrentPeriod ) && NOT ISBLANK ( ValuePreviousPeriod ), " + 429 | " ValueCurrentPeriod & \" vs \" & ValuePreviousPeriod & \" (%)\"" + 430 | " ) " + 431 | "RETURN " + 432 | " Result"; 433 | 434 | string YTD = 435 | "/*YTD*/" + 436 | "IF (" + 437 | " [" + ShowValueForDatesMeasureName + "]," + 438 | " CALCULATE (" + 439 | " " + CY+ "," + 440 | " DATESYTD (" + dateColumnWithTable + " )" + 441 | " )" + 442 | ") "; 443 | 444 | 445 | string YTDlabel = CYlabel + "& \" YTD\""; 446 | 447 | 448 | string PYTD = 449 | "/*PYTD*/" + 450 | "IF ( " + 451 | " [" + ShowValueForDatesMeasureName + "], " + 452 | " CALCULATE ( " + 453 | " " + YTD + "," + 454 | " CALCULATETABLE ( " + 455 | " DATEADD ( " + dateColumnWithTable + ", -1, YEAR ), " + 456 | " " + dateWithSalesWithTable + " = TRUE " + 457 | " )" + 458 | " )" + 459 | ") "; 460 | 461 | string PYTDlabel = PYlabel + "& \" YTD\""; 462 | 463 | 464 | string YOYTD = 465 | "/*YOYTD*/" + 466 | "VAR ValueCurrentPeriod = " + YTD + " " + 467 | "VAR ValuePreviousPeriod = " + PYTD + " " + 468 | "VAR Result = " + 469 | "IF ( " + 470 | " NOT ISBLANK ( ValueCurrentPeriod ) && NOT ISBLANK ( ValuePreviousPeriod ), " + 471 | " ValueCurrentPeriod - ValuePreviousPeriod" + 472 | " ) " + 473 | "RETURN " + 474 | " Result "; 475 | 476 | 477 | string YOYTDlabel = 478 | "/*YOYTD*/" + 479 | "VAR ValueCurrentPeriod = " + YTDlabel + " " + 480 | "VAR ValuePreviousPeriod = " + PYTDlabel + " " + 481 | "VAR Result = " + 482 | "IF ( " + 483 | " NOT ISBLANK ( ValueCurrentPeriod ) && NOT ISBLANK ( ValuePreviousPeriod ), " + 484 | " ValueCurrentPeriod & \" vs \" & ValuePreviousPeriod" + 485 | " ) " + 486 | "RETURN " + 487 | " Result "; 488 | 489 | 490 | 491 | string YOYTDpct = 492 | "/*YOYTD%*/" + 493 | "VAR ValueCurrentPeriod = " + YTD + " " + 494 | "VAR ValuePreviousPeriod = " + PYTD + " " + 495 | "VAR CurrentMinusPreviousPeriod = " + 496 | "IF ( " + 497 | " NOT ISBLANK ( ValueCurrentPeriod ) && NOT ISBLANK ( ValuePreviousPeriod ), " + 498 | " ValueCurrentPeriod - ValuePreviousPeriod" + 499 | " ) " + 500 | "VAR Result = " + 501 | "DIVIDE ( " + 502 | " CurrentMinusPreviousPeriod," + 503 | " ValuePreviousPeriod" + 504 | ") " + 505 | "RETURN " + 506 | " Result"; 507 | 508 | 509 | string YOYTDpctLabel = 510 | "/*YOY%*/ " + 511 | "VAR ValueCurrentPeriod = " + YTDlabel + " " + 512 | "VAR ValuePreviousPeriod = " + PYTDlabel + " " + 513 | "VAR Result = " + 514 | "IF ( " + 515 | " NOT ISBLANK ( ValueCurrentPeriod ) && NOT ISBLANK ( ValuePreviousPeriod ), " + 516 | " ValueCurrentPeriod & \" vs \" & ValuePreviousPeriod & \" (%)\"" + 517 | " ) " + 518 | "RETURN " + 519 | " Result"; 520 | 521 | 522 | string MAT = 523 | " /*TAM*/" + 524 | " IF (" + 525 | " [" + ShowValueForDatesMeasureName + "], " + 526 | " CALCULATE (" + 527 | " SELECTEDMEASURE()," + 528 | " DATESINPERIOD (" + 529 | " " + dateColumnWithTable + " ," + 530 | " MAX ( " + dateColumnWithTable + " )," + 531 | " -1," + 532 | " YEAR" + 533 | " )" + 534 | " " + 535 | " )" + 536 | " )"; 537 | 538 | 539 | string MATlabel = "\"MAT\""; 540 | 541 | string MATminus1 = 542 | " /*TAM*/" + 543 | " IF (" + 544 | " [" + ShowValueForDatesMeasureName + "], " + 545 | " CALCULATE (" + 546 | " SELECTEDMEASURE()," + 547 | " DATESINPERIOD (" + 548 | " " + dateColumnWithTable + "," + 549 | " LASTDATE( DATEADD( " + dateColumnWithTable + ", - 1, YEAR ) )," + 550 | " -1," + 551 | " YEAR" + 552 | " )" + 553 | " )" + 554 | " )"; 555 | 556 | string MATminus1label = "\"MAT-1\""; 557 | 558 | string MATvsMATminus1 = 559 | " /*MAT vs MAT-1*/\r\n" + 560 | " VAR MAT = " + MAT + "\r\n" + 561 | " VAR MAT_1 =" + MATminus1 + "\r\n" + 562 | " RETURN \r\n" + 563 | " IF( ISBLANK( MAT ) || ISBLANK( MAT_1 ), BLANK(), MAT - MAT_1 )"; 564 | 565 | string MATvsMATminus1label = "\"MAT vs MAT-1\""; 566 | 567 | string MATvsMATminus1pct = 568 | " /*MAT vs MAT-1(%)*/" + 569 | " VAR MAT = " + MAT+ "\r\n" + 570 | " VAR MAT_1 =" + MATminus1 + "\r\n" + 571 | " RETURN" + 572 | " IF(" + 573 | " ISBLANK( MAT ) || ISBLANK( MAT_1 )," + 574 | " BLANK()," + 575 | " DIVIDE( MAT - MAT_1, MAT_1 )" + 576 | " )"; 577 | 578 | string MATvsMATminus1pctlabel = "\"MAT vs MAT-1 (%)\""; 579 | 580 | string MMT = String.Format( 581 | @"/*MMT*/ 582 | IF( 583 | [{0}], 584 | CALCULATE( SELECTEDMEASURE( ), DATESINPERIOD( {1}, MAX( {1} ), -1, MONTH ) ) 585 | )",ShowValueForDatesMeasureName,dateColumnWithTable); 586 | 587 | string MMTlabel = "\"MMT\""; 588 | 589 | string MMTminus1 = String.Format( 590 | @"/*MMT*/ 591 | IF( 592 | [{0}], 593 | CALCULATE( SELECTEDMEASURE( ), DATESINPERIOD( {1}, LASTDATE( DATEADD( {1}, -1, MONTH ) ), -1, MONTH ) ) 594 | )",ShowValueForDatesMeasureName,dateColumnWithTable); 595 | 596 | string MMTminus1label = "\"MMT-1\""; 597 | 598 | string MMTvsMMTminus1 = 599 | " /*MMT vs MMT-1*/\r\n" + 600 | " VAR MMT = " + MMT + "\r\n" + 601 | " VAR MMT_1 =" + MMTminus1 + "\r\n" + 602 | " RETURN \r\n" + 603 | " IF( ISBLANK( MMT ) || ISBLANK( MMT_1 ), BLANK(), MMT - MMT_1 )"; 604 | 605 | string MMTvsMMTminus1label = "\"MMT vs MMT-1\""; 606 | 607 | string MMTvsMMTminus1pct = 608 | " /*MMT vs MMT-1(%)*/" + 609 | " VAR MMT = " + MMT+ "\r\n" + 610 | " VAR MMT_1 =" + MMTminus1 + "\r\n" + 611 | " RETURN" + 612 | " IF(" + 613 | " ISBLANK( MMT ) || ISBLANK( MMT_1 )," + 614 | " BLANK()," + 615 | " DIVIDE( MMT - MMT_1, MMT_1 )" + 616 | " )"; 617 | 618 | string MMTvsMMTminus1pctlabel = "\"MMT vs MMT-1 (%)\""; 619 | 620 | 621 | 622 | string MWT = String.Format( 623 | @"/*MWT*/ 624 | IF( 625 | [{0}], 626 | CALCULATE( SELECTEDMEASURE( ), DATESINPERIOD( {1}, MAX( {1} ), -7, DAY ) ) 627 | )",ShowValueForDatesMeasureName,dateColumnWithTable); 628 | 629 | string MWTlabel = "\"MWT\""; 630 | 631 | string MWTminus1 = String.Format( 632 | @"/*MWT*/ 633 | IF( 634 | [{0}], 635 | CALCULATE( SELECTEDMEASURE( ), DATESINPERIOD( {1}, LASTDATE( DATEADD( {1}, -7, DAY ) ), -7, DAY ) ) 636 | )",ShowValueForDatesMeasureName,dateColumnWithTable); 637 | 638 | string MWTminus1label = "\"MWT-1\""; 639 | 640 | string MWTvsMWTminus1 = 641 | " /*MWT vs MWT-1*/\r\n" + 642 | " VAR MWT = " + MWT + "\r\n" + 643 | " VAR MWT_1 =" + MWTminus1 + "\r\n" + 644 | " RETURN \r\n" + 645 | " IF( ISBLANK( MWT ) || ISBLANK( MWT_1 ), BLANK(), MWT - MWT_1 )"; 646 | 647 | string MWTvsMWTminus1label = "\"MWT vs MWT-1\""; 648 | 649 | string MWTvsMWTminus1pct = 650 | " /*MWT vs MWT-1(%)*/" + 651 | " VAR MWT = " + MWT+ "\r\n" + 652 | " VAR MWT_1 =" + MWTminus1 + "\r\n" + 653 | " RETURN" + 654 | " IF(" + 655 | " ISBLANK( MWT ) || ISBLANK( MWT_1 )," + 656 | " BLANK()," + 657 | " DIVIDE( MWT - MWT_1, MWT_1 )" + 658 | " )"; 659 | 660 | string MWTvsMWTminus1pctlabel = "\"MWT vs MWT-1 (%)\""; 661 | 662 | 663 | 664 | string defFormatString = "SELECTEDMEASUREFORMATSTRING()"; 665 | 666 | //if the flag expression is already present in the format string, do not change it, otherwise apply % format. 667 | string pctFormatString = 668 | "IF(" + 669 | "\n FIND( "+ flagExpression + ", SELECTEDMEASUREFORMATSTRING(), 1, - 1 ) <> -1," + 670 | "\n SELECTEDMEASUREFORMATSTRING()," + 671 | "\n \"#,##0.# %\"" + 672 | "\n)"; 673 | 674 | 675 | //the order in the array also determines the ordinal position of the item 676 | string[ , ] calcItems = 677 | { 678 | {"CY", CY, defFormatString, "Current year", CYlabel}, 679 | {"PY", PY, defFormatString, "Previous year", PYlabel}, 680 | {"YOY", YOY, defFormatString, "Year-over-year", YOYlabel}, 681 | {"YOY%", YOYpct, pctFormatString, "Year-over-year%", YOYpctLabel}, 682 | {"YTD", YTD, defFormatString, "Year-to-date", YTDlabel}, 683 | {"PYTD", PYTD, defFormatString, "Previous year-to-date", PYTDlabel}, 684 | {"YOYTD", YOYTD, defFormatString, "Year-over-year-to-date", YOYTDlabel}, 685 | {"YOYTD%", YOYTDpct, pctFormatString, "Year-over-year-to-date%", YOYTDpctLabel}, 686 | {"MAT", MAT, defFormatString, "Moving Anual Total", MATlabel}, 687 | {"MAT-1", MATminus1, defFormatString, "Moving Anual Total -1 year", MATminus1label}, 688 | {"MAT vs MAT-1", MATvsMATminus1, defFormatString, "Moving Anual Total vs Moving Anual Total -1 year", MATvsMATminus1label}, 689 | {"MAT vs MAT-1(%)", MATvsMATminus1pct, pctFormatString, "Moving Anual Total vs Moving Anual Total -1 year (%)", MATvsMATminus1pctlabel}, 690 | {"MMT", MMT, defFormatString, "Moving Monthly Total", MMTlabel}, 691 | {"MMT-1", MMTminus1, defFormatString, "Moving Monthly Total -1 month", MMTminus1label}, 692 | {"MMT vs MMT-1", MMTvsMMTminus1, defFormatString, "Moving Monthly Total vs Moving Monthly Total -1 month", MMTvsMMTminus1label}, 693 | {"MMT vs MMT-1(%)", MMTvsMMTminus1pct, pctFormatString, "Moving Monthly Total vs Moving Monthly Total -1 month (%)", MMTvsMMTminus1pctlabel}, 694 | {"MWT", MWT, defFormatString, "Moving Weekly Total", MWTlabel}, 695 | {"MWT-1", MWTminus1, defFormatString, "Moving Weekly Total -1 week", MWTminus1label}, 696 | {"MWT vs MWT-1", MWTvsMWTminus1, defFormatString, "Moving Weekly Total vs Moving Weekly Total -1 month", MWTvsMWTminus1label}, 697 | {"MWT vs MWT-1(%)", MWTvsMWTminus1pct, pctFormatString, "Moving Weekly Total vs Moving Weekly Total -1 week (%)", MWTvsMWTminus1pctlabel} 698 | }; 699 | 700 | 701 | int j = 0; 702 | 703 | 704 | //create calculation items for each calculation with formatstring and description 705 | foreach(var cg in Model.CalculationGroups) { 706 | if (cg.Name == calcGroupName) { 707 | for (j = 0; j < calcItems.GetLength(0); j++) { 708 | 709 | string itemName = calcItems[j,0]; 710 | 711 | string itemExpression = calcItemProtection.Replace("",calcItems[j,1]); 712 | itemExpression = itemExpression.Replace("",calcItems[j,4]); 713 | 714 | string itemFormatExpression = calcItemFormatProtection.Replace("",calcItems[j,2]); 715 | itemFormatExpression = itemFormatExpression.Replace("","\"\"\"\" & " + calcItems[j,4] + " & \"\"\"\""); 716 | 717 | //if(calcItems[j,2] != defFormatString) { 718 | // itemFormatExpression = calcItemFormatProtection.Replace("",calcItems[j,2]); 719 | //}; 720 | 721 | string itemDescription = calcItems[j,3]; 722 | 723 | if (!cg.CalculationItems.Contains(itemName)) { 724 | var nCalcItem = cg.AddCalculationItem(itemName, itemExpression); 725 | nCalcItem.FormatStringExpression = itemFormatExpression; 726 | nCalcItem.FormatDax(); 727 | nCalcItem.Ordinal = j; 728 | nCalcItem.Description = itemDescription; 729 | 730 | }; 731 | 732 | 733 | 734 | 735 | }; 736 | 737 | 738 | }; 739 | }; 740 | --------------------------------------------------------------------------------