Echarts-丝带图

Echarts-丝带图

demo地址

打开CodePen

什么是丝带图?

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

效果

思路

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

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

核心代码

  • 以散点图构建柱状图
js 复制代码
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]
    }
  };
}
  • 生成折线图数据
js 复制代码
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;
}
  • 生成折线图配置
js 复制代码
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];
}

完整代码

js 复制代码
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);
相关推荐
孜然卷k1 分钟前
前端导出word文件,并包含导出Echarts图表等
前端·javascript
家里有只小肥猫22 分钟前
uniApp小程序保存canvas图片
前端·小程序·uni-app
前端大全25 分钟前
Chrome 推出全新的 DOM API,彻底革新 DOM 操作!
前端·chrome
八角丶36 分钟前
元素尺寸的获取方式及区别
前端·javascript·html
冴羽44 分钟前
Svelte 最新中文文档教程(16)—— Context(上下文)
前端·javascript·svelte
前端小臻1 小时前
关于css中bfc的理解
前端·css·bfc
白嫖不白嫖1 小时前
网页版的俄罗斯方块
前端·javascript·css
HappyAcmen1 小时前
关于Flutter前端面试题及其答案解析
前端·flutter
顾比魁1 小时前
pikachu之CSRF防御:给你的请求加上“网络身份证”
前端·网络·网络安全·csrf
林的快手1 小时前
CSS文本属性
前端·javascript·css·chrome·node.js·css3·html5