Echarts-丝带图

ops/2024/9/24 0:29:39/

Echarts-丝带图

demo地址

打开CodePen

什么是丝带图?

丝带图是Power BI中独有额可视化视觉对象,它的工具提示能展示指标当期与下期的数据以及排名。需求:使用丝带图展示"2022年点播订单表"不同月份不同点播套餐对应订单数据。

效果

在这里插入图片描述

思路

由于丝带图是Power BI中独有额可视化视觉对象,所以目前没得任何示例参考,所以只能自己构思使用echarts还原了。当然还有完善的余地,中间的连线不够平滑,可根据产品需求采用某种曲线函数去生成一组点位。

1. 以散点图画出柱状堆叠效果(柱状图的堆叠图无法满足hover小块效果)- y轴分成100个刻度,每个刻度代表1%,以控制大数据视图效果
2. 在柱状图两根柱之间构建6个点,使用面积图,连接2块柱- 柱中间点位取的是y轴的平均值- (若想构建的曲线细腻,可以使用曲线函数来构建这部分的点)
3. 再使用上面6个点中的下面点绘制透明区域

核心代码

  • 以散点图构建柱状图
function createOption(initData) {const initDataResult = createData(initData);const { list, legendData, xAxisData, seriesDataMap, max } = initDataResult;const seriesData = [];for (const seriesIndex in Object.keys(seriesDataMap)) {const name = Object.keys(seriesDataMap)[seriesIndex];const data = seriesDataMap[name];seriesData.push({name,type: 'scatter',symbol: 'rect',z: 3,itemStyle: {opacity: 1},label: {show: true,color: '#fff',formatter: (params) => formatMoney(params.data.realValue, 0)},tooltip: {trigger: 'item',formatter: (params) => {return `<div><div>年度月份:${params.name}</div><div>${params.seriesName}${formatMoney(params.data.realValue, 0)}</div></div>`;}},data: getChartData({ data, name })});}function getChartData({ data = [], name }) {const dataResult = [];data?.forEach((value, dateIndex) => {const y = maxY * (value / max);const ySize = maxHeight * (y / maxY);const offset = getOffset({ list, dateIndex, name, max });const radioValue = y + offset > 100 ? 100 : y + offset;dataResult.push({name,value: radioValue,radioValue,realValue: value,symbolOffset: [0, '50%'],symbolSize: [50, ySize]});if (dateIndex < data?.length - 1) {new Array(3).fill(0).forEach((_, lineIndex) => {dataResult.push({value: '',radioValue,realValue: value,isLine: true,lineIndex});});}});return dataResult;}const lineSeries = createLineChart({ seriesData, initDataResult });return {option: {legend: {data: legendData},xAxis: {data: xAxisData,axisTick: {show: false}},series: [...seriesData, ...lineSeries]}};
}
  • 生成折线图数据
