此内容由人工智能翻译,尚未经过人工编辑审核。图像和图表保持其原始语言。
关键要点
- 要点 1。 DAX 用户自定义函数(或 DAX UDF)是集中管理 Visual 计算逻辑的好方法,并可将该逻辑保存在模型中。
- 要点 2。 Visual 计算专用的 DAX 函数可以在 DAX UDF 中使用,只要它们是从 Visual 计算中调用的,就能正常工作。
- 要点 3。 如果你在会修改评估的 Visual 上下文的表达式中使用参数,那么参数的“传递模式”类型就很重要。
本摘要由作者撰写,并非 AI 生成。
用 DAX UDF 复用 Visual 计算
DAX 用户自定义函数(DAX UDF)与 Visual 计算,是近期对 DAX 从业者而言两项非常重要的新功能。 Visual 计算更早推出,常被诟病的一点是:它们是在 Visual 级别定义的,因此无法在不同 Visual 或 Report 之间复用逻辑。 DAX UDF 于 2025 年九月以公开预览版推出,可用于封装 DAX 逻辑,其中也包括只能在 Visual 计算中使用的 DAX。 因此,现在可以将 Visual 计算的逻辑集中到 DAX UDF 中。
本文将演示如何用 Visual 计算解决一个用例,然后再将这段逻辑迁移到 DAX UDF 中,以便在其他 Visual 中复用。 整体流程相当直接,但一如既往,有几个细节需要注意。
坐标轴配置:让组合图的折线始终显示在柱形上方
在某些情况下,我们希望调整坐标轴的最大值和最小值,让图表更易理解。 大多数时候,无法把这些值写死,因为它们需要是动态的。 很长一段时间以来,都可以把该值绑定到一个度量值。 不过,我们可能不希望在语义模型里创建这种一次性的、缺乏分析价值的度量值。 这正是 Visual 计算的用武之地。 这类计算需要的数据都已经在 Visual 里,DAX 也更容易编写,因为不必考虑影子筛选语境等因素。
通过将坐标轴的最大值和最小值绑定到 Visual 计算,我们希望实现如下效果:

第一个需要注意的重要细节是:对于坐标轴或标题,当它们绑定到 Visual 计算时,实际使用的是“总计”行中的值。 为了在该行得到期望的值,我们可能需要使用 COLLAPSE 或 EXPAND 函数修改计算的 Visual 上下文(你可以在 这里 了解更多)。
注意
创建用于定义坐标轴最大值或最小值的 Visual 计算时,实际使用的将是“总计”中的值。
关于该用例的一般考虑
要确保组合图中的折线始终显示在柱形上方,我们需要修改主 Y 轴的最大值,以及次 Y 轴的最大值和最小值。 为此,我们还需要两个额外参数,它们会在其中两个或更多 Visual 计算里使用:一个用于在图表中的最大/最小值周围添加一定留白 padding,另一个用于定义纵轴中分配给折线图的百分比。 这两个参数当然可以在每个计算中单独写死,但那会让后续维护变得困难。 更好的做法是创建包含这些固定值的隐藏 Visual 计算,这样就能在我们要创建的 3 个 Visual 计算中复用。 如果需要集中管理这些配置值,可以先将这两个参数作为常规度量值添加到 Visual 中,然后在创建第一个 Visual 计算后再将其隐藏。
这些 Visual 计算并不算特别难,但确实需要认真推敲。 为了让事情更简单一点,下面两张图将帮助我们理清这些 DAX 表达式。 为便于说明,我们假设该用例中的所有数值都是正数。 这个实现可以进一步改进,使留白更成比例,并兼容负值。但为了清晰起见,我们让示例尽可能简单。


所有 Visual 计算都将创建为隐藏项,因为我们只想用它们来配置坐标轴。 默认情况下,它们都是可见的,因此需要在编辑 Visual 计算时显示的“可视化”窗格中,点击字段名称旁边的小眼睛图标将其隐藏。

添加可跨 Visual 计算复用的隐藏参数
如上一节所述,我们添加两个隐藏的 Visual 计算或度量值,分别用于 padding percent 值和 linechartWeight 值。 如下所示:

