D3.js-现金流动态瀑布图

1

写需求

1
2
3
4
5
6
7
8
D3.js画一个柱状图
柱状图有1000个数据
但是画布范围内只显示5个数据
然后随着时间的增长,数据从左到右动态更新,
注意每一个时间周期内都只显示5个数据,要通过连贯的动画将这1000个数据从左到右展示全

并且y轴的刻度范围是根据当前展示的5个数据来设定的,当前展示的5个数据的最大值和最小值作为y轴的刻度范围。
Y轴的刻度范围也是随着动画的播放而动态变更的。
1
3个legend图例,每个图例包括一个圆角矩形和文本,矩形和文本要水平对齐;三个图例同一排横向排布: operating: '#ffc200', investment: '#6081ea', financing: '#dd70e0',
1
2
以初始的绘图区域为范围,给图表画一个10x10的背景网格线,颜色淡一点,不要遮住柱状图了;只在初始绘制一次即可,更新柱状图的时候不要重新绘制
不用根据x和y的刻度数量来画网格数量,网格固定是10x10的

DEBUG

查询知识

1
D3.js如何获取2个柱子之间的宽度?

在 D3.js 中,可以使用 scaleBand 的 bandwidth 方法来获取每个柱子的宽度。要获取两个柱子之间的宽度(即柱子之间的间距),可以使用 scaleBand 的 step 方法,然后减去 bandwidth 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义x轴的比例尺
const x = d3
.scaleBand()
.domain(data.map((d) => d.name))
.range([0, width])
.paddingInner(padding)
.paddingOuter(padding);

// 获取每个柱子的宽度
const barWidth = x.bandwidth();
console.log('Bar width:', barWidth);

// 获取两个柱子之间的宽度
const barSpacing = x.step() - barWidth;
console.log('Bar spacing:', barSpacing);

在这个示例中:

使用 scaleBand 来定义 x 轴的比例尺。
使用 bandwidth 方法来获取每个柱子的宽度。
使用 step 方法来获取两个柱子之间的步长,然后减去 bandwidth 的值来计算两个柱子之间的间距。

1
如何找到同一层级的元素?

要选择父元素,可以使用 node().parentNode。

1
2
3
4
updates.select('path').attr('transform', function (d) {
// 找到同一层级的rect元素
const previousRect = d3.select(this.parentNode).select('rect');
});
1
2
// 如何将x轴的文本标签旋转45度:
const xAxis = d3.axisBottom(x).tickFormat((d, index) => {});

要将 x 轴的文本标签旋转 45 度,可以在调用 axisBottom 后,选择所有的文本标签并应用旋转变换。以下是如何实现的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const xAxis = d3
.axisBottom(x)
.tickFormat((d, index) => {
return d;
})
.tickSize(0);

// 添加 x 轴到图表
chart
.append('g')
.attr('class', 'x axis')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
.selectAll('text') // 选择所有的文本标签
.style('text-anchor', 'end') // 设置文本锚点
.attr('dx', '-0.8em') // 设置 x 偏移
.attr('dy', '0.15em') // 设置 y 偏移
.attr('transform', 'rotate(-45)'); // 旋转文本标签
1
如何让绘图区域的两侧留出一些空白?现在左侧的柱子和y轴叠一起了

要在绘图区域的两侧留出一些空白,可以调整 scaleBand 的 paddingInner 和 paddingOuter 属性。paddingOuter 属性可以在比例尺的两端添加一些空白,从而避免柱子和 y 轴重叠。

使用 paddingInner(0.1) 设置柱子之间的内部填充。
使用 paddingOuter(0.2) 设置比例尺两端的外部填充,从而在绘图区域的两侧留出一些空白。

1
2
3
4
5
6
const x = d3
.scaleBand()
.domain(displayedData.map((d) => d.name))
.range([0, width])
.paddingInner(0.1) // 内部填充
.paddingOuter(0.2); // 外部填充
1
y轴的首尾会有个横向的短刻度,这个怎么去掉?

要去掉 y 轴的首尾横向短刻度,可以通过自定义 y 轴的刻度线样式来实现。具体来说,可以使用 tickSize 方法来设置刻度线的长度,并使用 tickSizeInner 和 tickSizeOuter 方法来分别设置内部和外部刻度线的长度。

以下是如何实现的示例:

1
2
3
4
5
6
7
const yAxis = d3
.axisLeft(y)
.tickFormat((d) => d / 1000000 + 'M')
.tickSize(-width) // 设置刻度线的长度为负的宽度,使其覆盖整个绘图区域
.tickSizeOuter(0); // 设置外部刻度线的长度为0,去掉首尾的横向短刻度

chart.append('g').attr('class', 'y axis').call(yAxis);

在这个示例中:

使用 tickSize(-width) 设置刻度线的长度为负的宽度,使其覆盖整个绘图区域。
使用 tickSizeOuter(0) 设置外部刻度线的长度为 0,从而去掉 y 轴首尾的横向短刻度。

1
y轴的刻度文本显示不全,最左侧到图表外部去了,怎么处理?

要解决 y 轴的刻度文本显示不全的问题,可以通过以下几种方法:

增加左侧的边距:确保有足够的空间来显示 y 轴的刻度文本。
调整 y 轴文本的位置:通过设置 text-anchor 和 dx 属性来调整文本的位置。

1
x轴的文本标签和柱子,没居中对齐

要确保 x 轴的文本标签和柱子居中对齐,可以通过调整 scaleBand 的 paddingInner 和 paddingOuter 属性,并确保文本标签的 text-anchor 设置为 middle。此外,还需要确保柱子的 x 属性和文本标签的 x 属性正确对齐。

1
如何设置Y轴显示的刻度的数量?

要设置 Y 轴显示的刻度数量,可以使用 ticks 方法。ticks 方法允许你指定刻度的数量,D3.js 会根据你指定的数量自动计算合适的刻度值。

1
2
3
4
5
6
7
const yAxis = d3
.axisLeft(y)
.tickFormat((d) => d / 1000000 + 'M')
.ticks(5) // 设置刻度数量为5
.tickSizeOuter(0); // 设置外部刻度线的长度为0,去掉首尾的横向短刻度

chart.append('g').attr('class', 'y axis').call(yAxis);
1
y轴如何不显示轴线;

要隐藏 Y 轴的轴线,可以通过选择 Y 轴的路径元素并设置其样式为 none。你可以在调用 call(yAxis) 后添加代码来选择并隐藏轴线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const yAxis = d3
.axisLeft(y)
.tickFormat((d) => formatUnit(d))
// .tickSize(-width) // 设置刻度线的长度为负的宽度,使其覆盖整个绘图区域
.tickSizeOuter(0) // 设置外部刻度线的长度为0,去掉首尾的横向短刻度
.ticks(5);

// y轴的动画效果
const yLabel = yAxisEl
.transition()
.duration(updateInterval / 2)
.call(yAxis)
// 隐藏轴线
.select('.domain')
.attr('stroke', 'none');

写功能函数

1
2
3
4
5
6
7
8
9
10
11
12
13
D3.js编写一个函数,画一个带箭头的柱子,要求柱子高度从0开始,往下方生长,经过指定时间后,达到预设的高度
函数定义类似这样:
/**
* 生成一个动态生长的、一端带三角形箭头的柱状图
* @param {*} startPoint 起点的坐标 {x, y}
* @param {*} endPoint 终点的坐标 {x, y}
* @param {*} time 生长动画的时长
* @param {*} style 样式,类似css的style
*/
function bar(startPoint, endPoint, time, style)
{

}
1
写个函数,提取文本中的数字和英文
1
这是创建一系列柱子的代码,如何让创建的柱子逐个依次出现,而不是一起出现?

要让创建的柱子逐个依次出现,而不是一起出现,你可以在每个柱子的动画中添加一个延迟(delay)。这个延迟可以通过数据点的索引或者数据点的某个属性来计算,以确保每个柱子的动画都是顺序触发的。

1
如何设置柱子默认不显示,开始动画时才显示?

要设置柱子默认不显示,直到动画开始时才显示,你可以在柱子的初始样式中设置 opacity 为 0。然后在动画过渡中逐渐增加 opacity 至 1,这样柱子就会在动画开始时逐渐显现。

1
如何设置柱子默认不显示,在开始动画的瞬间就设置为显示.style('opacity', 1)

如果你想要在动画开始的瞬间就让柱子完全显示,而不是逐渐从透明变为不透明,你可以在动画的初始阶段就设置 opacity 为 1。这意味着柱子会在动画开始时立即显示,而不是通过渐变的方式。

1
D3.js如何在两次的数据中判断哪些是enter的数据?我想自己定义这个判断逻辑该怎么做?

在 D3.js 中,当你使用 data() 方法将数据绑定到 DOM 元素上时,D3.js 会返回一个包含所有选中元素的 selection 对象。这个 selection 对象包含了两个重要的子集:enter 和 update。

enter 子集包含了那些在数据中存在但没有对应 DOM 元素的 data elements。这些元素需要被创建。
update 子集包含了那些在数据中存在且已经有对应 DOM 元素的 data elements。这些元素可能会被修改。

1
D3.js如何获取 update 选择集,来判断哪些数据是上次就存在的?

