Echarts-丝带图

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);
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PowerBI系列课程之内置视觉对象和常用自定义视觉对象专题 内置视觉对象Stacked bar chart  堆积条形Stacked column chart 堆积柱形 - X轴按类别显示Clustered bar chart  簇状条形Clustered column chart  簇状柱形100% Stacked bar chart 百分比堆积条形100% Stacked column chart 百分比堆积柱形Line Chart  折线-预测功能Area Chart  面积Stacked Area Chart  堆积面积Line and stacked column chart 折线和堆积柱形-双Y轴Line and clustered column chart 折线和簇状柱形Ribbon Chart  丝带Waterfall Chart  瀑布Funnel  漏斗Scatter chart  散点Pie Chart  饼 Donut Chart 环形TreeMap 树状Map  地 Filled Map 着色地Shape Map 形状地Gauge 仪表Card 卡片Multi-row card 多行卡片KPI  Table 表格-条件样式Matrix 矩阵详解  Key influencers  关键影响者Decomposition tree 分解树Q&A 问答2021.6月增加分页表格视觉对象自定义视觉对象视觉对象使用建议下载排名前20,免费实用的视觉对象 AllDemo pbix介绍 Pareto 帕累托DrillDown Donut  可钻取饼Word Cloud   文字云Gantt  甘特Infographic Designer  信息柱状Timeline Slicer  时间轴切片器Chiclet Slicer 片切片器Text Filter 文本筛选器HierarchySlicer  层级切片器Pulse Chart脉动Power KPI Matrix  KPI矩阵Animated Bar Chart Race 动态条形Advance Card  高级卡片Sankey Chart  桑基Radar Chart   雷达Dial Gauge  码表Waffle 华夫占比Quadrant 象限 Tornado Chart  龙卷风Histogram Chart  直方 Box and Whisker chart 盒线Sunburst  阳光Chord Chart 和弦Bullet Chart  子弹HTML Content  html解析视觉对象  
可以使用Vue和Canvas来实现彩色丝带展示效果。以下是一个简单的实现示例: HTML部分: ```html <canvas ref="canvas"></canvas> ``` Vue部分: ```javascript export default { mounted() { const canvas = this.$refs.canvas; const ctx = canvas.getContext('2d'); const colors = ['#FFA07A', '#FFB6C1', '#87CEEB', '#FFD700', '#ADD8E6']; // 将canvas的宽度设置为父元素的宽度 canvas.width = canvas.parentNode.offsetWidth; // 定义一个彩色丝带的类 class Ribbon { constructor() { this.x = 0; this.y = Math.random() * canvas.height; this.color = colors[Math.floor(Math.random() * colors.length)]; this.speed = Math.random() * 2 + 1; this.offset = Math.random() * 100; } update() { this.x += this.speed; this.y = Math.sin(this.x / 50 + this.offset) * 50 + canvas.height / 2; if (this.x > canvas.width) { this.x = -100; } } draw() { ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.lineTo(this.x + 100, this.y + 100); ctx.strokeStyle = this.color; ctx.lineWidth = 2; ctx.stroke(); } } // 创建多个彩色丝带并进行动画绘制 const ribbons = []; for (let i = 0; i < 10; i++) { const ribbon = new Ribbon(); ribbons.push(ribbon); } function loop() { ctx.clearRect(0, 0, canvas.width, canvas.height); ribbons.forEach((ribbon) => { ribbon.update(); ribbon.draw(); }); requestAnimationFrame(loop); } loop(); }, }; ``` 在mounted()生命周期函数中,我们首先获取canvas元素和其上下文,然后将canvas的宽度设置为与其父元素相同。接着我们定义了一个Ribbon类,该类包含了x、y、color、speed、offset等属性,以及update()和draw()方法,用于更新彩色丝带的位置和绘制彩色丝带。最后,我们创建多个彩色丝带并进行动画绘制。 在loop()函数中,我们首先调用了clearRect()方法来清空canvas画布,然后遍历所有彩色丝带,对每个丝带进行更新和绘制。最后,我们使用requestAnimationFrame()方法来不断循环调用loop()函数,实现动画效果。 这就是一个简单的canvas-vue彩色丝带展示的实现方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值