使用 Vue 和 ECharts 创建交互式图表

使用 Vue 和 ECharts 创建交互式图表

引言

在现代 Web 应用中,数据可视化是一个重要的组成部分。它不仅能够帮助用户更好地理解复杂的数据,还能提升用户体验。

技术背景

Vue.js

Vue.js 是一个渐进式 JavaScript 框架,用于构建用户界面。它易于上手,同时提供了强大的功能来构建复杂的单页应用。Vue 的响应式系统使得数据绑定变得简单高效。

ECharts

ECharts 是一个基于 JavaScript 的开源可视化库,由百度前端技术部开发。它提供了丰富的图表类型和高度可定制的配置选项,适用于各种数据可视化需求。

项目搭建

首先,需要创建一个新的 Vue 项目。如果还没有安装 Vue CLI,可以通过以下命令进行安装:

bash 复制代码
npm install -g @vue/cli

然后,创建一个新的 Vue 项目:

bash 复制代码
vue create my-chart-app
cd my-chart-app

接下来,安装 ECharts:

bash 复制代码
npm install echarts

代码说明

  1. 图表容器

    • 使用 ref 获取图表容器的 DOM 元素。
    • onMounted 生命周期钩子中初始化 ECharts 实例并调用 updateChart 方法更新图表配置。
  2. 图表类型选择

    • 使用 v-model 绑定图表类型,并在选择改变时调用 updateChart 方法更新图表。
  3. 数据编辑

    • 提供两个模态对话框,一个用于编辑单个数据点,另一个用于编辑所有数据点。
    • 使用计算属性 selectedXAxisValueselectedSeriesValue 来同步选中的数据点的 X 轴值和系列数据值。
    • 提供 addDataPointdeleteDataPoint 方法来添加和删除数据点,并在操作后调用 updateChart 方法更新图表。
  4. 图表配置

    • 根据不同的图表类型(折线图、柱状图、饼图、散点图),设置不同的图表配置。
    • 使用 label 属性常驻显示数值标签,并在饼图中使用 labelLine 属性设置连接线的样式。

代码实现

typescript 复制代码
<script setup lang="ts">
import { defineComponent, onMounted, ref, computed } from 'vue'
import * as echarts from 'echarts'

// 定义图表容器引用
const chartRef = ref<HTMLDivElement | null>(null)
let chartInstance: echarts.ECharts | null = null

// 定义图表数据
const xAxisData = ref(["初始阶段", "开发阶段", "完成阶段"])
const seriesData = ref([10, 50, 80])

const chartType = ref('line')

// 初始化图表
const initChart = () => {
    if (!chartRef.value) return
    chartInstance = echarts.init(chartRef.value)

    updateChart()
}

