我们有许多著名的图表类型:条形图、甜甜圈图、线图、饼图,你可以说它是。所有流行的图表库都支持这些。还有一些甚至没有名字的图表类型。看看这个带有堆叠(嵌套)方块的梦幻般的图表,它可以帮助可视化相对大小,或者不同的值如何相互比较。

我们在做什么
在没有任何互动的情况下,创建这个设计是相当直接的。一种方法是将元素(例如SVG<rect> 元素,甚至HTML divs)以递减的方式堆叠起来,使其所有的左下角接触到同一点。

但是,一旦我们引入一些互动性,事情就变得棘手了。情况应该是这样的。当我们把鼠标移到其中一个形状上时,我们希望其他的形状能淡出并移开。
我们将使用矩形和遮罩来创建这些不规则的形状--文字<svg> ,并使用<rect> 和<mask> 元素。如果你对蒙版完全陌生,你就来对地方了。这是一篇介绍性的文章。如果你比较老练,那么也许这种切割效果是你可以带走的一个技巧。
现在,在我们开始之前,你可能想知道是否有比SVG更好的替代方法来使用自定义形状。这绝对是一种可能性!但是,用。但是,用<path> 绘制形状可能会让人望而生畏,甚至会变得很混乱。所以,我们要用 "更容易 "的元素来获得同样的形状和效果。
例如,下面是我们要用一个<path> 来表示最大的蓝色形状。
<svg viewBox="0 0 320 320" width="320" height="320">
<path d="M320 0H0V56H264V320H320V0Z" fill="#264653"/>
</svg>

如果0H0V56… 对你来说没有任何意义,请查看"The SVG path Syntax:An Illustrated Guide",以了解对该语法的详尽解释。
图表的基本原理
给定一个这样的数据集。
type DataSetEntry = {
label: string;
value: number;
};
type DataSet = DataSetEntry[];
const rawDataSet: DataSet = [
{ label: 'Bad', value: 1231 },
{ label: 'Beginning', value: 6321 },
{ label: 'Developing', value: 10028 },
{ label: 'Accomplished', value: 12123 },
{ label: 'Exemplary', value: 2120 }
];
......我们希望最终得到一个这样的SVG。
<svg viewBox="0 0 320 320" width="320" height="320">
<rect width="320" height="320" y="0" fill="..."></rect>
<rect width="264" height="264" y="56" fill="..."></rect>
<rect width="167" height="167" y="153" fill="..."></rect>
<rect width="56" height="56" y="264" fill="..."></rect>
<rect width="32" height="32" y="288" fill="..."></rect>
</svg>
确定最高值
稍后我们就会明白为什么我们需要最高值。我们可以使用Math.max() 来得到它。它接受任何数量的参数并返回一个集合中的最高值。
const dataSetHighestValue: number = Math.max(
...rawDataSet.map((entry: DataSetEntry) => entry.value)
);
由于我们有一个小的数据集,我们可以直接告诉我们将得到12123 。
计算矩形的尺寸
如果我们看一下设计图,代表最高值的矩形 (12123) 覆盖了图表的整个区域。
我们任意选择了320 作为SVG的尺寸。由于我们的矩形是正方形,所以宽度和高度是相等的。我们怎样才能使12123 与320 相等?那些不那么 "特殊 "的值呢?6321 矩形有多大?
换个角度问,我们如何将一个数字从一个范围([0, 12123] )映射到另一个范围([0, 320] )?或者,用更多的数学术语来说,我们如何将一个变量扩展到[a, b] 的一个区间?
为了我们的目的,我们将这样实现这个函数。
const remapValue = (
value: number,
fromMin: number,
fromMax: number,
toMin: number,
toMax: number
): number => {
return ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin) + toMin;
};
remapValue(1231, 0, 12123, 0, 320); // 32
remapValue(6321, 0, 12123, 0, 320); // 167
remapValue(12123, 0, 12123, 0, 320); // 320
由于我们在代码中把数值映射到同一个区间,我们可以创建一个封装函数,而不是一遍又一遍地传递最小值和最大值。
const valueRemapper = (
fromMin: number,
fromMax: number,
toMin: number,
toMax: number
) => {
return (value: number): number => {
return remapValue(value, fromMin, fromMax, toMin, toMax);
};
};
const remapDataSetValueToSvgDimension = valueRemapper(
0,
dataSetHighestValue,
0,
svgDimension
);
我们可以像这样使用它。
remapDataSetValueToSvgDimension(1231); // 32
remapDataSetValueToSvgDimension(6321); // 167
remapDataSetValueToSvgDimension(12123); // 320
创建和插入DOM元素
剩下的就是对DOM的操作了。我们必须创建<svg> 和五个<rect> 元素,设置它们的属性,并将它们追加到DOM中。我们可以用基本的createElementNS,setAttribute, 和appendChild 函数来完成这些工作。
请注意,我们使用的是createElementNS ,而不是更常见的createElement 。这是因为我们正在处理一个SVG。HTML和SVG元素有不同的规格,所以它们属于不同的命名空间URI。碰巧的是,createElement 方便地使用了HTML命名空间。所以,为了创建一个SVG,我们必须要这么啰嗦。
document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGSVGElement;
当然,我们可以创建另一个辅助函数。
const createSvgNSElement = (element: string): SVGElement => {
return document.createElementNS('http://www.w3.org/2000/svg', element);
};
当我们将矩形追加到DOM中时,我们必须注意它们的顺序。否则,我们将不得不明确地指定z-index 。第一个矩形必须是最大的,而最后一个矩形必须是最小的。最好在循环之前对数据进行排序。
const data = rawDataSet.sort(
(a: DataSetEntry, b: DataSetEntry) => b.value - a.value
);
data.forEach((d: DataSetEntry, index: number) => {
const rect: SVGRectElement = createSvgNSElement('rect') as SVGRectElement;
const rectDimension: number = remapDataSetValueToSvgDimension(d.value);
rect.setAttribute('width', `${rectDimension}`);
rect.setAttribute('height', `${rectDimension}`);
rect.setAttribute('y', `${svgDimension - rectDimension}`);
svg.appendChild(rect);
});
坐标系统从左上角开始;那是[0, 0] 。我们总是要从左边开始画矩形。x 属性控制水平位置,默认为0 ,所以我们不需要设置它。y 属性控制垂直位置。
为了给人以视觉上的印象,即所有的矩形都是从接触其左下角的同一点开始的,我们必须把这些矩形往下推。推了多少呢?确切地说,就是矩形不被填满的那个量。而这个值就是图表的尺寸和特定矩形之间的差异。如果我们把所有的比特放在一起,我们最终会得到这个结果。
CodePen嵌入回退
我们已经用CSS为这个演示添加了动画的代码。
切出的矩形
我们要把我们的矩形变成不规则的形状,看起来有点像数字7,或者是旋转180度的字母L。

