数据可视化交互

  数据可视化交互  

【实验名称】

数据可视化交互

【实验目的】

1. 了解数据可视化的一般原则

2. 掌握数据可视化的分类

3. 掌握数据可视化的常见技术

4. 本次实验是对全国的空气质量进行可视化分析并进行数据统计技术对比

【实验原理】

设计可视化系统或选择交互方式的时候,除了能够完成任 务本身之外,还要遵循一些基本的原则。例如,交互的延时性需要在用户可以接受的范围之内,并有效控制用户交互的成本。这些基本原则对交互的效果起着至关重要的作用。

另外,交互的技术有很多种,本次实验是对文本进行可视 化生成词云图片与传统的统计技术对比。 交互的原则、交互的分类以及常见的交互技术,尤其是几种常见的交互技术,只有熟练掌握并使用恰当,才可能设计出用户体验良好的可视化应用。尽管交互的技术有很多种,但交互技术本身并无优劣之分,选择哪种交互技术的依据是具体的场景和应用需求。

【实验环境】

Python

【实验步骤】

一、安装 pyecharts

pip install pyecharts

二、下载数据

下载数据文件(data.txt),该文件表示了一些城市某天的空气质量指数(AQI),请完成如下实验。

三、实验任务

实验 1:AQI 横向对比条形图

任务:使用 Pyecharts 绘制各城市 AQI 值的横向条形图,要求:

1. 按 AQI 从高到低排序

2. 添加全局配置项:标题为“城市 AQI 对比”,坐标轴名称分别为“AQI 指数”和“城市”

3. 使 MarkLine 标记 AQI 均值线(参考值:80),并设置不同颜色区分高于/低于均值的城市

# 实验 1:AQI 横向对比条形图
from pyecharts import options as opts
from pyecharts.charts import Bar

# 城市AQI数据
data = ["海门", 9, "鄂尔多斯", 12, "招远", 12, "舟山", 12, "齐齐哈尔", 14, "盐城", 15, "赤峰", 16, "青岛", 18, "乳山", 18, "金昌", 19, "泉州", 21, "莱西", 21, "日照", 21, "胶南", 22, "南通", 23, "拉萨", 140, "云浮", 24, "梅州", 25, "文登", 25, "上海", 25, "攀枝花", 25, "威海", 25, "承德", 25, "厦门", 26, "汕尾", 26, "潮州", 26, "丹东", 27, "太仓", 27, "曲靖", 27, "烟台", 28, "福州", 29, "瓦房店", 30, "即墨", 30, "抚顺", 31, "玉溪", 31, "张家口", 31, "阳泉", 31, "莱州", 32, "湖州", 32, "汕头", 32, "昆山", 33, "宁波", 33, "湛江", 33, "揭阳", 34, "荣成", 34, "连云港", 35, "葫芦岛", 35, "常熟", 36, "东莞", 36, "河源", 36, "淮安", 36, "泰州", 36, "南宁", 37, "营口", 37, "惠州", 37, "江阴", 37, "蓬莱", 37, "韶关", 38, "嘉峪关", 38, "广州", 38, "延安", 38, "太原", 39, "清远", 39, "中山", 39, "昆明", 39, "寿光", 40, "盘锦", 40, "长治", 41, "深圳", 41, "珠海", 42, "宿迁", 43, "咸阳", 43, "铜川", 44, "平度", 44, "佛山", 44, "海口", 44, "江门", 45, "章丘", 45, "肇庆", 46, "大连", 47, "临汾", 47, "吴江", 47, "石嘴山", 49, "沈阳", 50, "苏州", 50, "茂名", 50, "嘉兴", 51, "长春", 51, "胶州", 52, "银川", 52, "张家港", 52, "三门峡", 53, "锦州", 54, "南昌", 54, "柳州", 54, "三亚", 54, "自贡", 56, "吉林", 56, "阳江", 57, "泸州", 57, "西宁", 57, "宜宾", 58, "呼和浩特", 58, "成都", 58, "大同", 58, "镇江", 59, "桂林", 59, "张家界", 59, "宜兴", 59, "北海", 60, "西安", 61, "金坛", 62, "东营", 62, "牡丹江", 63, "遵义", 63, "绍兴", 63, "扬州", 64, "常州", 64, "潍坊", 65, "重庆", 66, "台州", 67, "南京", 67, "滨州", 70, "贵阳", 71, "无锡", 71, "本溪", 71, "克拉玛依", 72, "渭南", 72, "马鞍山", 72, "宝鸡", 72, "焦作", 75, "句容", 75, "北京", 79, "徐州", 79, "衡水", 80, "包头", 80, "绵阳", 80, "乌鲁木齐", 84, "枣庄", 84, "杭州", 84, "淄博", 85, "鞍山", 86, "溧阳", 86, "库尔勒", 86, "安阳", 90, "开封", 90, "济南", 92, "德阳", 93, "温州", 95, "九江", 96, "邯郸", 98, "临安", 99, "兰州", 99, "沧州", 100, "临沂", 103, "南充", 104, "天津", 105, "富阳", 106, "泰安", 112, "诸暨", 112, "郑州", 113, "哈尔滨", 114, "聊城", 116, "芜湖", 117, "唐山", 119, "平顶山", 119, "邢台", 119, "德州", 120, "济宁", 120, "荆州", 127, "宜昌", 130, "义乌", 132, "丽水", 133, "洛阳", 134, "秦皇岛", 136, "株洲", 143, "石家庄", 147, "莱芜", 148, "常德", 152, "保定", 153, "湘潭", 154, "金华", 157, "岳阳", 169, "长沙", 175, "衢州", 177, "廊坊", 193, "菏泽", 194, "合肥", 229, "武汉", 273, "大庆", 279]

# 提取城市和AQI数据
cities = data[::2]
aqi_values = data[1::2]

# 组合数据并按AQI从高到低排序
combined_data = list(zip(cities, aqi_values))
combined_data.sort(key=lambda x: x[1], reverse=True)

# 分离排序后的数据
sorted_cities = [item[0] for item in combined_data]
sorted_aqi = [item[1] for item in combined_data]

# 为每个城市设置颜色(高于均值为红色,低于均值为绿色)
colors = []
mean_aqi = 80
for aqi in sorted_aqi:
    if aqi >= mean_aqi:
        colors.append("#FF4500")  # 红色
    else:
        colors.append("#32CD32")  # 绿色

# 创建横向条形图
bar = (
    Bar()
    .add_xaxis(sorted_cities)
    .add_yaxis(
        "AQI值",
        sorted_aqi,
        label_opts=opts.LabelOpts(position="right"),
        itemstyle_opts=opts.ItemStyleOpts(color=lambda x: colors[x.data_index]),
        markline_opts=opts.MarkLineOpts(
            data=[opts.MarkLineItem(y=mean_aqi, name="AQI均值线")]
        )
    )
    .reversal_axis()  # 反转坐标轴,使条形图横向显示
    .set_global_opts(
        title_opts=opts.TitleOpts(title="城市 AQI 对比"),
        xaxis_opts=opts.AxisOpts(name="AQI 指数"),
        yaxis_opts=opts.AxisOpts(name="城市"),
        toolbox_opts=opts.ToolboxOpts(is_show=True),
    )
)

# 渲染图表
bar.render("city_aqi_comparison.html")



1

实验 2:AQI 等级分布饼图

任务:基于 AQI 等级划分(优/良/轻度污染/中度污染/重度污 染),绘制饼图并添加交互功能:

1. 使用 Pie 图表展示各等级下城市数量占比

2. 标签显示百分比和等级名称,突出显示占比最大的扇区

3. 添加点击事件:单击扇区时弹出该等级详细城市数量及城市名称列表

import matplotlib.pyplot as plt
from matplotlib.patches import Patch

# 设置中文字体
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
plt.rcParams["axes.unicode_minus"] = False  # 解决负号显示问题

