首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

  • 24-12-06 05:45
  • 3354
  • 17432
juejin.cn

项目接到一个需求,需要实现柱是像电池一样一节一节断开拼接上去的渐变柱,如图实现效果图

d-2.png

1.技术选型分析

搜索全网,基本都是2种,一种是echarts渐变实柱叠一层pictorial栅格柱达到渐变效果;另一种就是纯d3仿直角轴图表。优劣势非常明显,第一种用现成api实现比较快,但是不灵活,无法满足高动态配置项场景下理想渲染;第二种工期长,完全自定义轴边柱,起码多3倍时间,但是实现后可灵活调整,可定制,可满足更多配置项需求;我在项目中2种都实现过。最后选择d3的原因是,项目需要往外抛的配置项非常多,要满足几乎每一个样式都是可以配置且用户改变任何配置项,渲染保证正常。这么一听,肯定是d3更符合一点。下面就是开始开发。

2.数据准备

这是多系列数据data,画图表需要的类echarts数据集的数据,如果是常规接口数据,还需要转化后使用。

js
代码解读
复制代码
[ { "regionName": "钱塘区", "基教": 1575, "幼教": 401, "高教": 0, "中职": 0, "高职": 0}, { "regionName": "上城区", "基教": 1023, "幼教": 1222, "高教": 240, "高职": 0}, { "regionName": "滨江区", "基教": 2009, "幼教": 2050, "高教": 240, "中职": 0, "高职": 0}, { "regionName": "西湖区", "基教": 1518, "幼教": 1610, "高教": 0 }, { "regionName": "临安区", "基教": 2247, "幼教": 1335, "高教": 0, "高职": 0 }, { "regionName": "余杭区", "基教": 2620, "幼教": 1278, "高教": 23, "中职": 0, "高职": 0 }, { "regionName": "萧山区", "基教": 318, "幼教": 678, "高教": 23, "中职": 0, "高职": 0 }, { "regionName": "富阳区", "基教": 1245, "幼教": 780, "高教": 0, "中职": 0, "高职": 0}, { "regionName": "临平区", "基教": 2077, "幼教": 2216, "高教": 0, "中职": 0, "高职": 0 }, ]

3.绘制图表

3.1 绘制x轴和y轴

系列提取数组保存const seriesNames = ["基教", "幼教", "高教", "中职", "高职"];

js
代码解读
复制代码
const width = 928; const height = 600; const marginTop = 30; const marginRight = 40; const marginBottom = 30; const marginLeft = 40; ![image.png](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4220953e46554603809d3c18ec5a7821~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Y-M5Y-j6aaL54yr:q75.awebp?rk3s=f64ab15b&x-expires=1733892371&x-signature=apamS3C0fHVQjA27QSQn2CAkI6U%3D) // Create the SVG container const svg = d3 .create('svg') .attr('width', width) .attr('height', height) .attr('viewBox', [0, 0, width, height]) .attr('style', 'max-width: 100%; height: auto;'); const yTick = 0.2; const y0 = d3 .scaleBand() .rangeRound([marginTop, height - marginBottom]) .paddingInner(yTick); const y1 = d3.scaleBand().padding(0.16); const x = d3.scaleLinear().rangeRound([marginLeft, width - marginRight]); const seriesNames = ['基教', '幼教', '高教', '中职', '高职']; const xValueMap = {}; data.forEach((item) => { const newItem = { ...item }; delete newItem['regionName']; xValueMap[item['regionName']] = newItem; }); const layers = data.map((d) => seriesNames.map((key) => ({ key, value: d[key], ['regionName']: d['regionName'] })), ); y0.domain(data.map((d) => d['regionName'])); y1.domain(seriesNames).rangeRound([0, y0.bandwidth()]); const maxDataValue = d3.max(data, (d) => d3.max(seriesNames, (key) => d[key])); x.domain([0, maxDataValue]).nice(); const xAxis = (g) => { g.attr('transform', `translate(0,${height - marginBottom})`).call( d3.axisBottom(x).tickSize(0), ); }; // Draw X axes svg.append('g').call(xAxis); const yAxis = (g) => { g.attr('transform', `translate(${marginLeft},0)`).call( d3.axisLeft(y0).tickValues(y0.domain()), ); }; svg.append('g').call(yAxis);