如果我们把注意力集中在 "缺失的部分",那么我们可以看到它们是我们已经在处理的相同矩形的切口。

我们要隐藏这些剪影。这就是我们最终要得到我们想要的L形的方法。
遮罩101
遮罩是你定义并随后应用于一个元素的东西。通常,mask ,在它所属的<svg> 元素中被内联。而且,一般来说,它应该有一个唯一的id ,因为我们必须引用它,以便将遮罩应用于一个元素。
<svg>
<mask id="...">
<!-- ... -->
</mask>
</svg>
在<mask> 标签中,我们把作为实际掩码的形状放进去。我们还将mask 属性应用到元素上。
<svg>
<mask id="myCleverlyNamedMask">
<!-- ... -->
</mask>
<rect mask="url(#myCleverlyNamedMask)"></rect>
</svg>
这不是定义或应用遮罩的唯一方法,但对于这个演示来说,它是最直接的方法。在写代码生成遮罩之前,让我们做一下实验。
我们说过,我们要覆盖与现有矩形的尺寸相匹配的切口区域。如果我们取最大的元素,并应用前一个矩形作为遮罩,我们最终会得到这样的代码。
<svg viewBox="0 0 320 320" width="320" height="320">
<mask id="theMask">
<rect width="264" height="264" y="56" fill=""></rect>
</mask>
<rect width="320" height="320" y="0" fill="#264653" mask="url(#theMask)"></rect>
</svg>
遮罩内的元素需要一个fill 的值。那应该是什么呢?根据我们选择的fill 值(颜色),我们会看到完全不同的结果。
白色填充
如果我们用一个white 的值来表示fill ,那么我们会得到这个结果。

现在,我们的大矩形与遮蔽矩形的尺寸相同。这不是我们想要的结果。
黑色填充
如果我们用一个black 的值来代替,那么它看起来像这样。

我们没有看到任何东西。这是因为用黑色填充的东西是不可见的。我们用white 和black 填充来控制遮罩的可见性。虚线是作为一种视觉帮助,用来参考不可见区域的尺寸。
灰色填充
现在让我们使用介于白色和黑色之间的东西,例如gray 。