# AQI数据
aqi_data = {
    "海门": 9, "鄂尔多斯": 12, "招远": 12, "舟山": 12, "齐齐哈尔": 14,
    "盐城": 15, "赤峰": 16, "青岛": 18, "乳山": 18, "金昌": 19,
    "泉州": 21, "莱西": 21, "日照": 21, "胶南": 22, "南通": 23,
    "拉萨": 140, "云浮": 24, "梅州": 25, "文登": 25, "上海": 25,
    "攀枝花": 25, "威海": 25, "承德": 25, "厦门": 26, "汕尾": 26,
    "潮州": 26, "丹东": 27, "太仓": 27, "曲靖": 27, "烟台": 28,
    "福州": 29, "瓦房店": 30, "即墨": 30, "抚顺": 31, "玉溪": 31,
    "张家口": 31, "阳泉": 31, "莱州": 32, "湖州": 32, "汕头": 32,
    "昆山": 33, "宁波": 33, "湛江": 33, "揭阳": 34, "荣成": 34,
    "连云港": 35, "葫芦岛": 35, "常熟": 36, "东莞": 36, "河源": 36,
    "淮安": 36, "泰州": 36, "南宁": 37, "营口": 37, "惠州": 37,
    "江阴": 37, "蓬莱": 37, "韶关": 38, "嘉峪关": 38, "广州": 38,
    "延安": 38, "太原": 39, "清远": 39, "中山": 39, "昆明": 39,
    "寿光": 40, "盘锦": 40, "长治": 41, "深圳": 41, "珠海": 42,
    "宿迁": 43, "咸阳": 43, "铜川": 44, "平度": 44, "佛山": 44,
    "海口": 44, "江门": 45, "章丘": 45, "肇庆": 46, "大连": 47,
    "临汾": 47, "吴江": 47, "石嘴山": 49, "沈阳": 50, "苏州": 50,
    "茂名": 50, "嘉兴": 51, "长春": 51, "胶州": 52, "银川": 52,
    "张家港": 52, "三门峡": 53, "锦州": 54, "南昌": 54, "柳州": 54,
    "三亚": 54, "自贡": 56, "吉林": 56, "阳江": 57, "泸州": 57,
    "西宁": 57, "宜宾": 58, "呼和浩特": 58, "成都": 58, "大同": 58,
    "镇江": 59, "桂林": 59, "张家界": 59, "宜兴": 59, "北海": 60,
    "西安": 61, "金坛": 62, "东营": 62, "牡丹江": 63, "遵义": 63,
    "绍兴": 63, "扬州": 64, "常州": 64, "潍坊": 65, "重庆": 66,
    "台州": 67, "南京": 67, "滨州": 70, "贵阳": 71, "无锡": 71,
    "本溪": 71, "克拉玛依": 72, "渭南": 72, "马鞍山": 72, "宝鸡": 72,
    "焦作": 75, "句容": 75, "北京": 79, "徐州": 79, "衡水": 80,
    "包头": 80, "绵阳": 80, "乌鲁木齐": 84, "枣庄": 84, "杭州": 84,
    "淄博": 85, "鞍山": 86, "溧阳": 86, "库尔勒": 86, "安阳": 90,
    "开封": 90, "济南": 92, "德阳": 93, "温州": 95, "九江": 96,
    "邯郸": 98, "临安": 99, "兰州": 99, "沧州": 100, "临沂": 103,
    "南充": 104, "天津": 105, "富阳": 106, "泰安": 112, "诸暨": 112,
    "郑州": 113, "哈尔滨": 114, "聊城": 116, "芜湖": 117, "唐山": 119,
    "平顶山": 119, "邢台": 119, "德州": 120, "济宁": 120, "荆州": 127,
    "宜昌": 130, "义乌": 132, "丽水": 133, "洛阳": 134, "秦皇岛": 136,
    "株洲": 143, "石家庄": 147, "莱芜": 148, "常德": 152, "保定": 153,
    "湘潭": 154, "金华": 157, "岳阳": 169, "长沙": 175, "衢州": 177,
    "廊坊": 193, "菏泽": 194, "合肥": 229, "武汉": 273, "大庆": 279
}


# AQI等级划分函数
def get_aqi_level(aqi):
    if aqi <= 50:
        return "优"
    elif aqi <= 100:
        return "良"
    elif aqi <= 150:
        return "轻度污染"
    elif aqi <= 200:
        return "中度污染"
    else:
        return "重度污染"


# 统计各等级城市数量和列表
level_counts = {"优": 0, "良": 0, "轻度污染": 0, "中度污染": 0, "重度污染": 0}
level_cities = {"优": [], "良": [], "轻度污染": [], "中度污染": [], "重度污染": []}

for city, aqi in aqi_data.items():
    level = get_aqi_level(aqi)
    level_counts[level] += 1
    level_cities[level].append(city)

# 准备绘图数据
labels = list(level_counts.keys())
sizes = list(level_counts.values())
colors = ['#4CAF50', '#FFEB3B', '#FF9800', '#FF5722', '#B71C1C']  # 绿色(优)、黄色(良)、橙色(轻度)、红色(中度)、深红色(重度)
explode = [0.1 if s == max(sizes) else 0 for s in sizes]  # 突出显示最大扇区

# 创建图形和子图
fig, ax = plt.subplots(figsize=(12, 8))

# 绘制饼图
patches, texts, autotexts = ax.pie(
    sizes,
    explode=explode,
    labels=labels,
    colors=colors,
    autopct='%1.1f%%',
    shadow=True,
    startangle=90,
    pctdistance=0.85
)

# 设置标签和百分比文本的属性
plt.setp(autotexts, size=10, weight="bold")
plt.setp(texts, size=12)

# 添加白色圆环
centre_circle = plt.Circle((0, 0), 0.70, fc='white')
fig.gca().add_artist(centre_circle)

# 设置标题和图例
ax.set_title('各AQI等级城市数量占比分布', fontsize=16, pad=20)
legend_elements = [Patch(facecolor=colors[i], label=f'{labels[i]}') for i in range(len(labels))]
ax.legend(
    handles=legend_elements,
    loc='upper right',
    bbox_to_anchor=(1.2, 1),  # 调整图例位置避免遮挡饼图
    title="AQI等级"
)


# 点击事件处理函数
def on_pie_click(event):
    if event.inaxes == ax:  # 确保点击在饼图区域内
        for i, patch in enumerate(patches):
            if patch.contains(event)[0]:  # 判断是否点击了某个扇区
                level = labels[i]
                count = sizes[i]
                cities = level_cities[level]

                # 格式化城市列表(每行显示5个城市)
                city_text = "\n".join([", ".join(cities[j:j + 5]) for j in range(0, len(cities), 5)])

                # 创建弹窗显示详细信息
                fig_info = plt.figure(figsize=(10, 6))
                fig_info.suptitle(f'{level} - 城市列表', fontsize=14, fontweight='bold')
                ax_info = fig_info.add_subplot(111)
                ax_info.text(
                    0.5, 0.9,
                    f'等级:{level}\n城市数量:{count}',
                    horizontalalignment='center',
                    verticalalignment='top',
                    fontsize=12
                )
                ax_info.text(
                    0.5, 0.5,
                    city_text,
                    horizontalalignment='left',
                    verticalalignment='center',
                    fontsize=10,
                    wrap=True  # 自动换行
                )
                ax_info.axis('off')  # 隐藏坐标轴
                plt.tight_layout()
                plt.show()
                break


# 绑定点击事件
fig.canvas.mpl_connect('button_press_event', on_pie_click)

# 显示图形(非交互式环境需加此句)
plt.show()

# 保存为图片(如需保存,取消注释)
fig.savefig("aqi_pie_chart.png", dpi=300, bbox_inches='tight')


2

实验 3:多城市 AQI 对比仪表盘

任务:使用 Tab 或 Page 组件构建多图表仪表盘:

1. 第一选项卡:显示 AQI 前 10 城市的横向条形图

2. 第二选项卡:展示西北地区城市 AQI 散点数据(添加回归 线)与东部城市 AQI 散点数据(添加回归线)的对比3. 第三选项卡:组合折线图(各地区 AQI 指数的变化)

4. 要求所有图表共享主题风格(如 ThemeType.DARK)

import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import dash
from dash import dcc, html, Input, Output, State
import dash_bootstrap_components as dbc

# 确保中文显示正常
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