绘制如图:

image.png 如图是简单的绘制出了x轴数值刻度,这个刻度会因为每次数值的变化而变化数值,最糟糕的是,有的时候出现非整10的倍数或者整100的倍数的刻度显示,这是与预期刻度尺应该展示的不相符合的,所以要着重修改一下这个渲染:

js
代码解读
复制代码
const xAxis = (g) => { g.attr('transform', `translate(0,${height - marginBottom})`).call( d3.axisBottom(x).tickSize(0), // tickSize(0)改成tickValues(axisTicks),其中axisTicks需要着重计算 ); };

封装函数去计算刻度步数

js
代码解读
复制代码
function calcTStepAndDMax(maxValue, tickCount) { let tickStep, domainMax; const magnitude = Math.pow(10, Math.floor(Math.log10(maxValue))); tickStep = magnitude; domainMax = Math.ceil(maxValue / tickStep) * tickStep; // Adjust tickStep based on the number of ticks needed if (domainMax / tickStep < tickCount) { tickStep = tickStep / 2; // Use half steps if fewer ticks than desired } domainMax = Math.ceil(maxValue / tickStep) * tickStep; return { tickStep, domainMax }; }

然后得到axisTicks变量

js
代码解读
复制代码
const { tickStep, domainMax } = calcTStepAndDMax(maxDataValue, numGridLines); const axisTicks = d3.range(0, domainMax + tickStep, tickStep); //顺便绘制每个刻度线上的网格 const grid = svg.append("g") .attr("class", "grid") .attr("transform", `translate(0,0)`); const xGrid = d3 .axisBottom(x) .tickValues(axisTicks) // 设置x轴的刻度值 .tickSize(-height + marginTop + marginBottom) .tickFormat('') .tickSizeOuter(0); // 无外部刻度 // 应用xGrid网格线 svg .append('g') .attr('class', 'x grid') .attr('transform', `translate(0,${height - marginBottom})`) .call(xGrid);

此时绘制图如下:

image.png 剩下部分就是绘制系列柱形图,不过在这之前,要准备一个生成渐变色的函数

js
代码解读
复制代码
function createGradient(id, color1, color2) { const gradient = defs.append("linearGradient") .attr("id", id) .attr("x1", "0%") .attr("y1", "0%") .attr("x2", "100%") .attr("y2", "0%"); gradient.append("stop") .attr("offset", "0%") .attr("stop-color", color1); gradient.append("stop") .attr("offset", "100%") .attr("stop-color", color2); }
3.2 绘制分段渐变电池柱

上述已有代码不赘述了,再加bar生成的代码:

js
代码解读
复制代码
const segmentHeight = 7; const segmentGap = 2; const yStep = (height - marginTop - marginBottom) / data.length; const barsGroup = svg.append('g'); // Background bar on hover const backgroundBar = barsGroup .append('rect') .attr('class', 'background-bar') .attr('y', marginTop) .attr('height', y0.bandwidth()) .attr('x', marginLeft) .attr('width', width - marginLeft - marginRight) .attr('fill', 'lightgrey') .style('opacity', 0); let lastXField = null; let lastKey = null; let previousWidth = 0; const bars = barsGroup .selectAll('g') .data(layers) .enter() .append('g') .attr('class', 'bar-wrapper') .attr( 'transform', (d, i) => `translate(0, ${marginTop + yStep * i + yStep / 2 - y0.bandwidth() / 2})`, ) .selectAll('rect') .data((d) => d.flatMap((dataPoint) => { const gradients = []; const [startColor, endColor] = baseColors[dataPoint.key]; let color1 = startColor; const totalWidth = x(dataPoint.value) - x(0); let numSegments = Math.ceil(totalWidth / (segmentHeight + segmentGap)); let currentSegHeight = segmentHeight; if (totalWidth % (segmentHeight + segmentGap) > 0) { numSegments = Math.floor(totalWidth / (segmentHeight + segmentGap)); } if (dataPoint.value > 0 && numSegments <= 0) { numSegments = 1; } for (let i = 0; i < numSegments; i++) { const gradientId = `gradient${gradientCounter++}${new Date().getTime()}`; let color2; if (i === numSegments - 1) { color2 = endColor; } else { const colorInterpolator = d3.interpolateRgb(color1, endColor); color2 = colorInterpolator((i + 1) / numSegments); } createGradient(gradientId, color1, color2); gradients.push({ ...dataPoint, segmentIndex: i, gradientId, currentSegHeight }); color1 = color2; } return gradients; }), ) .enter() .append('rect') .attr('y', (d) => y1(d.key)) .attr('x', (d, i, nodes) => { if (lastXField !== d['regionName'] || lastKey !== d.key) { previousWidth = 0; // Reset the width when switching to a new xField or key } lastXField = d['regionName']; lastKey = d.key; const currentWidth = previousWidth; previousWidth += d.currentSegHeight + segmentGap; return marginLeft + currentWidth; }) .attr('height', y1.bandwidth()) .attr('width', (d) => d.currentSegHeight) .attr('rx', 4) .attr('ry', 4) .attr('fill', (d) => `url(#${d.gradientId})`);

这样最主要的图表绘制就差不多完成了。

3.3 完整代码展示

下面是完整代码,完整代码里还有一些增值功能,是项目中用到了的需要的展示,可作为参考,完整代码d3官网是调试通过了的。完整代码如下:

js
代码解读
复制代码
chart = { const width = 928; const height = 600; const marginTop = 30; const marginRight = 40; const marginBottom = 30; const marginLeft = 40; // Create the SVG container const svg = d3 .create('svg') .attr('width', width) .attr('height', height) .attr('viewBox', [0, 0, width, height]) .attr('style', 'max-width: 100%; height: auto;'); const yTick = 0.2; const y0 = d3 .scaleBand() .rangeRound([marginTop, height - marginBottom]) .paddingInner(yTick); const y1 = d3.scaleBand().padding(0.16); const x = d3.scaleLinear().rangeRound([marginLeft, width - marginRight]); const seriesNames = ['基教', '幼教', '高教', '中职', '高职']; const xValueMap = {}; data.forEach((item) => { const newItem = { ...item }; delete newItem['regionName']; xValueMap[item['regionName']] = newItem; }); const layers = data.map((d) => seriesNames.map((key) => ({ key, value: d[key], ['regionName']: d['regionName'] })), ); y0.domain(data.map((d) => d['regionName'])); y1.domain(seriesNames).rangeRound([0, y0.bandwidth()]); const maxDataValue = d3.max(data, (d) => d3.max(seriesNames, (key) => d[key])); x.domain([0, maxDataValue]).nice(); const numGridLines = 2; function calcTStepAndDMax(maxValue, tickCount) { let tickStep, domainMax; const magnitude = Math.pow(10, Math.floor(Math.log10(maxValue))); tickStep = magnitude; domainMax = Math.ceil(maxValue / tickStep) * tickStep; // Adjust tickStep based on the number of ticks needed if (domainMax / tickStep < tickCount) { tickStep = tickStep / 2; // Use half steps if fewer ticks than desired } domainMax = Math.ceil(maxValue / tickStep) * tickStep; return { tickStep, domainMax }; } const { tickStep, domainMax } = calcTStepAndDMax(maxDataValue, numGridLines); const axisTicks = d3.range(0, domainMax + tickStep, tickStep); const segmentHeight = 7; const segmentGap = 2; function createGradient(id, color1, color2) { const gradient = defs .append('linearGradient') .attr('id', id) .attr('x1', '0%') .attr('y1', '0%') .attr('x2', '100%') .attr('y2', '0%'); gradient.append('stop').attr('offset', '0%').attr('stop-color', color1); gradient.append('stop').attr('offset', '100%').attr('stop-color', color2); } const baseColors = { 基教: ['#7b5afe', '#fac858'], 幼教: ['blue', 'purple'], 高教: ['red', '#fa6a6a'], 中职: ['#f5c324', '#f5c324'], 高职: ['#ff8a65', '#c23000'], }; const defs = svg.append('defs'); let gradientCounter = 1; const tooltip = d3 .select('body') .append('div') .attr('class', 'tooltip') .style('position', 'absolute') .style('visibility', 'hidden') .style('background', '#fff') .style('border', '1px solid #ccc') .style('padding', '10px') .style('border-radius', '5px') .style('box-shadow', '0 0 10px rgba(0, 0, 0, 0.1)'); const grid = svg.append('g').attr('class', 'grid').attr('transform', `translate(0,0)`); const xGrid = d3 .axisBottom(x) .tickValues(axisTicks) // 设置x轴的刻度值 .tickSize(-height + marginTop + marginBottom) .tickFormat('') .tickSizeOuter(0); // 无外部刻度 // 应用xGrid网格线 svg .append('g') .attr('class', 'x grid') .attr('transform', `translate(0,${height - marginBottom})`) .call(xGrid); const yStep = (height - marginTop - marginBottom) / data.length; grid .selectAll('line.vertical') .data(data.slice(1)) .enter() .append('line') .attr('class', 'vertical') .attr('x1', marginLeft) .attr('x2', width - marginRight) .attr('y1', (d, i) => marginTop + yStep * (i + 1)) .attr('y2', (d, i) => marginTop + yStep * (i + 1)) .attr('stroke', '#ccc') .attr('stroke-width', 1); svg .append('line') .attr('x1', marginLeft) .attr('x2', marginLeft) .attr('y1', marginTop) .attr('y2', height - marginBottom) .attr('stroke', '#000') .attr('stroke-width', 1); svg .append('line') .attr('x1', width - marginRight) .attr('x2', width - marginRight) .attr('y1', marginTop) .attr('y2', height - marginBottom) .attr('stroke', '#000') .attr('stroke-width', 1); svg .append('line') .attr('x1', marginLeft) .attr('x2', width - marginRight) .attr('y1', height - marginBottom) .attr('y2', height - marginBottom) .attr('stroke', '#000') .attr('stroke-width', 1); const xAxis = (g) => { g.attr('transform', `translate(0,${height - marginBottom})`).call( d3.axisBottom(x).tickValues(axisTicks), ); }; // Draw X axes svg.append('g').call(xAxis); const yAxis = (g) => { g.attr('transform', `translate(${marginLeft},0)`).call(d3.axisLeft(y0).tickValues(y0.domain())); }; svg.append('g').call(yAxis); const barsGroup = svg.append('g'); // Background bar on hover const backgroundBar = barsGroup .append('rect') .attr('class', 'background-bar') .attr('y', marginTop) .attr('height', y0.bandwidth()) .attr('x', marginLeft) .attr('width', width - marginLeft - marginRight) .attr('fill', 'lightgrey') .style('opacity', 0); let lastXField = null; let lastKey = null; let previousWidth = 0; const bars = barsGroup .selectAll('g') .data(layers) .enter() .append('g') .attr('class', 'bar-wrapper') .attr( 'transform', (d, i) => `translate(0, ${marginTop + yStep * i + yStep / 2 - y0.bandwidth() / 2})`, ) .selectAll('rect') .data((d) => d.flatMap((dataPoint) => { const gradients = []; const [startColor, endColor] = baseColors[dataPoint.key]; let color1 = startColor; const totalWidth = x(dataPoint.value) - x(0); let numSegments = Math.ceil(totalWidth / (segmentHeight + segmentGap)); let currentSegHeight = segmentHeight; if (totalWidth % (segmentHeight + segmentGap) > 0) { numSegments = Math.floor(totalWidth / (segmentHeight + segmentGap)); } if (dataPoint.value > 0 && numSegments <= 0) { numSegments = 1; } for (let i = 0; i < numSegments; i++) { const gradientId = `gradient${gradientCounter++}${new Date().getTime()}`; let color2; if (i === numSegments - 1) { color2 = endColor; } else { const colorInterpolator = d3.interpolateRgb(color1, endColor); color2 = colorInterpolator((i + 1) / numSegments); } createGradient(gradientId, color1, color2); gradients.push({ ...dataPoint, segmentIndex: i, gradientId, currentSegHeight }); color1 = color2; } return gradients; }), ) .enter() .append('rect') .attr('y', (d) => y1(d.key)) .attr('x', (d, i, nodes) => { if (lastXField !== d['regionName'] || lastKey !== d.key) { previousWidth = 0; // Reset the width when switching to a new xField or key } lastXField = d['regionName']; lastKey = d.key; const currentWidth = previousWidth; previousWidth += d.currentSegHeight + segmentGap; return marginLeft + currentWidth; }) .attr('height', y1.bandwidth()) .attr('width', (d) => d.currentSegHeight) .attr('rx', 4) .attr('ry', 4) .attr('fill', (d) => `url(#${d.gradientId})`) .on('mouseover', function (event, d) { const currentAllSeries = xValueMap[d['xField']]; const xValue = d['xField']; backgroundBar.attr('y', y0(xValue)).style('opacity', 0.2); }) .on('mousemove', function (event) { tooltip.style('top', `${event.pageY - 10}px`).style('left', `${event.pageX + 10}px`); }) .on('mouseout', function () { backgroundBar.style('opacity', 0); tooltip.style('visibility', 'hidden'); }); const labelPosition = 'right'; // Options: 'left', 'middle', 'right', 'boundary' const labelOffset = 0; // Offset from the bar barsGroup .selectAll('g.bar-group') .data(layers) .enter() .append('g') .attr('class', 'bar-group') .attr( 'transform', (d, i) => `translate(0, ${marginTop + yStep * i + yStep / 2 - y0.bandwidth() / 2})`, ) .selectAll('.label') .data((d) => d) .enter() .append('text') .attr('class', 'label') .attr('x', (d) => { if (labelPosition === 'left') { return marginLeft - labelOffset; } else if (labelPosition === 'middle') { return marginLeft + (x(d.value) - x(0)) / 2; } else if (labelPosition === 'right') { return x(d.value) + labelOffset; } else if (labelPosition === 'boundary') { return width - marginRight + labelOffset; } }) .attr('y', (d) => y1(d.key) + y1.bandwidth() / 2) .attr('dy', '0.35em') // Center the text vertically .attr('text-anchor', 'middle') // .attr('text-anchor', d => (labelPosition === 'left' || labelPosition === 'boundary') ? 'end' : 'middle') .attr('fill', 'black') .text((d) => d.value); // Return the SVG element. return svg.node(); }

4. 小技巧

  1. 找个在线网站可以调试d3代码,比如d3官网的Examples板块就可以在线调试。
  2. 有个比较完整的有效代码,再结合chartGPT,修正代码,能快速实现想要的某个定制化功能,大致能得到普类基础型,然后再小修小改,得到最终想要的结果。

5. 总结

本次实现也是踩坑了蛮多点的,比如整数刻度问题,网格必须在系列中间,往往是单一功能修改正确后,会干扰别的有点想关联的其他功能,一旦改动,就需要重新修正别的部分,否则就会出现展示bug。还蛮挑战耐心的!而且搜索d3的比较完整的案例很多没有完整的代码,忧桑之~

注:本文转载自juejin.cn的双口馋猫的文章"https://juejin.cn/post/7428850037038301194"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

104
前端
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top