在 D3.js 中,要获取 update 选择集,即那些在数据更新时已经存在的数据,你可以使用 data()方法绑定数据后,通过 enter()方法获取 enter 选择集,然后使用 exit()方法获取 exit 选择集。update 选择集实际上就是除去 enter 和 exit 选择集后剩下的部分。

1
SVG的transform和x、y属性的区别是什么

在 SVG 中,transform 属性和 x、y 属性都可以用来改变图形的位置,但它们的作用和行为有所不同。

x 和 y 属性
x 和 y 是 SVG 元素的属性,用于定义图形的左上角在 SVG 坐标系中的位置。
当你设置一个元素的 x 和 y 属性时,你是在指定该元素的位置,而不会改变其方向或形状。
如果你只设置 x 属性,元素将水平移动;如果只设置 y 属性,元素将垂直移动。
x 和 y 属性通常用于简单的平移,而且它们不会创建任何依赖于坐标系变换的动画效果。
transform 属性
transform 是一个 SVG 属性,允许你对元素进行更复杂的变换,包括平移(translate)、旋转(rotate)、缩放(scale)和倾斜(skew)。
transform 属性可以接受一个或多个变换命令,这些命令依次应用于元素。
transform 属性的值是一个或多个变换指令的列表,例如 transform=”translate(10, 20) rotate(45)”。
transform 属性可以创建更复杂的视觉效果,如旋转或缩放,并且经常用于动画,因为它可以创建平滑的过渡效果。
区别
作用范围:x 和 y 属性只影响元素的位置,而 transform 属性可以影响元素的位置、方向和大小。
动画:transform 属性通常用于动画,因为它可以创建平滑的过渡效果,而 x 和 y 属性则不常用于动画。
复合变换:transform 属性可以一次应用多个变换,而 x 和 y 属性只能分别应用于水平和垂直移动。
继承性:x 和 y 属性不是 CSS 属性,不能继承自样式表,而 transform 是一个 CSS 属性,可以继承。
性能:在某些情况下,使用 transform 属性可能会提供更好的性能,尤其是当涉及到复杂的变换和动画时。
在实际应用中,选择使用 x、y 属性还是 transform 属性,取决于你想要实现的效果和性能要求。对于简单的平移,x 和 y 属性可能更直接;而对于需要复合变换或动画的情况,transform 属性则更为合适。

1
transform的参考位置是什么?如果我对一个元素执行多次transfrom属性的改变,其参考位置是同一个吗?

在 SVG 中,transform 属性的参考位置是相对于元素的当前位置或其父元素的坐标系。当你对一个元素执行多次 transform 属性的改变时,这些变换是累积的,并且它们的应用顺序很重要。变换函数按从左到右的顺序相乘,也就是说复合变换按从右到左的顺序有效地应用。

例如,如果你先平移一个元素,然后旋转它,平移操作会首先影响元素的位置,然后旋转操作会围绕新的(平移后的)位置应用。这是因为旋转变换是在平移变换之后应用的,所以旋转的“参考点”是平移后的位置。这与 CSS 变换的行为是一致的,其中变换的顺序会影响最终的布局和呈现。

在 SVG 中,transform 属性可以包含多种变换,如平移(translate)、旋转(rotate)、缩放(scale)、倾斜(skew)和矩阵(matrix)。这些变换可以组合在一起,以创建复杂的效果。

需要注意的是,SVG 的 transform 属性与 CSS 中的 transform 属性在某些方面有所不同。例如,SVG 的 transform 属性的坐标变换是相对于画布的左上角计算的,而 CSS 的 transform 属性的坐标变换是相对于元素本身计算的。这意味着在应用变换时,SVG 和 CSS 可能会有不同的表现,尤其是在旋转和缩放时

此外,SVG 变换可以变成动画,就像 CSS 变换一样。你可以使用 CSS 动画来控制 SVG 元素的 transform 属性,从而创建动态的视觉效果

1
为什么给一个path元素设置y属性,没有效果?

在 SVG 中,给一个 元素设置 y 属性没有效果,因为 元素并不支持 x 和 y 属性来控制其位置。相反, 的位置是通过其 d 属性中定义的路径命令来确定的。

要移动一个 元素,你可以使用 transform 属性来进行平移。

1
D3.js画的X轴的文本标签,如何换行显示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 假设你已经创建了一个 SVG 容器和比例尺
const svg = d3
.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);

const x = d3
.scaleBand()
.domain(data.map((d) => d.name)) // 假设你的数据对象有一个 'name' 属性
.range([0, width])
.padding(0.2);

// 创建 X 轴
const xAxis = d3.axisBottom(x);

// 添加 X 轴到 SVG
svg.append('g').attr('transform', `translate(0, ${height})`).call(xAxis);