# 创建示例数据
data = {
    "城市": ["海门", "鄂尔多斯", "招远", "舟山", "齐齐哈尔", "盐城", "赤峰", "青岛", "乳山", "金昌",
             "泉州", "莱西", "日照", "胶南", "南通", "拉萨", "云浮", "梅州", "文登", "上海", "攀枝花",
             "威海", "承德", "厦门", "汕尾", "潮州", "丹东", "太仓", "曲靖", "烟台", "福州", "瓦房店",
             "即墨", "抚顺", "玉溪", "张家口", "阳泉", "莱州", "湖州", "汕头", "昆山", "宁波", "湛江",
             "揭阳", "荣成", "连云港", "葫芦岛", "常熟", "东莞", "河源", "淮安", "泰州", "南宁", "营口",
             "惠州", "江阴", "蓬莱", "韶关", "嘉峪关", "广州", "延安", "太原", "清远", "中山", "昆明",
             "寿光", "盘锦", "长治", "深圳", "珠海", "宿迁", "咸阳", "铜川", "平度", "佛山", "海口", "江门",
             "章丘", "肇庆", "大连", "临汾", "吴江", "石嘴山", "沈阳", "苏州", "茂名", "嘉兴", "长春", "胶州",
             "银川", "张家港", "三门峡", "锦州", "南昌", "柳州", "三亚", "自贡", "吉林", "阳江", "泸州", "西宁",
             "宜宾", "呼和浩特", "成都", "大同", "镇江", "桂林", "张家界", "宜兴", "北海", "西安", "金坛", "东营",
             "牡丹江", "遵义", "绍兴", "扬州", "常州", "潍坊", "重庆", "台州", "南京", "滨州", "贵阳", "无锡", "本溪",
             "克拉玛依", "渭南", "马鞍山", "宝鸡", "焦作", "句容", "北京", "徐州", "衡水", "包头", "绵阳", "乌鲁木齐",
             "枣庄", "杭州", "淄博", "鞍山", "溧阳", "库尔勒", "安阳", "开封", "济南", "德阳", "温州", "九江", "邯郸",
             "临安", "兰州", "沧州", "临沂", "南充", "天津", "富阳", "泰安", "诸暨", "郑州", "哈尔滨", "聊城", "芜湖",
             "唐山", "平顶山", "邢台", "德州", "济宁", "荆州", "宜昌", "义乌", "丽水", "洛阳", "秦皇岛", "株洲",
             "石家庄",
             "莱芜", "常德", "保定", "湘潭", "金华", "岳阳", "长沙", "衢州", "廊坊", "菏泽", "合肥", "武汉", "大庆"],
    "AQI": [9, 12, 12, 12, 14, 15, 16, 18, 18, 19, 21, 21, 21, 22, 23, 140, 24, 25, 25, 25, 25, 25, 25,
            26, 26, 26, 27, 27, 27, 28, 29, 30, 30, 31, 31, 31, 31, 32, 32, 32, 33, 33, 33, 34, 34, 35,
            35, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 38, 38, 38, 38, 39, 39, 39, 39, 40, 40, 41, 41, 42,
            43, 43, 44, 44, 44, 44, 45, 45, 46, 47, 47, 47, 49, 50, 50, 50, 51, 51, 52, 52, 52, 53, 54, 54,
            54, 54, 56, 56, 57, 57, 57, 58, 58, 58, 58, 59, 59, 59, 59, 60, 61, 62, 62, 63, 63, 63, 64, 64,
            65, 66, 67, 67, 70, 71, 71, 71, 72, 72, 72, 72, 75, 75, 79, 79, 80, 80, 80, 84, 84, 84, 85, 86,
            86, 86, 90, 90, 92, 93, 95, 96, 98, 99, 99, 100, 103, 104, 105, 106, 112, 112, 113, 114, 116, 117,
            119, 119, 119, 120, 120, 127, 130, 132, 133, 134, 136, 143, 147, 148, 152, 153, 154, 157, 169, 175,
            177, 193, 194, 229, 273, 279]
}

# 地区分类
northwest_cities = ["呼和浩特", "包头", "乌海", "赤峰", "通辽", "鄂尔多斯", "呼伦贝尔", "巴彦淖尔", "乌兰察布",
                    "银川", "石嘴山", "吴忠", "固原", "中卫", "西宁", "海东", "拉萨", "日喀则", "昌都", "林芝",
                    "山南", "那曲", "乌鲁木齐", "克拉玛依", "吐鲁番", "哈密", "阿克苏", "喀什", "和田", "昌吉",
                    "博尔塔拉", "巴音郭楞", "克孜勒苏柯尔克孜", "伊犁哈萨克", "塔城", "阿勒泰", "西安", "铜川",
                    "宝鸡", "咸阳", "渭南", "延安", "汉中", "榆林", "安康", "商洛", "兰州", "嘉峪关", "安康", "商洛",
                    "兰州", "嘉峪关", "金昌", "白银",
                    "天水", "武威", "张掖", "平凉", "酒泉", "庆阳", "定西", "陇南", "临夏", "甘南"]

east_cities = ["北京", "天津", "河北", "山西", "内蒙古", "辽宁", "吉林", "黑龙江", "上海", "江苏", "浙江", "安徽",
               "福建", "江西", "山东", "广东", "广西", "海南", "台湾", "香港", "澳门"]

# 创建DataFrame
df = pd.DataFrame(data)

# 添加地区分类
df['地区'] = df['城市'].apply(lambda x: '西北' if x in northwest_cities else ('东部' if x in east_cities else '其他'))

# 创建时间序列数据(模拟AQI随时间的变化)
dates = pd.date_range(start='2023-01-01', end='2023-12-31', freq='D')
regions = ['西北', '东部', '南部', '中部', '东北']

time_series_data = []
for region in regions:
    # 为每个地区创建不同的基础值和波动
    base_value = np.random.randint(30, 80)
    trend = np.random.uniform(0.01, 0.05) if np.random.choice([True, False]) else -np.random.uniform(0.01, 0.05)

    for date in dates:
        # 添加季节性波动和随机噪声
        day_of_year = date.dayofyear
        seasonal_effect = 10 * np.sin(2 * np.pi * day_of_year / 365)
        random_noise = np.random.normal(0, 10)

        aqi = base_value + trend * day_of_year + seasonal_effect + random_noise
        aqi = max(0, aqi)  # 确保AQI不为负

        time_series_data.append({
            '日期': date,
            '地区': region,
            'AQI': aqi
        })

# 创建时间序列DataFrame
df_time = pd.DataFrame(time_series_data)

# 初始化Dash应用
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.DARKLY])
app.title = "城市AQI对比分析仪表盘"

# 移除错误的favicon设置
# app._favicon = ("https://picsum.photos/200/200")

# 应用布局
app.layout = dbc.Container([
    dbc.Row([
        dbc.Col(html.H1("城市AQI对比分析仪表盘", className="text-center mb-4"), width=12)
    ]),

    dbc.Tabs([
        # 第一个选项卡:AQI前10城市横向条形图
        dbc.Tab(label="AQI前10城市", children=[
            dbc.Container([
                dbc.Row([
                    dbc.Col([
                        html.H3("空气质量指数最高的10个城市", className="mt-4"),
                        dcc.Graph(id='top-10-cities-bar')
                    ])
                ]),
                dbc.Row([
                    dbc.Col([
                        html.Div([
                            html.H4("数据说明:", className="mt-3"),
                            html.P(
                                "此图表展示了空气质量指数(AQI)最高的10个城市。AQI值越高表示空气质量越差,对健康的影响也越大。"),
                            html.P(
                                "通过分析这些城市的AQI情况,可以帮助我们了解哪些地区的空气污染问题最为严重,从而采取相应的措施。"),
                            html.P("数据来源:示例数据(非真实数据)"),
                        ], className="bg-dark p-3 rounded")
                    ])
                ])
            ], fluid=True)
        ]),

        # 第二个选项卡:西北地区与东部城市AQI散点对比图
        dbc.Tab(label="西北与东部城市对比", children=[
            dbc.Container([
                dbc.Row([
                    dbc.Col([
                        html.H3("西北地区与东部城市AQI对比", className="mt-4"),
                        dcc.Graph(id='scatter-comparison')
                    ])
                ]),
                dbc.Row([
                    dbc.Col([
                        html.Div([
                            html.H4("数据说明:", className="mt-3"),
                            html.P(
                                "此图表对比了西北地区和东部城市的空气质量指数(AQI)。散点图中的每个点代表一个城市,颜色区分了不同的地区。"),
                            html.P(
                                "图中添加的回归线可以帮助我们观察两个地区AQI的整体趋势和关系。通过对比可以发现不同地理区域空气质量的差异。"),
                            html.P("数据来源:示例数据(非真实数据)"),
                        ], className="bg-dark p-3 rounded")
                    ])
                ])
            ], fluid=True)
        ]),

        # 第三个选项卡:各地区AQI指数变化折线图
        dbc.Tab(label="各地区AQI变化趋势", children=[
            dbc.Container([
                dbc.Row([
                    dbc.Col([
                        html.H3("各地区AQI指数随时间变化趋势", className="mt-4"),
                        dcc.Graph(id='time-series-chart')
                    ])
                ]),
                dbc.Row([
                    dbc.Col([
                        html.Div([
                            html.H4("数据说明:", className="mt-3"),
                            html.P(
                                "此图表展示了不同地区空气质量指数(AQI)随时间的变化趋势。通过观察趋势图,可以分析各地区空气质量的季节性变化、长期趋势等。"),
                            html.P(
                                "数据覆盖了2023年全年,按地区进行了分类。这有助于比较不同地区AQI的变化模式和波动情况。"),
                            html.P("数据来源:示例数据(非真实数据)"),
                        ], className="bg-dark p-3 rounded")
                    ])
                ]),
                dbc.Row([
                    dbc.Col([
                        html.H5("日期范围选择:", className="mt-3"),
                        dcc.DatePickerRange(
                            id='date-range',
                            min_date_allowed=dates.min(),
                            max_date_allowed=dates.max(),
                            start_date=dates.min(),
                            end_date=dates.max(),
                            display_format='YYYY-MM-DD',
                            className="bg-dark text-white"
                        )
                    ])
                ])
            ], fluid=True)
        ])
    ]),

    # 页脚
    dbc.Row([
        dbc.Col([
            html.Footer(
                html.P("© 2023 城市AQI对比分析仪表盘 | 数据仅供演示使用", className="text-center mt-4 text-muted"),
                className="border-top mt-5 pt-3"
            )
        ], width=12)
    ])

], fluid=True)


