最近12个月空气质量分布echarts history配置项内容和展示

AQI预测 - 空气质量预报 - 最近12个月空气质量分布

配置项如下
      const uploadedDataURL = '/asset/get/s/data-1635390802457-HRSoVX4EN.json';

// AQI 等级
const AQI_LEVELS = [
    {
        icon: 'k1',
        label: '优',
        otherName: '一级',
        value: 1,
        min: 0,
        max: 50,
        proposal: '空气质量令人满意,基本无空气污染',
        influence: '各类人群可正常活动',
        color: '#00E500',
        lightColor: '#D2F5A5',
    },
    {
        icon: 'k2',
        label: '良',
        otherName: '二级',
        value: 2,
        min: 51,
        max: 100,
        proposal: '空气质量可接受,但某些污染物可能对极少数异常敏感人群健康有较弱影响',
        influence: '极少数异常敏感人群应减少户外活动',
        color: '#FFFF00',
        lightColor: '#EBEBD8',
    },
    {
        icon: 'k3',
        label: '轻度',
        otherName: '三级',
        value: 3,
        min: 101,
        max: 150,
        proposal: '易感人群症状有轻度加剧,健康人群出现刺激症状',
        influence: '儿童、老年人及心脏病、呼吸系统疾病患者应减少长时间、高强度的户外锻炼',
        color: '#FF7E00',
        lightColor: '#F2E1C7',
    },
    {
        icon: 'k4',
        label: '中度',
        otherName: '四级',
        value: 4,
        min: 151,
        max: 200,
        proposal: '进一步加剧易感人群症状,可能对健康人群心脏、呼吸系统有影响',
        influence: '儿童、老年人及心脏病、呼吸系统疾病患者避免长时间、高强度的户外锻炼,一般人群适量减少户外运动',
        color: '#FE0000',
        lightColor: '#F3D3D3',
    },
    {
        icon: 'k5',
        label: '重度',
        otherName: '五级',
        value: 5,
        min: 201,
        max: 300,
        proposal: '心脏病和肺病患者症状显著加剧,运动耐受力降低,健康人群普遍出现症状',
        influence: '儿童、老年人及心脏病、肺病患者应停留在室内,停止户外运动,一般人群减少户外运动',
        color: '#99004C',
        lightColor: '#E5D2ED',
    },
    {
        icon: 'k6',
        label: '严重',
        otherName: '六级',
        value: 6,
        min: 301,
        max: '+',
        proposal: '健康人群运动耐受力降低,有明显强烈症状,提前出现某些疾病',
        influence: '儿童、老年人和病人应停留在室内,避免体力消耗,一般人群避免户外活动',
        color: '#7E0022',
        lightColor: '#F3CBD7',
    },
];

// 首要污染物别名配置
const PRIMARY = {
    NA: '无',
};

// 获取首要污染物Label
const getPrimaryLabel = (value) => {
    if (!value) return '';
    if (PRIMARY[value]) return PRIMARY[value];
    return value;
};

/**
 * @description 将接口数据转化为符合 echarts 的数据
 * @param {object} sourceData 原数据
 * @param {boolean} diff 是否按AQI等级拆分数据
 * @returns echartsData 符合echarts配置数据
 */
const transformData = (sourceData = {}, isDiff = false) => {
    let seriesData = [];
    let seriesDatas = [];
    for (const key in sourceData) {
        const value = (sourceData[key] || {}).aqi || null;
        seriesData.push([key, value]); // 笛卡尔坐标数据
    }

    // 按AQI等级区分数据
    if (isDiff) {
        seriesDatas = AQI_LEVELS.map(({ label, min, max, color }) => {
            const data = seriesData.filter(([, value]) => {
                if (max === '+') return value >= min;
                return value >= min && value <= max;
            });
            return { name: label, data, color };
        });
        seriesDatas.push({
            name: '空值',
            color: '#efefef',
            data: seriesData.filter((v) => v[1] === -1),
        });
    }

    // 开始日期
    const startDay = seriesData[0][0];
    // 结束日期
    const endDay = seriesData[seriesData.length - 1][0];

    return { startDay, endDay, seriesData, seriesDatas };
};

// 获取悬浮提示元素
const getTooltipRow = (params, isHideEmpty = false) => {
    if (isHideEmpty && !params.value) return '';
    return `
  <div style="display: flex; justify-content: space-between; min-width: 80px; padding-top: 3px;">
    <span>${params.marker || ''} ${params.seriesName}</span>
    <span style="padding-left: 10px; font-weight: bold;">${
        params.value === -1 || !params.value ? '-' : params.value
    }</span>
  </div>`;
};

// 补全日期
const getVirtulData = (data = {}, format) => {
    const today = new Date();
    const dayTime = 3600 * 24 * 1000;
    const thatday = today - dayTime * 365;
    const result = JSON.parse(JSON.stringify(data));
    for (let time = thatday; time < today; time += dayTime) {
        const key = format(new Date(time), 'Y-m-d');
        if (!result[key])
            result[key] = {
                aqi: -1,
                aqidate: key,
                data_type: 'history',
                primary: '',
            };
    }
    return result;
};