它既不是完全不透明的,也不是固体的;它是透明的。所以,现在我们知道我们可以通过使用不同于white 和black 的值来控制这里的 "可见度",这是一个很好的技巧,可以放在我们的后袋里。
最后一点
下面是我们到目前为止所涉及和学到的关于掩码的内容。
<mask>里面的元素控制着蒙版区域的尺寸。- 我们可以使遮罩区的内容可见、不可见或透明。
我们只用了一个遮罩的形状,但就像任何通用的HTML标签一样,我们可以在里面嵌套任意多的子元素。事实上,实现我们想要的东西的诀窍是使用两个SVG<rect> 元素。我们必须把它们一个一个地叠加起来。
<svg viewBox="0 0 320 320" width="320" height="320">
<mask id="maskW320">
<rect width="320" height="320" y="0" fill="???"></rect>
<rect width="264" height="264" y="56" fill="???"></rect>
</mask>
<rect width="320" height="320" y="0" fill="#264653" mask="url(#maskW320)"></rect>
</svg>
我们的一个遮蔽矩形被填充为white ;另一个被填充为black 。即使我们知道这些规则,我们也要尝试一下各种可能性。
<mask id="maskW320">
<rect width="320" height="320" y="0" fill="black"></rect>
<rect width="264" height="264" y="56" fill="white"></rect>
</mask>

<mask> 是最大元素的尺寸,而最大的元素被填充为black 。这意味着该区域下的所有东西都是不可见的。而在较小的矩形下的所有东西都是可见的。
现在让我们做翻转的事情,black 矩形在上面。
<mask id="maskW320">
<rect width="320" height="320" y="0" fill="white"></rect>
<rect width="264" height="264" y="56" fill="black"></rect>
</mask>

这就是我们想要的!
最大的白色填充矩形下的所有东西都是可见的,但较小的黑色矩形在它上面(在Z轴上离我们更近),掩盖了那部分。
生成遮罩
现在我们知道我们要做什么了,我们可以相对容易地创建遮罩。这与我们一开始生成彩色矩形的方式类似--我们创建一个二级循环,在其中创建mask 和两个rects。
这一次,我们没有直接将rects追加到SVG中,而是将其追加到mask 。
data.forEach((d: DataSetEntry, index: number) => {
const mask: SVGMaskElement = createSvgNSElement('mask') as SVGMaskElement;
const rectDimension: number = remapDataSetValueToSvgDimension(d.value);
const rect: SVGRectElement = createSvgNSElement('rect') as SVGRectElement;
rect.setAttribute('width', `${rectDimension}`);
// ...setting the rest of the attributes...
mask.setAttribute('id', `maskW${rectDimension.toFixed()}`);
mask.appendChild(rect);
// ...creating and setting the attributes for the smaller rectangle...
svg.appendChild(mask);
});
data.forEach((d: DataSetEntry, index: number) => {
// ...our code to generate the colored rectangles...
});
我们可以使用索引作为掩码的ID,但这似乎是一个更可读的选项,至少对我来说是这样。
mask.setAttribute('id', `maskW${rectDimension.toFixed()}`); // maskW320, masW240, ...
至于在遮罩中添加较小的矩形,我们可以很容易地获得我们需要的值,因为我们之前将矩形的值从高到低排序。这意味着循环中的下一个元素是较小的矩形,也就是我们应该参考的那个。而我们可以通过它的索引来实现这一点。
// ...previous part where we created the mask and the rectangle...
const smallerRectIndex = index + 1;
// there's no next one when we are on the smallest
if (data[smallerRectIndex] !== undefined) {
const smallerRectDimension: number = remapDataSetValueToSvgDimension(
data[smallerRectIndex].value
);
const smallerRect: SVGRectElement = createSvgNSElement(
'rect'
) as SVGRectElement;
// ...setting the rectangle attributes...
mask.appendChild(smallerRect);
}
svg.appendChild(mask);
剩下的就是给我们原来循环中的彩色矩形添加mask 属性。它应该符合我们选择的格式。
rect.setAttribute('mask', `url(#maskW${rectDimension.toFixed()})`); // maskW320, maskW240, ...
最后的结果
我们就完成了!我们已经成功地制作了一个由嵌套方块组成的图表。它甚至在鼠标悬停时也会分开。而这一切只需要一些SVG使用<mask> 元素来画出每个方块的切割区域。