# 回调函数:更新AQI前10城市横向条形图
@app.callback(
    Output('top-10-cities-bar', 'figure'),
    [Input('top-10-cities-bar', 'id')]
)
def update_top_10_bar(_):
    # 选择AQI最高的10个城市
    top_10 = df.sort_values('AQI', ascending=False).head(10)

    # 创建横向条形图
    fig = px.bar(
        top_10,
        x='AQI',
        y='城市',
        orientation='h',
        color='AQI',
        color_continuous_scale='Viridis',
        title='空气质量指数最高的10个城市',
        labels={'AQI': '空气质量指数', '城市': '城市名称'}
    )

    # 更新布局为暗色主题
    fig.update_layout(
        template='plotly_dark',
        paper_bgcolor='rgba(0,0,0,0)',
        plot_bgcolor='rgba(0,0,0,0)',
        font=dict(family="SimHei, WenQuanYi Micro Hei, Heiti TC", size=14),
        title=dict(font=dict(size=20)),
        coloraxis_colorbar=dict(
            title='AQI值',
            thicknessmode='pixels',
            thickness=20,
            lenmode='pixels',
            len=300,
            yanchor='middle',
            y=0.5,
            ticks='outside'
        )
    )

    # 添加AQI数值标签
    fig.update_traces(texttemplate='%{x}', textposition='outside')

    return fig


# 回调函数:更新西北地区与东部城市AQI散点对比图
@app.callback(
    Output('scatter-comparison', 'figure'),
    [Input('scatter-comparison', 'id')]
)
def update_scatter_comparison(_):
    # 筛选西北地区和东部城市的数据
    northwest_data = df[df['地区'] == '西北']
    east_data = df[df['地区'] == '东部']

    # 创建散点图
    fig = make_subplots(rows=1, cols=1)

    # 添加西北地区散点和回归线
    fig.add_trace(
        go.Scatter(
            x=northwest_data['城市'],
            y=northwest_data['AQI'],
            mode='markers',
            name='西北地区',
            marker=dict(
                color='rgba(255, 100, 100, 0.8)',
                size=12,
                line=dict(width=1, color='DarkSlateGrey')
            )
        )
    )

    # 添加东部城市散点和回归线
    fig.add_trace(
        go.Scatter(
            x=east_data['城市'],
            y=east_data['AQI'],
            mode='markers',
            name='东部城市',
            marker=dict(
                color='rgba(100, 149, 237, 0.8)',
                size=12,
                line=dict(width=1, color='DarkSlateGrey')
            )
        )
    )

    # 计算并添加回归线
    # 对于西北地区
    x_nw = np.arange(len(northwest_data))
    z_nw = np.polyfit(x_nw, northwest_data['AQI'], 1)
    p_nw = np.poly1d(z_nw)
    fig.add_trace(
        go.Scatter(
            x=northwest_data['城市'],
            y=p_nw(x_nw),
            mode='lines',
            name='西北回归线',
            line=dict(color='rgba(255, 100, 100, 0.5)', width=3, dash='dash')
        )
    )

    # 对于东部城市
    x_e = np.arange(len(east_data))
    z_e = np.polyfit(x_e, east_data['AQI'], 1)
    p_e = np.poly1d(z_e)
    fig.add_trace(
        go.Scatter(
            x=east_data['城市'],
            y=p_e(x_e),
            mode='lines',
            name='东部回归线',
            line=dict(color='rgba(100, 149, 237, 0.5)', width=3, dash='dash')
        )
    )

    # 更新布局为暗色主题(修正title设置)
    fig.update_layout(
        template='plotly_dark',
        paper_bgcolor='rgba(0,0,0,0)',
        plot_bgcolor='rgba(0,0,0,0)',
        title=dict(
            text='西北地区与东部城市AQI对比',
            font=dict(size=20)
        ),
        xaxis_title='城市',
        yaxis_title='空气质量指数(AQI)',
        font=dict(family="SimHei, WenQuanYi Micro Hei, Heiti TC", size=14),
        legend=dict(
            orientation="h",
            yanchor="bottom",
            y=1.02,
            xanchor="right",
            x=1
        )
    )

    # 更新x轴,使城市名称旋转45度以便阅读
    fig.update_xaxes(tickangle=45)

    return fig


# 回调函数:更新各地区AQI指数变化折线图
@app.callback(
    Output('time-series-chart', 'figure'),
    [Input('date-range', 'start_date'),
     Input('date-range', 'end_date')]
)
def update_time_series_chart(start_date, end_date):
    # 过滤日期范围
    filtered_df = df_time[(df_time['日期'] >= start_date) & (df_time['日期'] <= end_date)]

    # 创建组合折线图
    fig = px.line(
        filtered_df,
        x='日期',
        y='AQI',
        color='地区',
        title='各地区AQI指数随时间变化趋势',
        labels={'日期': '日期', 'AQI': '空气质量指数', '地区': '地区'}
    )

    # 添加平均值参考线
    for region in regions:
        region_data = filtered_df[filtered_df['地区'] == region]
        avg_aqi = region_data['AQI'].mean()

        fig.add_shape(
            type="line",
            x0=start_date,
            y0=avg_aqi,
            x1=end_date,
            y1=avg_aqi,
            line=dict(
                color=px.colors.qualitative.Plotly[regions.index(region)],
                width=1,
                dash="dash",
            ),
            name=f"{region}平均AQI: {avg_aqi:.1f}"
        )

        # 添加平均AQI文本标注
        fig.add_annotation(
            x=end_date,
            y=avg_aqi,
            text=f"{region}平均: {avg_aqi:.1f}",
            showarrow=False,
            yshift=10,
            font=dict(size=12)
        )

    # 更新布局为暗色主题(修正title设置)
    fig.update_layout(
        template='plotly_dark',
        paper_bgcolor='rgba(0,0,0,0)',
        plot_bgcolor='rgba(0,0,0,0)',
        title=dict(
            text='各地区AQI指数随时间变化趋势',
            font=dict(size=20)
        ),
        font=dict(family="SimHei, WenQuanYi Micro Hei, Heiti TC", size=14),
        legend=dict(
            orientation="h",
            yanchor="bottom",
            y=1.02,
            xanchor="right",
            x=1
        )
    )

    # 添加悬停提示格式化
    fig.update_traces(
        hovertemplate='日期: %{x|%Y-%m-%d}<br>AQI: %{y:.1f}<extra></extra>'
    )

    return fig


if __name__ == '__main__':
    # 修改为新的API调用方式
    app.run(debug=True)


3

实验 4:2D 地理 AQI 可视化

任务:结合 Geo 实现二维地理可视化:

1. 在中国地图上标记城市位置,使用不同颜色表示 AQI 值

2. 展示不同地区 AQI 数值

from pyecharts import options as opts
from pyecharts.charts import Geo
from pyecharts.globals import ThemeType, ChartType
import numpy as np