function getLineData(data, name, isSpace = false) {const result = data?.map((_, index) => {const dateIndex = Math.floor(index / 4);const lineIndex = index % 4;const item = data?.[index] || {};const lastItem = data?.[index - (4 - lineIndex)] || {};const nextItem = data?.[index + (4 - lineIndex)] || {};const offset = getOffset({ list, dateIndex, name, max });const nextOffset = getOffset({ list, dateIndex: dateIndex + 1, name, max });let spaceValue;let value = item.radioValue - offset;switch (lineIndex) {case 0:spaceValue = offset;break;case 1:spaceValue = offset;if (!nextItem?.radioValue) {value = undefined;}break;case 2:spaceValue = (nextOffset + offset) / 2;value = (nextItem.radioValue + item.radioValue) / 2 - spaceValue;break;case 3:spaceValue = nextOffset;value = nextItem.radioValue - nextOffset;if (!lastItem?.radioValue) {value = undefined;}break;}if (!lastItem?.radioValue && !nextItem?.radioValue) {value = undefined;}// console.log(lineIndex, item, offset, nextOffset, spaceValue, value);const newItem = {...item,value: isSpace ? spaceValue : value};return newItem;});// console.log('result', result);return result;
}
  • 生成折线图配置
function createLineChart({ seriesData = [], initDataResult }) {const { list, max } = initDataResult;const spaceLineSeries = [];const lineSeries = [];// console.log('seriesData', seriesData);for (const seriesIndex in seriesData) {const seriesItem = seriesData[seriesIndex];const defaultLineSeries = {type: 'line',name: seriesItem.name,stack: `Line-${seriesIndex}`,smooth: 0.3,lineStyle: {width: 0,opacity: 0},symbol: 'none',showSymbol: false,triggerLineEvent: true,silent: true,areaStyle: {},emphasis: {focus: 'series'}};spaceLineSeries.push({...defaultLineSeries,areaStyle: {opacity: 0},data: getLineData(seriesItem?.data, seriesItem.name, true)});lineSeries.push({...defaultLineSeries,data: getLineData(seriesItem?.data, seriesItem.name)});}function getLineData(data, name, isSpace = false) {const result = data?.map((_, index) => {const dateIndex = Math.floor(index / 4);const lineIndex = index % 4;const item = data?.[index] || {};const lastItem = data?.[index - (4 - lineIndex)] || {};const nextItem = data?.[index + (4 - lineIndex)] || {};const offset = getOffset({ list, dateIndex, name, max });const nextOffset = getOffset({ list, dateIndex: dateIndex + 1, name, max });let spaceValue;let value = item.radioValue - offset;switch (lineIndex) {case 0:spaceValue = offset;break;case 1:spaceValue = offset;if (!nextItem?.radioValue) {value = undefined;}break;case 2:spaceValue = (nextOffset + offset) / 2;value = (nextItem.radioValue + item.radioValue) / 2 - spaceValue;break;case 3:spaceValue = nextOffset;value = nextItem.radioValue - nextOffset;if (!lastItem?.radioValue) {value = undefined;}break;}if (!lastItem?.radioValue && !nextItem?.radioValue) {value = undefined;}// console.log(lineIndex, item, offset, nextOffset, spaceValue, value);const newItem = {...item,value: isSpace ? spaceValue : value};return newItem;});// console.log('result', result);return result;}return [...spaceLineSeries, ...lineSeries];
}

完整代码

var dom = document.getElementById('chart-container');
var myChart = echarts.init(dom, null, {renderer: 'canvas',useDirtyRect: false
});
var app = {};var option;const defaultData = [{date: '2022年02月',list: [{name: '安列克-常州四药',value: 48196},{name: '贝克宁-成都贝特',value: 85944},{name: '瀚宝-深圳瀚宇',value: 43122},{name: '卡贝缩宫素-杭州澳亚',value: 46082},{name: '卡贝缩宫素-天吉生物',value: 28473},{name: '卡贝缩宫素-星银药业',value: 20584}]},{date: '2022年03月',list: [{name: '安列克-常州四药',value: 97775},{name: '贝克宁-成都贝特',value: 134262},{name: '瀚宝-深圳瀚宇',value: 102538},{name: '卡贝缩宫素-杭州澳亚',value: 77479},{name: '卡贝缩宫素-天吉生物',value: 59422},{name: '卡贝缩宫素-星银药业',value: 32413}]},{date: '2022年04月',list: [{name: '安列克-常州四药',value: 91399},{name: '贝克宁-成都贝特',value: 151064},{name: '瀚宝-深圳瀚宇',value: 74733},{name: '卡贝缩宫素-杭州澳亚',value: 75197},{name: '卡贝缩宫素-天吉生物',value: 46853},{name: '卡贝缩宫素-星银药业',value: 24845}]},{date: '2022年05月',list: [{name: '安列克-常州四药',value: 83667},{name: '贝克宁-成都贝特',value: 114716},{name: '瀚宝-深圳瀚宇',value: 57327},{name: '卡贝缩宫素-杭州澳亚',value: 62267},{name: '卡贝缩宫素-天吉生物',value: 38604},{name: '卡贝缩宫素-星银药业',value: 19766}]},{date: '2022年06月',list: [{name: '安列克-常州四药',value: 80524},{name: '贝克宁-成都贝特',value: 155227},{name: '瀚宝-深圳瀚宇',value: 67098},{name: '卡贝缩宫素-杭州澳亚',value: 61857},{name: '卡贝缩宫素-天吉生物',value: 44098},{name: '卡贝缩宫素-星银药业',value: 26956}]},{date: '2022年07月',list: [{name: '安列克-常州四药',value: 92172},{name: '贝克宁-成都贝特',value: 118129},{name: '瀚宝-深圳瀚宇',value: 61548},{name: '卡贝缩宫素-杭州澳亚',value: 64490},{name: '卡贝缩宫素-天吉生物',value: 38073},{name: '卡贝缩宫素-星银药业',value: 21705}]},{date: '2022年08月',list: [{name: '安列克-常州四药',value: 94615},{name: '贝克宁-成都贝特',value: 119397},{name: '瀚宝-深圳瀚宇',value: 60547},{name: '卡贝缩宫素-杭州澳亚',value: 73835},{name: '卡贝缩宫素-天吉生物',value: 37406},{name: '卡贝缩宫素-星银药业',value: 26228}]}
]function formatMoney(money) {return money
}function run({ data = defaultData, height = 500 }) {const chartHeight = height;const maxY = 100;const maxHeight = chartHeight - maxY;function createData(initData = []) {const list = initData?.map((item) => ({...item,total: item.list.reduce((pre, cur) => pre + cur.value, 0),list: item.list?.sort((a, b) => a.value - b.value)}));const legendData = [];const xAxisData = [];const seriesDataMap = {};let max = 0;// 生成x轴、图例数据for (const dateIndex in list) {const item = list[dateIndex];xAxisData.push(item.date);if (dateIndex < list?.length - 1) {new Array(3).fill(0).forEach((_, lineIndex) => {xAxisData.push(`line-${lineIndex}`);});}max = Math.max(max, item.total);for (const index in item.list) {const dataItem = item.list[index];if (!legendData?.includes(dataItem.name)) {legendData.push(dataItem.name);}}}// 根据图例生成数据for (const index in list) {const item = list[index];for (const name of legendData) {const dataItem = item?.list?.find((dataItem) => dataItem.name === name);_.set(seriesDataMap, `${name}.${index}`, dataItem?.value);}}const result = { list, legendData, xAxisData, seriesDataMap, max };// console.log('result', result);return result;}function createLineChart({ seriesData = [], initDataResult }) {const { list, max } = initDataResult;const spaceLineSeries = [];const lineSeries = [];// console.log('seriesData', seriesData);for (const seriesIndex in seriesData) {const seriesItem = seriesData[seriesIndex];const defaultLineSeries = {type: 'line',name: seriesItem.name,stack: `Line-${seriesIndex}`,smooth: 0.3,lineStyle: {width: 0,opacity: 0},symbol: 'none',showSymbol: false,triggerLineEvent: true,silent: true,areaStyle: {},emphasis: {focus: 'series'}};spaceLineSeries.push({...defaultLineSeries,areaStyle: {opacity: 0},data: getLineData(seriesItem?.data, seriesItem.name, true)});lineSeries.push({...defaultLineSeries,data: getLineData(seriesItem?.data, seriesItem.name)});}function getLineData(data, name, isSpace = false) {const result = data?.map((_, index) => {const dateIndex = Math.floor(index / 4);const lineIndex = index % 4;const item = data?.[index] || {};const lastItem = data?.[index - (4 - lineIndex)] || {};const nextItem = data?.[index + (4 - lineIndex)] || {};const offset = getOffset({ list, dateIndex, name, max });const nextOffset = getOffset({ list, dateIndex: dateIndex + 1, name, max });let spaceValue;let value = item.radioValue - offset;switch (lineIndex) {case 0:spaceValue = offset;break;case 1:spaceValue = offset;if (!nextItem?.radioValue) {value = undefined;}break;case 2:spaceValue = (nextOffset + offset) / 2;value = (nextItem.radioValue + item.radioValue) / 2 - spaceValue;break;case 3:spaceValue = nextOffset;value = nextItem.radioValue - nextOffset;if (!lastItem?.radioValue) {value = undefined;}break;}if (!lastItem?.radioValue && !nextItem?.radioValue) {value = undefined;}// console.log(lineIndex, item, offset, nextOffset, spaceValue, value);const newItem = {...item,value: isSpace ? spaceValue : value};return newItem;});// console.log('result', result);return result;}return [...spaceLineSeries, ...lineSeries];}function createOption(initData) {const initDataResult = createData(initData);const { list, legendData, xAxisData, seriesDataMap, max } = initDataResult;const seriesData = [];for (const seriesIndex in Object.keys(seriesDataMap)) {const name = Object.keys(seriesDataMap)[seriesIndex];const data = seriesDataMap[name];seriesData.push({name,type: 'scatter',symbol: 'rect',z: 3,itemStyle: {opacity: 1},label: {show: true,color: '#fff',formatter: (params) => formatMoney(params.data.realValue, 0)},tooltip: {trigger: 'item',formatter: (params) => {return `<div><div>年度月份:${params.name}</div><div>${params.seriesName}${formatMoney(params.data.realValue, 0)}</div></div>`;}},data: getChartData({ data, name })});}function getChartData({ data = [], name }) {const dataResult = [];data?.forEach((value, dateIndex) => {const y = maxY * (value / max);const ySize = maxHeight * (y / maxY);const offset = getOffset({ list, dateIndex, name, max });const radioValue = y + offset > 100 ? 100 : y + offset;dataResult.push({name,value: radioValue,radioValue,realValue: value,symbolOffset: [0, '50%'],symbolSize: [50, ySize]});if (dateIndex < data?.length - 1) {new Array(3).fill(0).forEach((_, lineIndex) => {dataResult.push({value: '',radioValue,realValue: value,isLine: true,lineIndex});});}});return dataResult;}const lineSeries = createLineChart({ seriesData, initDataResult });return {option: {legend: {data: legendData},xAxis: {data: xAxisData,axisTick: {show: false}},series: [...seriesData, ...lineSeries]}};}function getOffset({ list, dateIndex, name, max }) {const dateData = list[dateIndex]?.list || [];const itemIndex = dateData?.findIndex((item) => item.name === name);let offset = 0;for (let i = 0; i < itemIndex; i++) {const itemValue = dateData[i].value;offset += maxY * (itemValue / max);}return offset;}const { option: newOption } = createOption(data);return _.merge({grid: {top: 40,left: 20,right: 20,bottom: 40,containLabel: true},yAxis: {show: false,max: maxY},tooltip: {// show: true,// trigger: 'axis',// axisPointer: {//   type: 'none'// },// formatter: (params, ticket) => {//   // console.log('params', params, ticket);//   return '';// }},dataZoom: [{type: 'slider',filterMode: 'weakFilter',showDataShadow: false,showDetail: false,brushSelect: false,height: 20,bottom: 10,startValue: 1,endValue: 5,xAxisIndex: 0,start: 0,end: 100}],xAxis: {type: 'category',data: newOption.xAxis.data,axisLabel: {formatter: function (value) {return value?.includes('line') ? '' : value;}}}},newOption);
}function getOption(data, height) {return run({ data, height });
}option = getOption(defaultData);if (option && typeof option === 'object') {myChart.setOption(option);
}window.addEventListener('resize', myChart.resize);

http://www.ppmy.cn/ops/15649.html

相关文章

Bootstrap弹框使用

Bootstrap 是一个流行的前端框架&#xff0c;它提供了许多预定义的组件&#xff0c;包括弹框&#xff08;modal&#xff09;。使用 Bootstrap 的弹框可以轻松地创建弹出窗口&#xff0c;用于显示信息、收集用户输入等。 下面是使用 Bootstrap 弹框的基本步骤&#xff1a; 1. …

【前端Vue】Vue3+Pinia小兔鲜电商项目第6篇:整体认识和路由配置,本资源由 收集整理【附代码文档】

Vue3ElementPlusPinia开发小兔鲜电商项目完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;认识Vue3&#xff0c;使用create-vue搭建Vue3项目1. Vue3组合式API体验,2. Vue3更多的优势,1. 认识create-vue,2. 使用create-vue创建项目,1. setup选项的写法和执行…

Vitis HLS 学习笔记--优化指令-ARRAY_PARTITION

目录 1. ARRAY_PARTITION 概述 2. 语法解析 2.1 参数解释 2.1.1 variable 2.1.2 type 2.1.3 factor 2.1.4 dim 2.2 典型示例 2.2.1 dim1 2.2.2 dim2 2.2.3 dim0 3. 实例演示 4. 总结 1. ARRAY_PARTITION 概述 ARRAY_PARTITION 指令中非常重要&#xff0c;它用于优…

基于B2C的网上拍卖系统——秒杀与竞价

点击下载源码和论文https://download.csdn.net/download/liuhaikang/89222887 课题背景及意义 随着网络的进一步普及和电子商务的高速发展&#xff0c;越来越多的人们开始在网络中寻求方便。网上网物具备了省时、省事、省心、高效等特点&#xff0c;从而受到越来越多人的欢迎。…

Flutter 上架如何解决 ITMS-91053 问题

最近&#xff0c;我的 Flutter App 发布到 TestFlight 后&#xff0c;就会收到一封邮件&#xff1a;The uploaded build for YOUR APP has one or more issues. 上面的邮件主要是说&#xff0c;我的 App 缺少了调用 API 的声明&#xff0c;以前从来没看到过&#xff0c;上网一查…

美易官方:人民币国际支付占比升至近5%

随着全球金融市场的不断发展和数字化进程的加速&#xff0c;人民币的国际支付地位逐渐提升&#xff0c;成为备受瞩目的焦点。最近的数据显示&#xff0c;人民币在国际支付中的占比已经升至近5%&#xff0c;自11月以来已成为第四大交易货币。这一变化不仅反映了中国经济的崛起和…

【C++航海王:追寻罗杰的编程之路】C++11(二)

目录 C11(上) 1 -> STL中的一些变化 2 -> 右值引用和移动语义 2.1 -> 左值引用和右值引用 2.2 -> 左值引用与右值引用比较 2.3 -> 右值引用使用场景与意义 2.4 -> 右值引用引用左值及其更深入的使用场景分析 2.5 -> 完美转发 C11(上) 1 -> STL…

mysql 触发器

学习了mysql 视图&#xff0c;接着学习触发器 1&#xff0c;创建触发器 触发器&#xff08;trigger)是个特殊的存储过程&#xff0c;不同的是&#xff0c;执行存储过程要使用CALL语句来调用&#xff0c;而触发器的执行不需要使用CALL语句来调用&#xff0c;也不需要手工启动&am…