最近帮大佬的幻灯片做了点杂事,乱七八糟的东西总结一下,不过可能也没啥卵用。
火焰图
用到了很多个火焰图,所以魔改了一通生成火焰图的 perl 脚本。
主要是:
- 从下往上 grow 的动画效果,并且隔 2s 后重复播放动画
- 地图式 zoom 功能,鼠标滚轮
- zoom 的同时,需要把文字逐步显示出来,像 map 一样
- 本来加了个跟随鼠标的 tooltip,不过后来还是去掉了
grow 动画
flamegraph.pl
实际上是通过读取数据文件来生成一个单独的 svg document。
$ ./flamegraph.pl data.cbt > output.svg
生成的 svg 中主要就是一堆的方块 <rect>
和文本 <text>
元素的组合,具体怎么计算 rect 的位置大小等都不需要我仔细了解。
但既然每个方块的位置大小都是直接计算出来,那么 grow 动画的逻辑就很简单了:
给每个 rect 加一个简单的 fadeIn animation,但根据方块的高度来决定 animation-delay
,效果就足够了。
至于隔 2s 后重复播放动画,本来我以为需要用上 animation-iteration-count: infinite
,
但实际上 infinite 似乎没办法在每两次动画中间加上间隔。所以开始想方法手动 trigger 动画重播。
function reset_animation() {
var el = document.getElementById('animated');
el.style.animation = 'none';
el.offsetHeight; /* trigger reflow */
el.style.animation = null;
}
基本的方法就是先把元素的 animation 值去掉,触发 reflow,再把 animation 值还原回去,让动画重播。
不过在我的实际应用中,答案里的 magic el.offsetHeight;
没有起到效果,尝试了多次之后,
我直接在去掉 animation 值之后 setTimeout
了一小段时间后再赋值回去也正确触发了 reflow,让动画重播,具体是啥原因呢 🤔
zoom 功能
原本的火焰图自带的 zoom 功能是点击之后放大某一个 rect,依次改变其他相关 rect,大佬表示不满意,想要 Google Map 那种地图式的放大,但不改变初始火焰图整体形态。 毫无头绪的搜索了一阵后,用上了 d3.js。直接套用自带 zoom 函数就好(特别 nice:
参考例子:
<script type="text/javascript" xlink:href="https://d3js.org/d3.v4.min.js"></script>
<script type="text/ecmascript">
<![CDATA[
var outerGroup = d3.select("g.outer_g");
var rectGroup = d3.select("g.func_g");
outerGroup
.call(d3.zoom()
.scaleExtent([1, 100]) // 最大和最小 scale 的值
.on("zoom", zooming) // 正在 zoom
.on("end", zoomed)); // zoom 结束
function zooming() {
// 将 transform 的值添加到每一个方块上
// 比如: transform="translate(126.50096885580297,-50.13540780123475) scale(1.3736362334296233)"
rectGroup.attr("transform", d3.event.transform);
}
function zoomed() {}
]]>
</script>
d3 selection 其实完全可以理解为类似 jQuery 的元素选择器 $('.class-name')
$('span#id')
之类的。
还比较 easy 地做到了 zoom 呢~ 然后发现新问题,文字大小也会随着 scale 一起放大…所以文字内容并不会比正常大小的时候变多….emmmmm….
原本的点击 zoom 处理方法是计算 rect 宽度,以及文字所需要的宽度,然后 JS 里计算可以显示多少 substring。
所以还是沿用这个方法,只是在 scale 之后,想要正确计算文字的宽度,
必须改变 font-size
,否则怎么计算都是和初始一样。
改了下 zooming
函数,在 zooming 时改变文字 font-size
, zoom 结束时计算更新文字应该显示的内容。
var baseFontSize = 12;
function zooming() {
rectGroup.attr("transform", d3.event.transform);
var nodeList = rectGroup._groups[0];
for (var i = 0; i < nodeList.length; i++) {
var g = nodeList[i];
var transform = g.attributes["transform"].value;
// get the scale number
var scale = parseFloat(transform.match(/scale\\((.*)\\)/)[1]);
var t = find_child(g, "text");
if (scale > 1) {
// lower the font-size by the scale number
t.attributes['font-size'].value = baseFontSize / scale;
} else {
t.attributes['font-size'].value = baseFontSize;
}
}
}
function zoomed() {
var scale;
var nodeList = rectGroup._groups[0];
for (var i = 0; i < nodeList.length; i++) {
var g = nodeList[i];
var transform = g.attributes["transform"].value;
update_text(g);
}
}
原本计算如何更新显示的文字的函数长这样:
// avg width relative to fontsize
var fontWidth = 0.59;
function update_text(e) {
var r = find_child(e, "rect");
var t = find_child(e, "text");
var w = parseFloat(r.attributes["width"].value) -3;
var txt = find_child(e, "title").textContent.replace(/\\([^(]*\\)\$/,"");
t.attributes["x"].value = parseFloat(r.attributes["x"].value) +3;
// Smaller than this size won't fit anything
if (w < 2 * baseFontSize * fontWidth) {
t.textContent = "";
return;
}
t.textContent = txt;
// Fit in full text width
if (/^ *\$/.test(txt) || t.getSubStringLength(0, txt.length) < w)
return;
for (var x=txt.length-2; x>0; x--) {
if (t.getSubStringLength(0, x+2) <= w) {
t.textContent = txt.substring(0,x) + "..";
return;
}
}
t.textContent = "";
}
原作者定义了一个 fontWidth
的常量,按我的理解是一个平均 ratio,也就是平均一个字母的宽度相对于它 font-size 的 ratio。就比如说 12px font-size
的字母平均宽度为 12px * 0.59 = 7.08px
。
这个 ratio 从哪里得来的我没找到。然而这个 ratio 在文字被 scale 放大之后就不太对了。
按照我前面在放大之后将 font-size 改小来希望可以显示出更多文字来看,如果文字是 12px,scale 是 2,那么放大后文字的 size 被缩小设置为 6px,当然实际看上去的大小并不是。
那么平均一个字母的宽度就是 6px * 0.59 = 3.54px
,单纯比较这个数字和方块的宽度的话,文字一下就变成全部显示出来了,此时看到的结果就是其实文字都超出 rect 的宽度了。
前面的一个错误是字母在设置成 6px 之后,实际上计算宽度时应该将 scale 乘回去,也就是 6px * 0.59 * 2 = 7.08px
。所以实际上不管如何 scale,都是希望文字的实际宽度没有大的变化的。觉得文字变大变小了,是因为其他图形的 scale 变化而产生的相对的视觉效果。
观察和计算了一下不同 scale 时,字母的平均宽度之后,我保守的直接把 fontWidth = 6.7
了,比前面的 7.08 小一丢丢。
// avg character width regardless of font-size and scale
var fontWidth = 6.7;
function update_text(e) {
var r = find_child(e, "rect");
var t = find_child(e, "text");
var transform = e.attributes["transform"].value;
// get scale number
var scale = transform ? parseInt(transform.match(/scale\\((.*)\\)/)[1]) : 1;
var w = parseFloat(r.attributes["width"].value) * scale -3;
var txt = find_child(e, "title").textContent.replace(/\\([^(]*\\)\$/,"");
t.attributes["x"].value = parseFloat(r.attributes["x"].value) +3;
// Smaller than this size won't fit anything
if (w < 3 * fontWidth) {
t.textContent = "";
return;
}
t.textContent = txt;
// Fit in full text width
if (/^ *\$/.test(txt) || fontWidth * txt.length < w)
return;
for (var x=txt.length-2; x>0; x--) {
if ((x + 2) * fontWidth < w) {
t.textContent = txt.substring(0,x) + "..";
return;
}
}
t.textContent = "";
}
tooltip
用 d3 加 tooltip 也是很简单的呢:
首先准备一个 tooltip 的 div,可以用 d3 创建,也可以直接在 svg 或 html 里添加好。然后用 d3 监听 mouseover/mousemove/mouseout 事件即可控制 tooltip 的样式及位置来显示内容了。
outerGroup
.on("mouseover", function () {
var target = event.target;
var text = get_text(target);
// show the tooltip
tooltip.style("opacity", .9);
tooltip.select("p").text(text);
})
.on("mousemove", function () {
// tooltip move with the mouse
tooltip.attr("x", event.pageX - 10)
.attr("y", event.pageY + 10);
})
.on("mouseout", function () {
// hide the tooltip
tooltip.style("opacity", 0);
});