# 城市AQI数据(沿用之前数据)
data = ["海门", 9, "鄂尔多斯", 12, "招远", 12, "舟山", 12, "齐齐哈尔", 14, "盐城", 15, "赤峰", 16, "青岛", 18, "乳山", 18, "金昌", 19, "泉州", 21, "莱西", 21, "日照", 21, "胶南", 22, "南通", 23, "拉萨", 140, "云浮", 24, "梅州", 25, "文登", 25, "上海", 25, "攀枝花", 25, "威海", 25, "承德", 25, "厦门", 26, "汕尾", 26, "潮州", 26, "丹东", 27, "太仓", 27, "曲靖", 27, "烟台", 28, "福州", 29, "瓦房店", 30, "即墨", 30, "抚顺", 31, "玉溪", 31, "张家口", 31, "阳泉", 31, "莱州", 32, "湖州", 32, "汕头", 32, "昆山", 33, "宁波", 33, "湛江", 33, "揭阳", 34, "荣成", 34, "连云港", 35, "葫芦岛", 35, "常熟", 36, "东莞", 36, "河源", 36, "淮安", 36, "泰州", 36, "南宁", 37, "营口", 37, "惠州", 37, "江阴", 37, "蓬莱", 37, "韶关", 38, "嘉峪关", 38, "广州", 38, "延安", 38, "太原", 39, "清远", 39, "中山", 39, "昆明", 39, "寿光", 40, "盘锦", 40, "长治", 41, "深圳", 41, "珠海", 42, "宿迁", 43, "咸阳", 43, "铜川", 44, "平度", 44, "佛山", 44, "海口", 44, "江门", 45, "章丘", 45, "肇庆", 46, "大连", 47, "临汾", 47, "吴江", 47, "石嘴山", 49, "沈阳", 50, "苏州", 50, "茂名", 50, "嘉兴", 51, "长春", 51, "胶州", 52, "银川", 52, "张家港", 52, "三门峡", 53, "锦州", 54, "南昌", 54, "柳州", 54, "三亚", 54, "自贡", 56, "吉林", 56, "阳江", 57, "泸州", 57, "西宁", 57, "宜宾", 58, "呼和浩特", 58, "成都", 58, "大同", 58, "镇江", 59, "桂林", 59, "张家界", 59, "宜兴", 59, "北海", 60, "西安", 61, "金坛", 62, "东营", 62, "牡丹江", 63, "遵义", 63, "绍兴", 63, "扬州", 64, "常州", 64, "潍坊", 65, "重庆", 66, "台州", 67, "南京", 67, "滨州", 70, "贵阳", 71, "无锡", 71, "本溪", 71, "克拉玛依", 72, "渭南", 72, "马鞍山", 72, "宝鸡", 72, "焦作", 75, "句容", 75, "北京", 79, "徐州", 79, "衡水", 80, "包头", 80, "绵阳", 80, "乌鲁木齐", 84, "枣庄", 84, "杭州", 84, "淄博", 85, "鞍山", 86, "溧阳", 86, "库尔勒", 86, "安阳", 90, "开封", 90, "济南", 92, "德阳", 93, "温州", 95, "九江", 96, "邯郸", 98, "临安", 99, "兰州", 99, "沧州", 100, "临沂", 103, "南充", 104, "天津", 105, "富阳", 106, "泰安", 112, "诸暨", 112, "郑州", 113, "哈尔滨", 114, "聊城", 116, "芜湖", 117, "唐山", 119, "平顶山", 119, "邢台", 119, "德州", 120, "济宁", 120, "荆州", 127, "宜昌", 130, "义乌", 132, "丽水", 133, "洛阳", 134, "秦皇岛", 136, "株洲", 143, "石家庄", 147, "莱芜", 148, "常德", 152, "保定", 153, "湘潭", 154, "金华", 157, "岳阳", 169, "长沙", 175, "衢州", 177, "廊坊", 193, "菏泽", 194, "合肥", 229, "武汉", 273, "大庆", 279]

# 解析城市和AQI数据
cities = data[::2]
aqi_values = data[1::2]
geo_data = list(zip(cities, aqi_values))

# 手动补充部分城市的经纬度(Pyecharts内置可能缺失)
custom_coords = {
    "海门": [121.15, 31.89], "鄂尔多斯": [109.98, 39.81], "招远": [120.38, 37.35],
    "舟山": [122.16, 30.01], "齐齐哈尔": [123.97, 47.33], "盐城": [120.13, 33.38],
    "赤峰": [118.87, 42.28], "青岛": [120.33, 36.07], "乳山": [121.52, 36.89],
    "金昌": [102.18, 38.52], "泉州": [118.58, 24.93], "莱西": [120.53, 36.86],
    "日照": [119.46, 35.42], "胶南": [119.97, 35.88], "南通": [120.86, 32.01],
    "拉萨": [91.11, 29.97], "云浮": [112.02, 22.93], "梅州": [116.1, 24.3],
    "文登": [122.05, 37.2], "上海": [121.48, 31.22], "攀枝花": [101.72, 26.58],
    "威海": [122.1, 37.5], "承德": [117.93, 40.97], "厦门": [118.1, 24.46],
    "汕尾": [115.37, 22.78], "潮州": [116.63, 23.68], "丹东": [124.37, 40.13],
    "太仓": [121.1, 31.45], "曲靖": [103.79, 25.51], "烟台": [121.39, 37.52],
    "福州": [119.3, 26.08], "瓦房店": [121.97, 39.63], "即墨": [120.45, 36.38],
    "抚顺": [123.97, 41.97], "玉溪": [102.52, 24.35], "张家口": [114.87, 40.82],
    "阳泉": [113.57, 37.85], "莱州": [119.94, 37.17], "湖州": [120.1, 30.86],
    "汕头": [116.69, 23.39], "昆山": [120.95, 31.39], "宁波": [121.56, 29.86],
    "湛江": [110.35, 21.27], "揭阳": [116.35, 23.55], "荣成": [122.41, 37.16],
    "连云港": [119.16, 34.6], "葫芦岛": [120.83, 40.73], "常熟": [120.74, 31.64],
    "东莞": [113.75, 23.04], "河源": [114.68, 23.73], "淮安": [119.02, 33.59],
    "泰州": [119.92, 32.49], "南宁": [108.33, 22.84], "营口": [122.18, 40.65],
    "惠州": [114.4, 23.09], "江阴": [120.26, 31.91], "蓬莱": [120.75, 37.8],
    "韶关": [113.62, 24.84], "嘉峪关": [98.28, 39.77], "广州": [113.27, 23.13],
    "延安": [109.47, 36.6], "太原": [112.55, 37.87], "清远": [113.01, 23.7],
    "中山": [113.38, 22.52], "昆明": [102.71, 25.04], "寿光": [118.73, 36.86],
    "盘锦": [122.07, 41.12], "长治": [113.12, 36.19], "深圳": [114.07, 22.54],
    "珠海": [113.55, 22.22], "宿迁": [118.3, 33.96], "咸阳": [108.72, 34.36],
    "铜川": [109.11, 35.09], "平度": [119.97, 36.77], "佛山": [113.11, 23.05],
    "海口": [110.35, 20.02], "江门": [113.06, 22.61], "章丘": [117.53, 36.72],
    "肇庆": [112.44, 23.05], "大连": [121.62, 38.92], "临汾": [111.5, 36.08],
    "吴江": [120.63, 31.16], "石嘴山": [106.39, 39.04], "沈阳": [123.43, 41.8],
    "苏州": [120.62, 31.32], "茂名": [110.88, 21.68], "嘉兴": [120.76, 30.77],
    "长春": [125.35, 43.88], "胶州": [120.03, 36.26], "银川": [106.27, 38.47],
    "张家港": [120.55, 31.87], "三门峡": [111.19, 34.77], "锦州": [121.15, 41.13],
    "南昌": [115.89, 28.68], "柳州": [109.4, 24.33], "三亚": [109.51, 18.25],
    "自贡": [104.78, 29.35], "吉林": [126.57, 43.87], "阳江": [111.95, 21.85],
    "泸州": [105.44, 28.89], "西宁": [101.77, 36.62], "宜宾": [104.63, 28.77],
    "呼和浩特": [111.65, 40.82], "成都": [104.06, 30.67], "大同": [113.3, 40.12],
    "镇江": [119.44, 32.2], "桂林": [110.28, 25.29], "张家界": [110.48, 29.11],
    "宜兴": [119.82, 31.36], "北海": [109.12, 21.49], "西安": [108.95, 34.27],
    "金坛": [119.56, 31.74], "东营": [118.49, 37.46], "牡丹江": [129.58, 44.6],
    "遵义": [106.9, 27.7], "绍兴": [120.58, 30.01], "扬州": [119.42, 32.39],
    "常州": [119.95, 31.79], "潍坊": [119.1, 36.62], "重庆": [106.5, 29.53],
    "台州": [121.42, 28.66], "南京": [118.78, 32.04], "滨州": [118.03, 37.36],
    "贵阳": [106.71, 26.57], "无锡": [120.29, 31.59], "本溪": [123.73, 41.3],
    "克拉玛依": [84.77, 45.59], "渭南": [109.5, 34.52], "马鞍山": [118.48, 31.67],
    "宝鸡": [107.15, 34.38], "焦作": [113.21, 35.24], "句容": [119.16, 31.95],
    "北京": [116.4, 39.9], "徐州": [117.18, 34.26], "衡水": [115.72, 37.74],
    "包头": [109.83, 40.65], "绵阳": [104.73, 31.48], "乌鲁木齐": [87.68, 43.8],
    "枣庄": [117.57, 34.86], "杭州": [120.16, 30.27], "淄博": [118.05, 36.81],
    "鞍山": [122.85, 41.12], "溧阳": [119.48, 31.43], "库尔勒": [86.06, 41.68],
    "安阳": [114.35, 36.1], "开封": [114.35, 34.79], "济南": [117, 36.65],
    "德阳": [104.37, 31.13], "温州": [120.65, 28.01], "九江": [115.97, 29.71],
    "邯郸": [114.47, 36.6], "临安": [119.72, 30.23], "兰州": [103.83, 36.06],
    "沧州": [116.83, 38.33], "临沂": [118.35, 35.05], "南充": [106.11, 30.8],
    "天津": [117.2, 39.13], "富阳": [119.95, 30.07], "泰安": [117.13, 36.18],
    "诸暨": [120.23, 29.71], "郑州": [113.65, 34.76], "哈尔滨": [126.63, 45.75],
    "聊城": [115.97, 36.45], "芜湖": [118.38, 31.33], "唐山": [118.02, 39.63],
    "平顶山": [113.29, 33.75], "邢台": [114.48, 37.05], "德州": [116.29, 37.45],
    "济宁": [116.59, 35.38], "荆州": [112.23, 30.33], "宜昌": [111.3, 30.7],
    "义乌": [120.06, 29.32], "丽水": [119.92, 28.45], "洛阳": [112.44, 34.7],
    "秦皇岛": [119.57, 39.95], "株洲": [113.16, 27.83], "石家庄": [114.48, 38.03],
    "莱芜": [117.67, 36.22], "常德": [111.69, 29.05], "保定": [115.48, 38.85],
    "湘潭": [112.91, 27.87], "金华": [119.64, 29.12], "岳阳": [113.09, 29.37],
    "长沙": [112.98, 28.12], "衢州": [118.88, 28.97], "廊坊": [116.7, 39.53],
    "菏泽": [115.48, 35.23], "合肥": [117.27, 31.86], "武汉": [114.31, 30.52],
    "大庆": [125.03, 46.58]
}