const formatDate = (dateObj = null, fmt = 'Y-m-d', empty = '-') => {
    const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
    const months = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];

    const lz = (n) => {
        const num = n.toString();
        if (num.length === 1) {
            return `0${num}`;
        }
        return num;
    };

    let t = dateObj;
    // typeof dateObj !== 'string'是为了判断万一传过来的是y-m-d h:i:s格式,会直接执行最后一个else if,格式化为日期格式
    if (dateObj === null || (Number.isNaN(dateObj) && typeof dateObj !== 'string')) {
        if (typeof empty === 'string') {
            return empty;
        }
        t = new Date();
    } else if (typeof dateObj === 'number') {
        if (dateObj === 0) {
            return empty;
        }
        t = new Date(dateObj * 1000);
    } else if (typeof dateObj === 'string') {
        if (parseInt(dateObj) == dateObj) {
            // eslint-disable-line
            t = new Date(dateObj * 1000);
        } else {
            if (dateObj.includes('T')) {
                t = new Date(dateObj);
            } else {
                t = new Date(dateObj.replace(/-/g, '/'));
            }
        }
    }
    if (!(t instanceof Date)) {
        throw new Error('被处理的只能是Date对象');
    }
    if (Number.isNaN(t.getTime())) {
        if (typeof dateObj === 'string') {
            return dateObj;
        }
        throw new Error('非法日期对象');
    }

    const date = t.getDate();
    const year = t.getFullYear();
    const hours = t.getHours();
    const day = t.getDay();
    const dayName = days[day];
    const min = t.getMinutes();
    const sec = t.getSeconds();
    const month = t.getMonth();
    const am = hours < 12 ? 'am' : 'pm';
    const ms = t.getTime() % 1000;
    let hours12 = hours % 12;
    hours12 = hours12 || 12;

    const fd = new Date(t.getFullYear(), 0, 1);
    const dayOfYear = Math.round((t - fd) / 8.64e7);
    const weekNum = Math.ceil(((t - fd) / 8.64e7 + fd.getDay() - 1) / 7);

    const a = {
        d: lz(date),
        D: dayName.substr(0, 3),
        l: dayName,
        w: day,
        j: date,
        z: dayOfYear,
        W: weekNum,
        M: months[month].substr(0, 3),
        F: months[month],
        Y: year,
        a: am,
        A: am.toUpperCase(),
        y: `${year}`.substr(2, 2),
        c: t.toISOString(),
        m: lz(month + 1),
        U: Math.round(t / 1000),
        g: hours12,
        G: lz(hours12),
        h: hours,
        H: lz(hours),
        i: lz(min),
        s: lz(sec),
        e: ms,
    };

    const v = [];
    Object.keys(a).forEach((k) => {
        v.push(`(\\\\)?${k}`);
    });

    return fmt.replace(new RegExp(v.join('|'), 'g'), (k) => {
        if (a[k]) {
            return a[k];
        }
        return k.replace('\\', '');
    });
};

$.getJSON(uploadedDataURL, (apiData) => {
    const data = getVirtulData(apiData.history, formatDate);
    const titleText = '最近12个月空气质量分布';
    const visualMapPieces = AQI_LEVELS.map(({ min, max, color }) => ({ min, max, color }));
    const { seriesDatas } = transformData(data, true);
    const getPrimary = (key) => getPrimaryLabel((data[key] || {}).primary) || '';
    const myOption = {
        title: {
            text: titleText,
            left: 'center',
        },
        tooltip: {
            position: 'top',
            formatter: (params) => {
                const primary = getPrimary(params.value[0]);
                return `${params.value[0]}<br />${getTooltipRow({ ...params, value: params.value[1] })}${getTooltipRow({
                    seriesName: '首要污染物',
                    value: primary,
                })}`;
            },
        },
        legend: {
            show: true,
            icon: 'circle',
            bottom: 0,
        },
        calendar: {
            // range: [startDay, endDay],
            range: new Date().getFullYear(),
            top: 60,
            left: 60,
            right: 15,
            bottom: 40,
            height: 150,
            cellSize: [14, 14],
            itemStyle: {
                borderWidth: 4,
                borderColor: '#fff',
            },
            yearLabel: {
                show: false,
            },
            splitLine: {
                show: false,
            },
            monthLabel: {
                nameMap: 'cn',
            },
            dayLabel: {
                nameMap: ['日', '一', '二', '三', '四', '五', '六'],
                firstDay: 1,
                left: 10,
            },
        },
        visualMap: {
            show: false,
            type: 'piecewise',
            pieces: [{ max: 0, min: -10, color: '#efefef', name: '空值' }, ...visualMapPieces],
        },
        series: seriesDatas.map(({ name, data, color }) => ({
            type: 'heatmap',
            coordinateSystem: 'calendar',
            name,
            data,
            color,
            label: {
                show: true,
                fontSize: 10,
                formatter: (params) => getPrimary(params.value[0]),
            },
        })),
    };
    myChart.setOption(myOption);
});

    
截图如下