主轴最大值的 Visual 计算
添加隐藏参数这一步比较简单。 现在我们来计算 mainAxisMax。 要计算它,我们首先需要取各列中的最大值(图中的 columnsMax)。 接下来,我们需要把它稍微放大一点,为柱形和折线图区域之间留出一些“呼吸空间”(columnsMaxAdj)。最后,我们还需要加上为折线图预留的全部空间,并将其表示为总图表高度的百分比。 这会返回轴要使用的最大值(mainAxisMax)。
一个 Visual Calculations 的实现如下:
mainAxisMax =
VAR columnsMax = CALCULATE(MAXX(ROWS,MAX([Sales Amount],[Total Cost])),EXPANDALL(ROWS))
VAR columnsMaxAdj = columnsMax * (1 + [Padding] )
VAR mainAxisMax = columnsMaxAdj / (1 - [LineChartWeight] )
RETURN mainAxisMax
这个计算的关键是:在 columnsMax 的计算中使用 EXPANDALL,这样就能在总计行中,基于 ROWS 所用字段的组合来计算行级最大值。 用 EXPANDALL 而不是 EXPAND,意味着总计处的最大值会基于用于行的所有字段组合来计算。 如果只有一列,两者没有区别;但如果我们用了“年”和“月”,在总计处就会得到跨年份的最大值,而不是跨“年 + 月”的最大值——而后者才是这个场景需要的。 额外好处是:这个计算完全不引用用于行的字段,所以即使你通过字段参数更改图表粒度,它也能正常工作。
注意
Visual calcs 始终会考虑添加到该 Visual 的所有字段,无论图表处于上钻还是下钻状态。 因此,如果月份被折叠、图表以“年”级别显示,Visual Calculation 的总计不会改变,折线就不会再位于柱形上方了。 当使用字段参数切换多个字段时,默认会以折叠状态显示,这可能会导致一些不符合预期的行为。
现在,点击轴最大值的 fx 按钮,就可以把该值绑定到 Visual Calculation,它会取总计处的值。

次轴最大值的 Visual Calculation
折线图的最大值可能是最简单的:我们只需要处理一点 padding(甚至也可以不加)。
secondaryAxisMax =
VAR lineMax = CALCULATE(MAXX(ROWS,[Margin %]),EXPANDALL(ROWS))
VAR lineMaxAdj = lineMax * (1 + [Padding] )
RETURN lineMaxAdj
次轴最小值的 Visual Calculation
接下来三者里最棘手的,是次轴最小值。 为此,我们需要先计算折线图区域的实际高度,然后使用 lineChartWeight 参数计算整个次轴的高度,最后用轴最大值减去该高度,从而得到轴最小值。 我们可以复用次轴最大值的 Visual Calculation,但也可以用同样的表达式重新计算一遍,让两者彼此独立,也更容易理解。
secondaryAxisMin =
VAR LineMax = CALCULATE(MAXX(ROWS,[Margin %]),EXPANDALL(ROWS))
VAR LineMaxAdj = LineMax * (1 + [Padding])
VAR LineMin = CALCULATE(MINX(ROWS,[Margin %]),EXPANDALL(ROWS))
VAR LineMinAdj = LineMin * (1 - [Padding])
VAR LineHeight = LineMaxAdj - LineMinAdj
VAR SecondaryAxisHeight = LineHeight / [LineChartWeight]
VAR SecondaryAxisMin = LineMaxAdj - SecondaryAxisHeight
RETURN SecondaryAxisMin
如果我们现在把次轴的最大值和最小值绑定到这些隐藏的 Visual Calculations,就应该会看到类似这样的效果:

至少对折线来说,强烈建议开启数据标签,并隐藏次轴的刻度值和标题。
将 Visual Calculation 转换为 DAX UDF
效果看起来很不错,但如果我们想在 Report 的其他地方复用同样的效果,甚至在另一份 Report 里复用,该怎么办? 这就是 DAX UDFs 的用武之地。 要创建 DAX UDF,你可以通过“模型视图”、“DAX 查询视图”、“TMDL 视图”,当然也可以在 Tabular Editor 中完成。 函数创建完成后,在这个模型里任何用到 DAX 的地方都能调用它;如果是 Visual Calculations,那么任何连接到该模型的 Report 都能用。 如你所料,如果函数包含只能在 Visual Calculations 中使用的表达式,那么从其他位置调用就会报错。 DAX UDFs 可以复制到其他语义模型中;只要没有显式引用模型名或 Visual Calculation 名称,它们就能正常工作。
创建任何 DAX UDF 时,你需要为其命名、指定参数,然后提供要计算的表达式。 对用于 Visual Calculations 的 DAX UDF 来说也没有区别。 将 mainAxisMax 这个 Visual Calculation 的逻辑写成 DAX UDF,大概会是这样:
PrimaryAxisMax =
(
maxColumnExpression : expr,
padding,
lineChartWeight
) =>
VAR ColumnsMax =
CALCULATE(
MAXX(ROWS,maxColumnExpression),
EXPANDALL(ROWS)
)
VAR ColumnsMaxAdj = ColumnsMax * (1 + padding )
VAR MainAxisMax = ColumnsMaxAdj / (1- lineChartWeight )
RETURN MainAxisMax
而从 Visual Calculation 调用该 DAX UDF 会是这样:
mainAxisMax2 =
PrimaryAxisMax(
MAX([Sales Amount],[Total Cost]),
[Padding],
[LineChartWeight]
)
不出所料,这两个 Visual Calculation 会返回相同的值:

注意
在 2025 年十一月版的 Power BI Desktop 中,IntelliSense 仍然无法在 Visual Calculations 中识别 DAX UDFs,也无法在 DAX UDFs 中识别 Visual Calculation 函数;但语法是有效的,也能按预期运行。
DAX UDF 的表达式确实与原始的 Visual Calculation 非常接近,但有几个方面值得讨论。
第一个选择是:避免创建两个参数来传入 Sales Amount 和 Total Cost 两个度量值(例如 visualColumn1 和 visualColumn2,因为它们已不再是度量值)。 相反,这里选择传入一个表达式,该表达式本身就会返回两者中的最大值。 原因是:当 Visual 的列数不同,我们可能希望在略有差异的场景中复用这个 DAX UDF。 由于截至本文写作时,DAX UDFs 还无法传入可变数量的参数,这是最合理的变通方案。 你可以先创建一个 Visual Calculation,按行预先计算这个最大值;也可以直接用表达式来做。 计算三个及以上数字的最大值,最紧凑的写法是使用一个匿名表,例如:
MAXX( {[a], [b], [c]}, [Value])
第二个需要注意的细节是:maxColumnExpression 必须使用“expr”传参模式作为参数。 这表示该值会在使用它的表达式中被求值,而不是作为一个静态值传入。 如果完全不指定传参模式,就会默认如此——所以这一点别忘了。
注意
在 DAX UDF 中,凡是在 CALCULATE 的计算表达式里使用到的参数,都需要使用“expr”传参模式。 这一点对 Visual Calculations 和常规 DAX 同样适用。 这同样适用于其他底层使用 CALCULATE 的函数,例如当 COLLAPSE 和 EXPAND 不作为 CALCULATE 修饰符使用时(点此了解更多)。
secondaryAxisMax 这一 Visual Calculation 的逻辑非常简单,因此得到的 DAX UDF 也相当直观:
SecondaryAxisMax =
(
lineMaxExpression:expr,
padding
) =>
VAR lineMax =
CALCULATE(
MAXX(ROWS,lineMaxExpression),
EXPANDALL(ROWS)
)
VAR lineMaxAdj = lineMax * (1 + padding )
RETURN lineMaxAdj
两者返回的值也完全一致:

这里唯一值得一提的是:图表中也可能有不止一条折线,因此我们需要传入一个表达式,在可视化矩阵的每一行返回所有折线中的最大值。
接着看 secondaryAxisMin,原始的 Visual Calculation 更复杂,因此对应的 DAX UDF 也更复杂,如下所示:
SecondaryAxisMin =
(
lineMaxExpression: expr,
lineMinExpression: expr,
padding,
lineChartWeight
) =>
VAR LineMax = CALCULATE(MAXX(ROWS,lineMaxExpression),EXPANDALL(ROWS))
VAR LineMaxAdj = LineMax * (1 + padding)
VAR LineMin = CALCULATE(MINX(ROWS,lineMinExpression),EXPANDALL(ROWS))
VAR LineMinAdj = LineMin * (1 - padding)
VAR LineHeight = LineMaxAdj - LineMinAdj
VAR SecondaryAxisHeight = LineHeight / lineChartWeight
VAR SecondaryAxisMin = LineMaxAdj - SecondaryAxisHeight
RETURN SecondaryAxisMin
是的,它们的返回值同样一致:

到这里,我们已经没有更多需要说明的内容了。 不过我们可以注意到:虽然单条折线图只需要一个参数就能处理,但如果希望该 DAX UDF 能用于包含多条折线的图表,我们就需要提供一个用于求 MAX 的表达式,以及另一个用于求最小值的表达式。 希望未来能支持可变数量的参数,这样这些计算也能封装到 DAX UDF 中。
另外,对于这个更复杂的 DAX UDF,我们遵循了 SQLBI 的命名规范:变量使用 PascalCase,参数使用 camelCase,以提升代码可读性。
关于 Visual Calculation DAX UDF 的可复用性
从技术透视来看,Visual Calculation DAX UDF 似乎——按定义——与模型无关,因此很适合在 daxlib.org 或其他用于此目的的 repository 代码仓库中分享。 不过,正如 这篇文章 所解释的那样,通过名称进行引用也会让函数无法做到 100% 与模型无关。因此,我们想要分享的 Visual Calculation DAX UDF 不应直接按名称引用特定的可视化列;所有列引用都应通过参数传入。
然而,虽然 daxlib.org 上的所有 DAX UDF 都与模型无关,但这并不意味着它们都能用于 Visual Calculation。 任何需要模型中的度量值、表或列才能运行的 DAX UDF,都无法在 Visual Calculation 里调用,因为 Visual Calculation 无法访问模型,只能访问 Visual。
结论
DAX UDF 提供了一种在语义模型中集中 管理 Visual Calculations 逻辑的方法。 所有 Visual Calculations 专用的 DAX 函数都可以在 UDF 中使用,这会让 Visual Calculation 的表达式紧凑得多。
此外,如果它们不直接引用视觉对象矩阵的列,甚至还能在不同的 Report 和 Visual 之间复用。
不过,和其他任何 DAX UDF 一样,我们仍需要关注细节,例如每个参数的传递模式,并考虑函数可能被使用的不同场景。