// 更新图表配置
const updateChart = () => {
    if (!chartInstance) return

    let option;

    switch (chartType.value) {
        case 'line':
        case 'bar':
            option = {
                tooltip: {
                    trigger: 'axis',
                    formatter: '{b}: {c}'
                },
                legend: {
                    orient: 'vertical',
                    left: 'left',
                    textStyle: { color: '#666' }
                },
                xAxis: {
                    show: true,
                    type: 'category',
                    data: xAxisData.value,
                    axisLine: { lineStyle: { color: '#999' } },
                    axisLabel: { color: '#666' }
                },
                yAxis: {
                    show: true,
                    type: 'value',
                    axisLine: { lineStyle: { color: '#999' } },
                    splitLine: { lineStyle: { color: ['#eaeaea'], width: 1, type: 'dashed' } },
                    axisLabel: { color: '#666' }
                },
                series: [
                    {
                        data: seriesData.value,
                        type: chartType.value,
                        itemStyle: { color: '#5470c6' },
                        label: { // 常驻显示数值标签
                            show: true,
                            position: 'top', // 标签位置
                            color: '#666'
                        },
                        ...(chartType.value === 'line' ? { areaStyle: { color: 'rgba(84, 112, 198, 0.3)' } } : {})
                    }
                ],
                grid: { left: '5%', right: '5%', bottom: '10%' }
            };
            break;
        case 'pie':
            option = {
                tooltip: {
                    trigger: 'item',
                    formatter: '{a} <br/>{b}: {c} ({d}%)'
                },
                legend: {
                    orient: 'vertical',
                    left: 'left',
                    textStyle: { color: '#666' }
                },
                xAxis: {
                    show: false // 明确禁用 X 轴
                },
                yAxis: {
                    show: false // 明确禁用 Y 轴
                },
                series: [
                    {
                        name: '数据',
                        type: 'pie',
                        radius: ['40%', '70%'],
                        avoidLabelOverlap: false,
                        label: {
                            show: true, // 常驻显示数值标签
                            position: 'outside', // 标签位置
                            formatter: '{b}: {c} ({d}%)', // 自定义标签格式
                            color: '#666'
                        },
                        emphasis: {
                            label: { show: true, fontSize: '20', fontWeight: 'bold' }
                        },
                        data: xAxisData.value.map((name, index) => ({
                            name,
                            value: seriesData.value[index],
                            itemStyle: { color: ['#5470c6', '#91cc75', '#fac858'][index % 3] }
                        }))
                    }
                ]
            };
            break;
        case 'scatter':
            option = {
                tooltip: {
                    trigger: 'item',
                    formatter: '{b}: {c}'
                },
                legend: {
                    orient: 'vertical',
                    left: 'left',
                    textStyle: { color: '#666' }
                },
                xAxis: {
                    show: true,
                    type: 'category',
                    data: xAxisData.value,
                    axisLine: { lineStyle: { color: '#999' } },
                    axisLabel: { color: '#666' }
                },
                yAxis: {
                    show: true,
                    type: 'value',
                    axisLine: { lineStyle: { color: '#999' } },
                    splitLine: { lineStyle: { color: ['#eaeaea'], width: 1, type: 'dashed' } },
                    axisLabel: { color: '#666' }
                },
                series: [
                    {
                        symbolSize: 20,
                        data: xAxisData.value.map((name, index) => [index, seriesData.value[index]]),
                        type: 'scatter',
                        label: { // 常驻显示数值标签
                            show: true,
                            position: 'top', // 标签位置
                            color: '#666'
                        },
                        itemStyle: { color: '#5470c6' }
                    }
                ]
            };
            break;
        default:
            option = {};
    }

    chartInstance.setOption(option)
    console.log('option',option)
}

// 监听图表点击事件
onMounted(() => {
    initChart()

    chartInstance?.on('click', (params) => {
        showModalSingle.value = true;
        selectedDataIndex.value = params.dataIndex ?? -1;
    });
})

// 处理 X 轴数据变化
const handleXAxisChange = (index: number, value: string) => {
    xAxisData.value[index] = value
    updateChart()
}

// 处理系列数据变化
const handleSeriesChange = (index: number, value: string) => {
    seriesData.value[index] = parseFloat(value)
    updateChart()
}

// 模态对话框状态
const showModalSingle = ref(false);
const showModalAll = ref(false);
const selectedDataIndex = ref(-1);

// 计算属性:获取选中的 X 轴值
const selectedXAxisValue = computed({
    get: () => xAxisData.value[selectedDataIndex.value],
    set: (newValue) => handleXAxisChange(selectedDataIndex.value, newValue)
});

// 计算属性:获取选中的系列数据值
const selectedSeriesValue = computed({
    get: () => seriesData.value[selectedDataIndex.value].toString(),
    set: (newValue) => handleSeriesChange(selectedDataIndex.value, newValue)
});

// 添加数据点
const addDataPoint = () => {
    xAxisData.value.push(`新数据点 ${xAxisData.value.length + 1}`);
    seriesData.value.push(0);
    updateChart(); // 更新图表以反映新增的数据点
};

// 删除数据点
const deleteDataPoint = (index: number) => {
    xAxisData.value.splice(index, 1);
    seriesData.value.splice(index, 1);
    updateChart();
};
</script>