# 构建带坐标的数据集
def get_coordinate(city):
    try:
        return Geo().get_coordinate(city) or custom_coords[city]
    except:
        return None  # 忽略无法识别的城市

formatted_data = []
for city, aqi in geo_data:
    coord = get_coordinate(city)
    if coord:
        formatted_data.append((city, aqi, coord[0], coord[1]))

# 提取有效数据
cities_with_coord = [d[0] for d in formatted_data]
aqi_with_coord = [d[1] for d in formatted_data]
lons = [d[2] for d in formatted_data]
lats = [d[3] for d in formatted_data]

# 准备可视化数据
visual_data = [(city, aqi) for city, aqi in zip(cities_with_coord, aqi_with_coord)]

# 创建地理可视化
c = (
    Geo(init_opts=opts.InitOpts(
        theme=ThemeType.DARK,
        width="1200px",
        height="800px"
    ))
    .add_schema(
        maptype="china",  # 使用中国地图
        zoom=1.2,  # 初始缩放比例
        label_opts=opts.LabelOpts(is_show=False)
    )
    .add(
        series_name="AQI值",
        type_=ChartType.SCATTER,  # 散点图类型
        data_pair=visual_data,
        symbol_size=8,  # 标记大小
        label_opts=opts.LabelOpts(is_show=False),
    )
    .set_global_opts(
        title_opts=opts.TitleOpts(title="中国城市AQI地理分布"),
        visualmap_opts=opts.VisualMapOpts(
            is_piecewise=False,  # 连续型映射
            max_=max(aqi_with_coord),
            min_=min(aqi_with_coord),
            range_color=["#00FFFF", "#00FF00", "#FFFF00", "#FF0000"],  # 颜色梯度(蓝->绿->黄->红)
            textstyle_opts=opts.TextStyleOpts(color="white")
        )
    )
)

c.render("aqi_geo_visualization.html")


4

实验 5:3D 地理 AQI 可视化

任务:结合 Geo3D 和 Bar3D 实现三维地理可视化:

1. 在地球模型上标记城市位置,高度表示 AQI 值

2. 使用 Map3D 配置光照效果和区域颜色(如中国区域高亮)

3. 展示不同地区 AQI 数值

import plotly.graph_objects as go

# AQI数据
aqi_data = {
    "海门": 9, "鄂尔多斯": 12, "招远": 12, "舟山": 12, "齐齐哈尔": 14,
    "盐城": 15, "赤峰": 16, "青岛": 18, "乳山": 18, "金昌": 19,
    "泉州": 21, "莱西": 21, "日照": 21, "胶南": 22, "南通": 23,
    "拉萨": 140, "云浮": 24, "梅州": 25, "文登": 25, "上海": 25,
    "攀枝花": 25, "威海": 25, "承德": 25, "厦门": 26, "汕尾": 26,
    "潮州": 26, "丹东": 27, "太仓": 27, "曲靖": 27, "烟台": 28,
    "福州": 29, "瓦房店": 30, "即墨": 30, "抚顺": 31, "玉溪": 31,
    "张家口": 31, "阳泉": 31, "莱州": 32, "湖州": 32, "汕头": 32,
    "昆山": 33, "宁波": 33, "湛江": 33, "揭阳": 34, "荣成": 34,
    "连云港": 35, "葫芦岛": 35, "常熟": 36, "东莞": 36, "河源": 36,
    "淮安": 36, "泰州": 36, "南宁": 37, "营口": 37, "惠州": 37,
    "江阴": 37, "蓬莱": 37, "韶关": 38, "嘉峪关": 38, "广州": 38,
    "延安": 38, "太原": 39, "清远": 39, "中山": 39, "昆明": 39,
    "寿光": 40, "盘锦": 40, "长治": 41, "深圳": 41, "珠海": 42,
    "宿迁": 43, "咸阳": 43, "铜川": 44, "平度": 44, "佛山": 44,
    "海口": 44, "江门": 45, "章丘": 45, "肇庆": 46, "大连": 47,
    "临汾": 47, "吴江": 47, "石嘴山": 49, "沈阳": 50, "苏州": 50,
    "茂名": 50, "嘉兴": 51, "长春": 51, "胶州": 52, "银川": 52,
    "张家港": 52, "三门峡": 53, "锦州": 54, "南昌": 54, "柳州": 54,
    "三亚": 54, "自贡": 56, "吉林": 56, "阳江": 57, "泸州": 57,
    "西宁": 57, "宜宾": 58, "呼和浩特": 58, "成都": 58, "大同": 58,
    "镇江": 59, "桂林": 59, "张家界": 59, "宜兴": 59, "北海": 60,
    "西安": 61, "金坛": 62, "东营": 62, "牡丹江": 63, "遵义": 63,
    "绍兴": 63, "扬州": 64, "常州": 64, "潍坊": 65, "重庆": 66,
    "台州": 67, "南京": 67, "滨州": 70, "贵阳": 71, "无锡": 71,
    "本溪": 71, "克拉玛依": 72, "渭南": 72, "马鞍山": 72, "宝鸡": 72,
    "焦作": 75, "句容": 75, "北京": 79, "徐州": 79, "衡水": 80,
    "包头": 80, "绵阳": 80, "乌鲁木齐": 84, "枣庄": 84, "杭州": 84,
    "淄博": 85, "鞍山": 86, "溧阳": 86, "库尔勒": 86, "安阳": 90,
    "开封": 90, "济南": 92, "德阳": 93, "温州": 95, "九江": 96,
    "邯郸": 98, "临安": 99, "兰州": 99, "沧州": 100, "临沂": 103,
    "南充": 104, "天津": 105, "富阳": 106, "泰安": 112, "诸暨": 112,
    "郑州": 113, "哈尔滨": 114, "聊城": 116, "芜湖": 117, "唐山": 119,
    "平顶山": 119, "邢台": 119, "德州": 120, "济宁": 120, "荆州": 127,
    "宜昌": 130, "义乌": 132, "丽水": 133, "洛阳": 134, "秦皇岛": 136,
    "株洲": 143, "石家庄": 147, "莱芜": 148, "常德": 152, "保定": 153,
    "湘潭": 154, "金华": 157, "岳阳": 169, "长沙": 175, "衢州": 177,
    "廊坊": 193, "菏泽": 194, "合肥": 229, "武汉": 273, "大庆": 279
}

