D3.js-现金流动态瀑布图
写需求
用D3.js画一个柱状图
柱状图有1000个数据
但是画布范围内只显示5个数据
然后随着时间的增长,数据从左到右动态更新,
注意每一个时间周期内都只显示5个数据,要通过连贯的动画将这1000个数据从左到右展示全
并且y轴的刻度范围是根据当前展示的5个数据来设定的,当前展示的5个数据的最大值和最小值作为y轴的刻度范围。
Y轴的刻度范围也是随着动画的播放而动态变更的。
画3个legend图例,每个图例包括一个圆角矩形和文本,矩形和文本要水平对齐;三个图例同一排横向排布: operating: '#ffc200', investment: '#6081ea', financing: '#dd70e0',
以初始的绘图区域为范围,给图表画一个10x10的背景网格线,颜色淡一点,不要遮住柱状图了;只在初始绘制一次即可,更新柱状图的时候不要重新绘制
不用根据x和y的刻度数量来画网格数量,网格固定是10x10的
DEBUG
查询知识
D3.js如何获取2个柱子之间的宽度?
在 D3.js 中,可以使用 scaleBand 的 bandwidth 方法来获取每个柱子的宽度。要获取两个柱子之间的宽度(即柱子之间的间距),可以使用 scaleBand 的 step 方法,然后减去 bandwidth 的值。
// 定义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 的值来计算两个柱子之间的间距。
如何找到同一层级的元素?
要选择父元素,可以使用 node().parentNode。
updates.select('path').attr('transform', function (d) {
// 找到同一层级的rect元素
const previousRect = d3.select(this.parentNode).select('rect');
});
// 如何将x轴的文本标签旋转45度:
const xAxis = d3.axisBottom(x).tickFormat((d, index) => {});
要将 x 轴的文本标签旋转 45 度,可以在调用 axisBottom 后,选择所有的文本标签并应用旋转变换。以下是如何实现的示例:
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)'); // 旋转文本标签
如何让绘图区域的两侧留出一些空白?现在左侧的柱子和y轴叠一起了
要在绘图区域的两侧留出一些空白,可以调整 scaleBand 的 paddingInner 和 paddingOuter 属性。paddingOuter 属性可以在比例尺的两端添加一些空白,从而避免柱子和 y 轴重叠。
使用 paddingInner(0.1) 设置柱子之间的内部填充。 使用 paddingOuter(0.2) 设置比例尺两端的外部填充,从而在绘图区域的两侧留出一些空白。
const x = d3
.scaleBand()
.domain(displayedData.map((d) => d.name))
.range([0, width])
.paddingInner(0.1) // 内部填充
.paddingOuter(0.2); // 外部填充
y轴的首尾会有个横向的短刻度,这个怎么去掉?
要去掉 y 轴的首尾横向短刻度,可以通过自定义 y 轴的刻度线样式来实现。具体来说,可以使用 tickSize 方法来设置刻度线的长度,并使用 tickSizeInner 和 tickSizeOuter 方法来分别设置内部和外部刻度线的长度。
以下是如何实现的示例:
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 轴首尾的横向短刻度。
y轴的刻度文本显示不全,最左侧到图表外部去了,怎么处理?
要解决 y 轴的刻度文本显示不全的问题,可以通过以下几种方法:
增加左侧的边距:确保有足够的空间来显示 y 轴的刻度文本。 调整 y 轴文本的位置:通过设置 text-anchor 和 dx 属性来调整文本的位置。
x轴的文本标签和柱子,没居中对齐
要确保 x 轴的文本标签和柱子居中对齐,可以通过调整 scaleBand 的 paddingInner 和 paddingOuter 属性,并确保文本标签的 text-anchor 设置为 middle。此外,还需要确保柱子的 x 属性和文本标签的 x 属性正确对齐。
如何设置Y轴显示的刻度的数量?
要设置 Y 轴显示的刻度数量,可以使用 ticks 方法。ticks 方法允许你指定刻度的数量,D3.js 会根据你指定的数量自动计算合适的刻度值。
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);
y轴如何不显示轴线;
要隐藏 Y 轴的轴线,可以通过选择 Y 轴的路径元素并设置其样式为 none。你可以在调用 call(yAxis) 后添加代码来选择并隐藏轴线。
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');
写功能函数
用D3.js编写一个函数,画一个带箭头的柱子,要求柱子高度从0开始,往下方生长,经过指定时间后,达到预设的高度
函数定义类似这样:
/**
* 生成一个动态生长的、一端带三角形箭头的柱状图
* @param {*} startPoint 起点的坐标 {x, y}
* @param {*} endPoint 终点的坐标 {x, y}
* @param {*} time 生长动画的时长
* @param {*} style 样式,类似css的style
*/
function bar(startPoint, endPoint, time, style)
{
}
写个函数,提取文本中的数字和英文
这是创建一系列柱子的代码,如何让创建的柱子逐个依次出现,而不是一起出现?
要让创建的柱子逐个依次出现,而不是一起出现,你可以在每个柱子的动画中添加一个延迟(delay)。这个延迟可以通过数据点的索引或者数据点的某个属性来计算,以确保每个柱子的动画都是顺序触发的。
如何设置柱子默认不显示,开始动画时才显示?
要设置柱子默认不显示,直到动画开始时才显示,你可以在柱子的初始样式中设置 opacity 为 0。然后在动画过渡中逐渐增加 opacity 至 1,这样柱子就会在动画开始时逐渐显现。
如何设置柱子默认不显示,在开始动画的瞬间就设置为显示.style('opacity', 1)
如果你想要在动画开始的瞬间就让柱子完全显示,而不是逐渐从透明变为不透明,你可以在动画的初始阶段就设置 opacity 为 1。这意味着柱子会在动画开始时立即显示,而不是通过渐变的方式。
D3.js如何在两次的数据中判断哪些是enter的数据?我想自己定义这个判断逻辑该怎么做?
在 D3.js 中,当你使用 data() 方法将数据绑定到 DOM 元素上时,D3.js 会返回一个包含所有选中元素的 selection 对象。这个 selection 对象包含了两个重要的子集:enter 和 update。
enter 子集包含了那些在数据中存在但没有对应 DOM 元素的 data elements。这些元素需要被创建。 update 子集包含了那些在数据中存在且已经有对应 DOM 元素的 data elements。这些元素可能会被修改。
D3.js如何获取 update 选择集,来判断哪些数据是上次就存在的?
在 D3.js 中,要获取 update 选择集,即那些在数据更新时已经存在的数据,你可以使用 data()方法绑定数据后,通过 enter()方法获取 enter 选择集,然后使用 exit()方法获取 exit 选择集。update 选择集实际上就是除去 enter 和 exit 选择集后剩下的部分。
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 属性则更为合适。
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 属性,从而创建动态的视觉效果 。
为什么给一个path元素设置y属性,没有效果?
在 SVG 中,给一个
要移动一个
D3.js画的X轴的文本标签,如何换行显示
// 假设你已经创建了一个 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 + ' ');
}
});
/**
* 自动根据数值,格式化单位百万,且按照英文数字风格,每三位加一个逗号
* @param {*} value
*/
function formatUnit(value) {}
这是创建一系列柱子的代码,如何让创建的柱子逐个依次出现,而不是一起出现?
如何让y轴范围和Y轴的文本标签,根据当前展示的数据范围,动态变更?
当数据较多时,保证当前图表区域只显示5根柱子,然后随着新数据的出现,图表横向移动,永远只展示最新的5个柱子,y轴的范围也随着变更,为当前展示的5根柱子的范围
这是用D3.js画的一个柱状图,包含有很多数据和动画,请将其修改为:
1、画布范围内只显示5个数据
2、随着时间的增长,数据从左到右动态更新,柱子从左到右移动出现
3、注意每一个时间周期内都只显示5个数据,要通过连贯的动画将所有数据从左到右展示全
4、y轴的刻度范围是根据当前展示的5个数据来设定的,当前展示的5个数据的最大值和最小值作为y轴的刻度范围
5、Y轴的刻度范围也是随着动画的播放而动态变更的
存在问题:
1、Y轴范围变更的动画和X轴柱子的横向移动动画,需要改为平滑、连贯的
2、从左到右每次只出现一根新的柱子,而不是一次将下个周期的5根柱子都展示出来
D3.js如何在两次的数据中判断哪些是enter的数据?我想自己定义这个判断逻辑该怎么做?
在 D3.js 中,enter 选择集包含在新数据中存在但在当前 DOM 中不存在的元素。默认情况下,D3.js 使用数据的索引来进行绑定和判断哪些元素是 enter、update 或 exit。如果你想自定义这个判断逻辑,可以使用 key function 来指定数据绑定的唯一标识。
那为什么我这里设置了data的key,但是每次仍然全部数据都会进入enter逻辑?
在 D3.js 中,如果你发现所有数据都进入了 enter 选择集,可能是因为在更新数据时,DOM 元素的绑定没有正确保留。确保你在更新数据时没有意外地删除或重新创建了整个选择集。 —排查发现是我在每次 update 之前先把所有 bar 元素给 remove()清空了。
如何判断哪些数据是上次就存在的?
在 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 的效用很高。 第二天调整细节,更多的是在现有基础上根据需求和自己的思考去改代码。