// 现在,我们将为每个标签添加换行
svg.selectAll('.x.axis text').each(function () {
var text = d3.select(this);
var words = text.text().split(/\s+/).reverse();
var word = words.pop();
text
.text(null)
.append('tspan')
.attr('x', 0)
.attr('y', 0)
.text(word + ' ');

while ((word = words.pop())) {
text
.append('tspan')
.attr('x', 0)
.attr('dy', '1em') // 你可以根据需要调整行距
.text(word + ' ');
}
});
1
2
3
4
5
/**
* 自动根据数值,格式化单位百万,且按照英文数字风格,每三位加一个逗号
* @param {*} value
*/
function formatUnit(value) {}
1
这是创建一系列柱子的代码,如何让创建的柱子逐个依次出现,而不是一起出现?
1
如何让y轴范围和Y轴的文本标签,根据当前展示的数据范围,动态变更?
1
当数据较多时,保证当前图表区域只显示5根柱子,然后随着新数据的出现,图表横向移动,永远只展示最新的5个柱子,y轴的范围也随着变更,为当前展示的5根柱子的范围
1
2
3
4
5
6
7
8
9
10
11
这是用D3.js画的一个柱状图,包含有很多数据和动画,请将其修改为:
1、画布范围内只显示5个数据
2、随着时间的增长,数据从左到右动态更新,柱子从左到右移动出现
3、注意每一个时间周期内都只显示5个数据,要通过连贯的动画将所有数据从左到右展示全
4、y轴的刻度范围是根据当前展示的5个数据来设定的,当前展示的5个数据的最大值和最小值作为y轴的刻度范围
5、Y轴的刻度范围也是随着动画的播放而动态变更的


存在问题:
1、Y轴范围变更的动画和X轴柱子的横向移动动画,需要改为平滑、连贯的
2、从左到右每次只出现一根新的柱子,而不是一次将下个周期的5根柱子都展示出来
1
D3.js如何在两次的数据中判断哪些是enter的数据?我想自己定义这个判断逻辑该怎么做?

在 D3.js 中,enter 选择集包含在新数据中存在但在当前 DOM 中不存在的元素。默认情况下,D3.js 使用数据的索引来进行绑定和判断哪些元素是 enter、update 或 exit。如果你想自定义这个判断逻辑,可以使用 key function 来指定数据绑定的唯一标识。

1
那为什么我这里设置了data的key,但是每次仍然全部数据都会进入enter逻辑?

在 D3.js 中,如果你发现所有数据都进入了 enter 选择集,可能是因为在更新数据时,DOM 元素的绑定没有正确保留。确保你在更新数据时没有意外地删除或重新创建了整个选择集。
–排查发现是我在每次 update 之前先把所有 bar 元素给 remove()清空了。

1
如何判断哪些数据是上次就存在的?

在 D3.js 中,可以通过 key function 来唯一标识数据,并使用 enter、update 和 exit 选择集来判断哪些数据是上次就存在的。具体来说:

enter 选择集包含在新数据中存在但在当前 DOM 中不存在的元素。
update 选择集包含在新数据和当前 DOM 中都存在的元素。
exit 选择集包含在当前 DOM 中存在但在新数据中不存在的元素。
要判断哪些数据是上次就存在的,可以使用 update 选择集。

Take aways

第一原则:遵循数据驱动的编程思想

不管是程序整体框架设计还是细节编码上。

否则需求变更后(探索性的功能,经常变更,这次变了 3 版),你的改动成本会很高。

用 D3.js,那么一开始就把 enter、exit、update 给写上去。

重头让 AI 生成远远好于在现有项目上改

已有的代码,因为存在一定复杂度+可能不是业界通用写法,LLM 理解和修改比较困难。

Demo 阶段,全部代码写一个文件里面,别分散多个文件

这样做的好处是:

  • 理解性:LLM 好全局性理解(未验证@workspace 和@Codebase 与之对比的差异)。
  • 可用性:LLM 给出的代码可用性高,直接 apply 就能跑。

但是要注意封装为函数,方便后面做结构拆分和优化。

很大一部分问题集中在 enter、exit、update 的选择集上

update = selection - enter - exit
merge = enter.merge(update)

几何计算很重要

比如算箭头的动画位置,看起来是很简单的,但是花了我不少时间。

MARK 标记对于定位代码很有用

特别是未拆分的长代码。

Bookmarks 扩展更强大,但是因为我手的问题,打字很困难,所以我用 MARK,眼睛看+鼠标点。

AI 随着项目开发的进展,效用会递减

第一天基本全是用 AI,第二天基本全是自己写(根据需求改细节)

因为第一天是从 0 开始,各种 API 和 D3.js 的特性不熟悉,所以 AI 的效用很高。
第二天调整细节,更多的是在现有基础上根据需求和自己的思考去改代码。