# 城市经纬度数据
city_coordinates = {
    "海门": [121.15, 31.89], "鄂尔多斯": [109.98, 39.81], "招远": [120.38, 37.35],
    "舟山": [122.16, 30.01], "齐齐哈尔": [123.97, 47.33], "盐城": [120.13, 33.38],
    "赤峰": [118.87, 42.28], "青岛": [120.33, 36.07], "乳山": [121.52, 36.89],
    "金昌": [102.18, 38.52], "泉州": [118.58, 24.93], "莱西": [120.53, 36.86],
    "日照": [119.46, 35.42], "胶南": [119.97, 35.88], "南通": [120.86, 32.01],
    "拉萨": [91.11, 29.97], "云浮": [112.02, 22.93], "梅州": [116.1, 24.3],
    "文登": [122.05, 37.2], "上海": [121.48, 31.22], "攀枝花": [101.72, 26.58],
    "威海": [122.1, 37.5], "承德": [117.93, 40.97], "厦门": [118.1, 24.46],
    "汕尾": [115.37, 22.78], "潮州": [116.63, 23.68], "丹东": [124.37, 40.13],
    "太仓": [121.1, 31.45], "曲靖": [103.79, 25.51], "烟台": [121.39, 37.52],
    "福州": [119.3, 26.08], "瓦房店": [121.97, 39.63], "即墨": [120.45, 36.38],
    "抚顺": [123.97, 41.97], "玉溪": [102.52, 24.35], "张家口": [114.87, 40.82],
    "阳泉": [113.57, 37.85], "莱州": [119.94, 37.17], "湖州": [120.1, 30.86],
    "汕头": [116.69, 23.39], "昆山": [120.95, 31.39], "宁波": [121.56, 29.86],
    "湛江": [110.35, 21.27], "揭阳": [116.35, 23.55], "荣成": [122.41, 37.16],
    "连云港": [119.16, 34.6], "葫芦岛": [120.83, 40.73], "常熟": [120.74, 31.64],
    "东莞": [113.75, 23.04], "河源": [114.68, 23.73], "淮安": [119.02, 33.59],
    "泰州": [119.92, 32.49], "南宁": [108.33, 22.84], "营口": [122.18, 40.65],
    "惠州": [114.4, 23.09], "江阴": [120.26, 31.91], "蓬莱": [120.75, 37.8],
    "韶关": [113.62, 24.84], "嘉峪关": [98.28, 39.77], "广州": [113.27, 23.13],
    "延安": [109.47, 36.6], "太原": [112.55, 37.87], "清远": [113.01, 23.7],
    "中山": [113.38, 22.52], "昆明": [102.71, 25.04], "寿光": [118.73, 36.86],
    "盘锦": [122.07, 41.12], "长治": [113.12, 36.19], "深圳": [114.07, 22.54],
    "珠海": [113.55, 22.22], "宿迁": [118.3, 33.96], "咸阳": [108.72, 34.36],
    "铜川": [109.11, 35.09], "平度": [119.97, 36.77], "佛山": [113.11, 23.05],
    "海口": [110.35, 20.02], "江门": [113.06, 22.61], "章丘": [117.53, 36.72],
    "肇庆": [112.44, 23.05], "大连": [121.62, 38.92], "临汾": [111.5, 36.08],
    "吴江": [120.63, 31.16], "石嘴山": [106.39, 39.04], "沈阳": [123.43, 41.8],
    "苏州": [120.62, 31.32], "茂名": [110.88, 21.68], "嘉兴": [120.76, 30.77],
    "长春": [125.35, 43.88], "胶州": [120.03, 36.26], "银川": [106.27, 38.47],
    "张家港": [120.55, 31.87], "三门峡": [111.19, 34.77], "锦州": [121.15, 41.13],
    "南昌": [115.89, 28.68], "柳州": [109.4, 24.33], "三亚": [109.51, 18.25],
    "自贡": [104.78, 29.35], "吉林": [126.57, 43.87], "阳江": [111.95, 21.85],
    "泸州": [105.44, 28.89], "西宁": [101.77, 36.62], "宜宾": [104.63, 28.77],
    "呼和浩特": [111.65, 40.82], "成都": [104.06, 30.67], "大同": [113.3, 40.12],
    "镇江": [119.44, 32.2], "桂林": [110.28, 25.29], "张家界": [110.48, 29.11],
    "宜兴": [119.82, 31.36], "北海": [109.12, 21.49], "西安": [108.95, 34.27],
    "金坛": [119.56, 31.74], "东营": [118.49, 37.46], "牡丹江": [129.58, 44.6],
    "遵义": [106.9, 27.7], "绍兴": [120.58, 30.01], "扬州": [119.42, 32.39],
    "常州": [119.95, 31.79], "潍坊": [119.1, 36.62], "重庆": [106.5, 29.53],
    "台州": [121.42, 28.66], "南京": [118.78, 32.04], "滨州": [118.03, 37.36],
    "贵阳": [106.71, 26.57], "无锡": [120.29, 31.59], "本溪": [123.73, 41.3],
    "克拉玛依": [84.77, 45.59], "渭南": [109.5, 34.52], "马鞍山": [118.48, 31.67],
    "宝鸡": [107.15, 34.38], "焦作": [113.21, 35.24], "句容": [119.16, 31.95],
    "北京": [116.4, 39.9], "徐州": [117.18, 34.26], "衡水": [115.72, 37.74],
    "包头": [109.83, 40.65], "绵阳": [104.73, 31.48], "乌鲁木齐": [87.68, 43.8],
    "枣庄": [117.57, 34.86], "杭州": [120.16, 30.27], "淄博": [118.05, 36.81],
    "鞍山": [122.85, 41.12], "溧阳": [119.48, 31.43], "库尔勒": [86.06, 41.68],
    "安阳": [114.35, 36.1], "开封": [114.35, 34.79], "济南": [117, 36.65],
    "德阳": [104.37, 31.13], "温州": [120.65, 28.01], "九江": [115.97, 29.71],
    "邯郸": [114.47, 36.6], "临安": [119.72, 30.23], "兰州": [103.83, 36.06],
    "沧州": [116.83, 38.33], "临沂": [118.35, 35.05], "南充": [106.11, 30.8],
    "天津": [117.2, 39.13], "富阳": [119.95, 30.07], "泰安": [117.13, 36.18],
    "诸暨": [120.23, 29.71], "郑州": [113.65, 34.76], "哈尔滨": [126.63, 45.75],
    "聊城": [115.97, 36.45], "芜湖": [118.38, 31.33], "唐山": [118.02, 39.63],
    "平顶山": [113.29, 33.75], "邢台": [114.48, 37.05], "德州": [116.29, 37.45],
    "济宁": [116.59, 35.38], "荆州": [112.23, 30.33], "宜昌": [111.3, 30.7],
    "义乌": [120.06, 29.32], "丽水": [119.92, 28.45], "洛阳": [112.44, 34.7],
    "秦皇岛": [119.57, 39.95], "株洲": [113.16, 27.83], "石家庄": [114.48, 38.03],
    "莱芜": [117.67, 36.22], "常德": [111.69, 29.05], "保定": [115.48, 38.85],
    "湘潭": [112.91, 27.87], "金华": [119.64, 29.12], "岳阳": [113.09, 29.37],
    "长沙": [112.98, 28.12], "衢州": [118.88, 28.97], "廊坊": [116.7, 39.53],
    "菏泽": [115.48, 35.23], "合肥": [117.27, 31.86], "武汉": [114.31, 30.52],
    "大庆": [125.03, 46.58]
}