<template>
  <!-- 图表容器 -->
  <div ref="chartRef" :style="{ width: '100%', height: '400px', backgroundColor: '#fff', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)' }"></div>

  <!-- 图表类型选择 -->
  <select v-model="chartType" @change="updateChart" style="margin-top: 20px; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
    <option value="line">折线图</option>
    <option value="bar">柱状图</option>
    <option value="pie">饼图</option>
    <option value="scatter">散点图</option>
  </select>

  <!-- 编辑所有数据按钮 -->
  <button @click="showModalAll = true" style="margin-top: 20px; margin-left: 10px; padding: 8px 16px; background-color: #5470c6; color: #fff; border: none; border-radius: 4px; cursor: pointer;">
    编辑所有数据
  </button>

  <!-- 单个数据点模态对话框 -->
  <div v-if="showModalSingle" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center;">
    <div style="background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);">
      <h3>编辑数据点 {{ selectedDataIndex + 1 }}</h3>
      <div>
        <label>X轴数据:</label>
        <input 
          :value="selectedXAxisValue" 
          @input="selectedXAxisValue = ($event.target as HTMLInputElement).value" 
          style="width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #ccc; border-radius: 4px;"
        />
      </div>
      <div>
        <label>系列数据:</label>
        <input 
          :value="selectedSeriesValue" 
          @input="selectedSeriesValue = ($event.target as HTMLInputElement).value" 
          style="width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #ccc; border-radius: 4px;"
        />
      </div>
      <button @click="showModalSingle = false" style="margin-top: 10px; padding: 8px 16px; background-color: #5470c6; color: #fff; border: none; border-radius: 4px; cursor: pointer;">
        关闭
      </button>
    </div>
  </div>

  <!-- 所有数据模态对话框 -->
  <div v-if="showModalAll" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center;">
    <div style="background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); width: 80%; max-width: 600px;">
      <h3>编辑所有数据</h3>
      <table style="width: 100%; border-collapse: collapse;">
        <thead>
          <tr>
            <th style="padding: 8px; text-align: left; background-color: #f2f2f2; color: #333;">序号</th>
            <th style="padding: 8px; text-align: left; background-color: #f2f2f2; color: #333;">X轴数据</th>
            <th style="padding: 8px; text-align: left; background-color: #f2f2f2; color: #333;">系列数据</th>
            <th style="padding: 8px; text-align: left; background-color: #f2f2f2; color: #333;">操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(item, index) in xAxisData" :key="index">
            <td style="border-bottom: 1px solid #ddd; padding: 8px;">{{ index + 1 }}</td>
            <td style="border-bottom: 1px solid #ddd; padding: 8px;">
              <input 
                :value="xAxisData[index]" 
                @input="handleXAxisChange(index, ($event.target as HTMLInputElement).value)" 
                style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"
              />
            </td>
            <td style="border-bottom: 1px solid #ddd; padding: 8px;">
              <input 
                :value="seriesData[index]" 
                @input="handleSeriesChange(index, ($event.target as HTMLInputElement).value)" 
                style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"
              />
            </td>
            <td style="border-bottom: 1px solid #ddd; padding: 8px;">
              <button @click="deleteDataPoint(index)" style="padding: 8px 16px; background-color: #ff4d4f; color: #fff; border: none; border-radius: 4px; cursor: pointer;">
                删除
              </button>
            </td>
          </tr>
        </tbody>
      </table>
      <button @click="addDataPoint" style="margin-top: 10px; padding: 8px 16px; background-color: #5470c6; color: #fff; border: none; border-radius: 4px; cursor: pointer;">
        添加数据点
      </button>
      <button @click="showModalAll = false" style="margin-top: 10px; padding: 8px 16px; background-color: #5470c6; color: #fff; border: none; border-radius: 4px; cursor: pointer;">
        关闭
      </button>
    </div>
  </div>
</template>
相关推荐
桂月二二30 分钟前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
沈梦研2 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062062 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb2 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角2 小时前
CSS 颜色
前端·css
轻口味2 小时前
Vue.js 组件之间的通信模式
vue.js
浪浪山小白兔3 小时前
HTML5 新表单属性详解
前端·html·html5
lee5763 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579653 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me4 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架