# 数据校验
for city in aqi_data:
    if city not in city_coordinates:
        raise ValueError(f"缺失城市经纬度: {city}")

# 数据整理
cities = list(aqi_data.keys())
lons = [city_coordinates[city][0] for city in cities]
lats = [city_coordinates[city][1] for city in cities]
aqi_values = list(aqi_data.values())


# 颜色映射
def get_color(aqi):
    if aqi <= 50:
        return 'rgba(0, 200, 80, 0.8)'  # 绿色(优)
    elif aqi <= 100:
        return 'rgba(255, 165, 0, 0.8)'  # 橙色(良)
    else:
        return 'rgba(255, 50, 50, 0.8)'  # 红色(污染)


# 创建3D可视化(使用Scatter3D模拟柱状图)
fig = go.Figure()

# 为每个城市创建柱状图
for i, (city, lon, lat, aqi) in enumerate(zip(cities, lons, lats, aqi_values)):
    color = get_color(aqi)

    # 创建柱状体(使用线条模拟)
    fig.add_trace(go.Scatter3d(
        x=[lon, lon],
        y=[lat, lat],
        z=[0, aqi],
        mode='lines',
        line=dict(
            color=color,
            width=15  # 控制柱状体粗细
        ),
        hoverinfo='text',
        text=f"{city}<br>AQI: {aqi}",
        showlegend=False
    ))

    # 添加柱状体顶部标记
    fig.add_trace(go.Scatter3d(
        x=[lon],
        y=[lat],
        z=[aqi],
        mode='markers',
        marker=dict(
            size=8,
            color=color,
            symbol='circle',
            line=dict(color='white', width=1)
        ),
        showlegend=False
    ))

# 添加地球模型(通过散点图模拟)
fig.add_trace(go.Scatter3d(
    x=[0], y=[0], z=[0],
    mode='markers',
    marker=dict(
        size=100,
        symbol='circle',
        color='rgb(50, 100, 200)',  # 蓝色地球
        opacity=0.1
    ),
    hoverinfo='none'
))

# 布局设置
fig.update_layout(
    title=dict(
        text='中国城市AQI三维地理可视化',
        x=0.5,
        y=0.95,
        font=dict(size=24, color='darkblue')
    ),
    scene=dict(
        xaxis=dict(title='经度', showgrid=False),
        yaxis=dict(title='纬度', showgrid=False),
        zaxis=dict(title='AQI值', showgrid=False),
        camera=dict(
            eye=dict(x=1.8, y=1.8, z=1.2),  # 调整视角
            up=dict(x=0, y=0, z=1)
        ),
        aspectratio=dict(x=1.2, y=1.2, z=0.6)  # 调整纵横比
    ),
    coloraxis=dict(
        colorscale=[
            [0, 'rgba(0, 200, 80, 0.8)'],  # 绿色(优)
            [0.5, 'rgba(255, 165, 0, 0.8)'],  # 橙色(良)
            [1, 'rgba(255, 50, 50, 0.8)']  # 红色(污染)
        ],
        colorbar=dict(
            title=dict(
                text='AQI等级',
                side='right'  # 正确属性名:title的side参数
            ),
            tickvals=[0, 50, 100, 200, 300],
            ticktext=['优', '良', '轻度污染', '中度污染', '重度污染'],
            x=1.1,  # 色带位置
            thickness=25,
            len=0.8
        )
    ),
    width=1200,
    height=900,
    margin=dict(l=50, r=50, b=50, t=80)
)


# 保存为HTML文件(可选)
fig.write_html("aqi_3d_visualization.html")

5

数据可视化交互实验总结

本次 “数据可视化交互” 实验围绕全国空气质量数据展开,通过五个实验任务全面实践了数据可视化的原则、分类及技术,涵盖二维图表、三维地理可视化及交互功能开发,深化了对数据可视化在实际场景中应用的理解。

一、实验内容与技术应用回顾

(一)基础图表绘制:从条形图到饼图的交互实践

实验 1 使用 Pyecharts 绘制 AQI 横向对比条形图,通过add_xaxisadd_yaxis构建基础图表,结合reversal_axis实现横向显示,并利用MarkLineOpts标记 AQI 均值线。通过颜色区分(红色表示高于均值,绿色表示低于均值),直观展现城市间差异。此过程强化了对 Pyecharts 全局配置项(如标题、坐标轴名称)及数据排序的掌握。

实验 2 基于 Matplotlib 实现 AQI 等级分布饼图,通过pie函数绘制扇形区域,利用autopct显示百分比,并通过explode参数突出占比最大扇区。点击事件的绑定(fig.canvas.mpl_connect)实现了动态交互,单击扇区可弹出城市列表,增强了用户与图表的互动性。

(二)仪表盘与地理可视化:多维度数据呈现

实验 3 采用 Dash 框架构建多选项卡仪表盘,整合 Plotly 图表。第一选项卡通过px.bar展示 AQI 前 10 城市的横向条形图,第二选项卡使用make_subplots对比西北与东部城市的 AQI 散点图并添加回归线,第三选项卡通过px.line呈现各地区 AQI 变化趋势。全局使用暗黑主题(dbc.themes.DARKLY)确保风格统一,日期范围选择组件(dcc.DatePickerRange)实现数据动态过滤。

实验 4 和实验 5 分别基于 Pyecharts 和 Plotly 实现 2D 与 3D 地理可视化。实验 4 通过Geo组件在中国地图上标记城市,以颜色梯度(蓝 - 绿 - 黄 - 红)映射 AQI 值;实验 5 利用Scatter3D模拟柱状图,以高度表示 AQI 值,结合地球模型和光照效果,增强空间维度的视觉冲击力。

二、核心技能提升与工具对比

(一)工具选型与场景适配
  • Pyecharts:适合传统二维图表及地理可视化,配置灵活,支持 HTML 渲染,但在复杂交互(如多选项卡)中需结合其他框架。
  • Matplotlib:底层绘图库,定制性强,但交互功能需手动编码(如点击事件),适合静态图表或轻量级交互。
  • Plotly+Dash:构建交互式仪表盘的黄金组合,Plotly 的动态图表(如可缩放散点图、动态折线图)与 Dash 的组件化布局完美结合,支持数据过滤、实时更新等高级交互。
(二)交互设计原则的实践
  • 清晰性:通过颜色编码(如实验 1 的红绿区分、实验 5 的色带映射)和标签突出关键信息,避免视觉混淆。
  • 响应性:实验 2 的点击弹窗、实验 3 的日期筛选确保用户操作即时反馈,符合交互延时性原则。
  • 一致性:全局使用统一主题(如暗黑模式)和配色方案,增强用户体验的连贯性。

三、问题与优化方向

(一)数据预处理的挑战

实验中部分城市经纬度需手动补充(如实验 4 的custom_coords),反映出真实数据中地理信息缺失的常见问题。未来可通过调用公开 API(如百度地图 API)自动获取坐标,提升数据处理效率。

(二)性能与可视化平衡

实验 5 的 3D 图表在数据量较大时可能出现卡顿,可通过简化图形元素(如减少柱状体数量)或使用 WebGL 加速渲染(如 Three.js)优化性能。

(三)交互深度拓展

当前交互以基础点击和筛选为主,可进一步引入刷选(Brushing)、数据钻取(Drill-down)等高级功能,例如在实验 3 中点击折线图节点查看具体城市数据,提升分析的灵活性。

四、总结与展望

本次实验系统性地覆盖了数据可视化的核心技术栈,从基础图表到地理空间分析,从静态展示到动态交互,不仅验证了理论知识,更积累了实际项目经验。未来可探索以下方向:

  1. 实时数据可视化:结合物联网数据,实现 AQI 数据实时更新与动态图表刷新。
  2. 机器学习集成:在实验 3 的趋势分析中加入预测模型,通过可视化界面展示预测结果。
  3. 移动端适配:优化 Dash 布局,使其兼容手机和平板,提升跨设备用户体验。

数据可视化不仅是技术实现,更是数据故事的讲述方式。通过合理选择工具、遵循设计原则并深化交互逻辑,可让枯燥的空气质量数据转化为直观、易懂的洞察,为环境决策提供有力支持。   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值