数据库内容渲染为 chart (图表)的可视化分享

本文参考文章:

《利用 数据库 和 Chart(图表) 做小朋友身高管理》

300 积分悬赏,将数据库内容渲染为 Char(图表) - 链滴


效果图

image

效果视频

具体功能(自定义)

  1. 关联数据库自定义
  2. 图标 X 轴、Y 轴、固定虚线、计算方法选择等自定义

代码

(async () => {

//1.关联数据库配置区域
    // 关联的数据库块id
    const avBlockId = '20250715013326-ab0y38s';

    // 关联的图表块id
    const chartBlockId = '20250715020010-95gpkrh';

    //数据库数据同步到图表的列名,注意名称需对应
    const mydata = '记录日期';
    const number = '压力值';

//2.图表的设置
    // 起点开始日期(格式:YYYY-MM-DD HH:mm)
    const baseDate = new Date('2025-07-25 00:00');

    //此处对应数据库‘压力值’列计算方法,自行尝试
    // 选择显示模式:'raw' 原始值 或 'cumulative' 累计值
    const displayMode = 'cumulative'; // 可更改为 'raw' 或 'cumulative'

    //固定虚线的数值设置
    const guding = 400;

   //Y轴的最大值和最小值设置
    const ymin = -100;
    const ymax = 800;
    
    //X轴的时间单位选择,跨度数量选择
    // 时间单位选择:'hour'(小时)、'day'(日)、'month'(月)、'year'(年)
    const timeUnit = 'day'; // 可更改为 'hour', 'day', 'month', 'year'
    
    // 时间跨度数量
    const timeSpan = 8; // 时间跨度数量(若小时单位下建议48小时)
    
//3.X轴标签格式配置
    // X轴标签格式
    const timeUnitFormat = {
        hour: '{MM}月{dd}日{HH}时',    // 小时单位格式:月/日 时:00  {yyyy}-{MM}-{dd} {HH}:{mm}
        day: '{MM}-{dd}',             // 天单位格式:月-日
        month: '{yyyy}-{MM}',         // 月单位格式:年-月
        year: '{yyyy}'                // 年单位格式:年
    };

    // 点-悬浮提示日期格式:
    const tooltipDateFormat = {
        hour: '{yyyy}-{MM}-{dd} {HH}:{mm}',  // 小时单位提示格式:年-月-日 时:分
        day: '{yyyy}-{MM}-{dd} {HH}:{mm}',   // 天单位提示格式:年-月-日
        month: '{yyyy}-{MM}',                 // 月单位提示格式:年-月
        year: '{yyyy}'                        // 年单位提示格式:年
    };    
    
//4.X轴标签显示设置
    // X轴标签旋转角度(单位:度)
    // 0: 水平显示, 45: 斜角显示, 90: 垂直显示
    const xAxisLabelRotate = 0; // 可更改为 0, 45, 90 等
   
//================================================================

    // X轴标签显示间隔(0表示全部显示,1表示隔一个显示一个,以此类推)
    const xAxisLabelInterval = 0; // 可更改为 0, 1, 2 等

    // 轴指针标签格式(暂时不知道有什么用,可能与时间精度有关,删了也不影响,留着先)
    const axisPointerFormat = {
        hour: '{yyyy}-{MM}-{dd} {HH}:{mm}',  // 小时单位指针格式:年-月-日 时:分
        day: '{yyyy}-{MM}-{dd}',              // 天单位指针格式:年-月-日
        month: '{yyyy}-{MM}',                 // 月单位指针格式:年-月
        year: '{yyyy}'                        // 年单位指针格式:年
    };

    // 根据时间单位计算结束日期
    const endDate = new Date(baseDate);
    if (timeUnit === 'hour') {
        endDate.setHours(baseDate.getHours() + timeSpan);
    } else if (timeUnit === 'day') {
        endDate.setDate(baseDate.getDate() + timeSpan);
    } else if (timeUnit === 'month') {
        endDate.setMonth(baseDate.getMonth() + timeSpan);
    } else if (timeUnit === 'year') {
        endDate.setFullYear(baseDate.getFullYear() + timeSpan);
    }

    // 自动刷新延迟
    const autoFreshDelay = 1000;

    // 实际测量数据
    let actualMeasurements = [];

    // 根据时间单位生成基准日期
    let baseDates = [];
    for (let i = 0; i < timeSpan; i++) {
        const date = new Date(baseDate);
        if (timeUnit === 'hour') {
            date.setHours(baseDate.getHours() + i);
        } else if (timeUnit === 'day') {
            date.setDate(baseDate.getDate() + i);
        } else if (timeUnit === 'month') {
            date.setMonth(baseDate.getMonth() + i);
        } else if (timeUnit === 'year') {
            date.setFullYear(baseDate.getFullYear() + i);
        }
        // 分钟和秒归零
        date.setMinutes(0, 0, 0);
        baseDates.push(date);
    }
  
    // 生成临界值数据(固定guding)
    const standardData = baseDates.map((date, index) => ({
        date: date.getTime(),
        height: guding,
        index: index + 1
    }));
  
    // 生成时间轴数据
    const xAxisData = baseDates.map(date => date.getTime());

    // 转换临界值数据为时间戳格式
    const standardSeriesData = standardData.map(item => ({
        name: `${timeUnit === 'hour' ? '第' : ''}${item.index}${timeUnit === 'hour' ? '小时' : timeUnit === 'day' ? '天' : timeUnit === 'month' ? '月' : '年'}`,
        value: [item.date, item.height],
        index: item.index,
        height: item.height,
        date: item.date
    }));

    // 获取时间单位中文名称
    const timeUnitName = {
        hour: '小时',
        day: '日',
        month: '月',
        year: '年'
    }[timeUnit] || '单位';

    let option = {
        title: {
            text: `压力值变化曲线 (${timeSpan}${timeUnitName})`,
            subtext: `临界值:${guding} pa | 显示模式:${displayMode === 'raw' ? '原始值' : '累计值'}`,
            left: 'center',
            top: 20,
            textStyle: {
                fontSize: 18
            },
            subtextStyle: {
                fontSize: 12
            }
        },
        tooltip: {
            trigger: 'item',
            formatter: function(params) {
                if (params.seriesName === '临界值') {
                    const index = params.data?.index;
                    let unitLabel;
                    if (timeUnit === 'hour') unitLabel = '小时';
                    else if (timeUnit === 'day') unitLabel = '天';
                    else if (timeUnit === 'month') unitLabel = '月';
                    else if (timeUnit === 'year') unitLabel = '年';
                    
                    return `第${index}${unitLabel}<br/>临界值:${guding} pa`;
                }
                try {
                    if (Array.isArray(params.value)) {
                        const timestamp = params.value[0];
                        const height = params.value[1];
                        // 使用配置的日期格式
                        const dateFormat = tooltipDateFormat[timeUnit] || '{yyyy}-{MM}-{dd}';
                        const dateStr = echarts.time.format(timestamp, dateFormat, false);
                        
                        if (displayMode === 'cumulative') {
                            return `测量时间:${dateStr}<br/>累计压力值:${height.toFixed(1)}pa`;
                        } else {
                            return `测量时间:${dateStr}<br/>实际压力值:${height.toFixed(1)}pa`;
                        }
                    }
                } catch (e) {
                    console.error('工具提示处理失败:', e);
                    return `数据解析错误:${e.message}`;
                }
                return '数据格式异常';
            }
        },
        legend: {
            data: ['临界值', displayMode === 'raw' ? '实际压力值' : '累计压力值'],
            top: 60,
            right: 20,
            itemGap: 20
        },
        grid: {
            top: 100
        },
        xAxis: {
            type: 'time',
            name: '日期',
            min: baseDates[0].getTime(),
            max: baseDates[baseDates.length - 1].getTime(),
            axisLabel: {
                formatter: function(value) {
                    // 使用配置的日期格式
                    const format = timeUnitFormat[timeUnit] || '{yyyy}';
                    return echarts.time.format(value, format, false);
                },
                interval: xAxisLabelInterval, // 使用配置的显示间隔
                rotate: xAxisLabelRotate // 使用配置的旋转角度
            },
            axisPointer: {
                label: {
                    formatter: function(params) {
                        // 使用配置的日期格式
                        const format = axisPointerFormat[timeUnit] || '{yyyy}-{MM}-{dd}';
                        return echarts.time.format(params.value, format, false);
                    }
                }
            }
        },
        dataZoom: [
            {
                type: 'slider',
                show: true,
                xAxisIndex: 0,
                start: 0,
                end: 100,
                minSpan: 10,
                maxSpan: 100,
                filterMode: 'none'
            },
            {
                type: 'inside',
                xAxisIndex: 0,
                zoomLock: false,
                zoomOnMouseWheel: 'shift',
                moveOnMouseMove: true,
                moveOnMouseWheel: true
            }
        ],
        yAxis: {
            type: 'value',
            name: '压力 (pa)',
            min: ymin,
            max: ymax
        },
        series: [
            {
                name: '临界值',
                data: standardSeriesData,
                type: 'line',
                smooth: true,
                encode: {
                    x: 'value[0]',
                    y: 'value[1]',
                    tooltip: ['index', 'height']
                },
                itemStyle: { color: '#ccc' },
                lineStyle: { type: 'dashed' },
                areaStyle: { color: 'rgba(200, 200, 200, 0.1)' }
            },
            {
                name: displayMode === 'raw' ? '实际压力值' : '累计压力值',
                data: [],
                type: 'line',
                smooth: true,
                itemStyle: { color: '#1890ff' },
                lineStyle: { width: 2 },
                symbolSize: 8,
                encode: {
                    x: 0,
                    y: 1
                },
                dimensions: [
                    { name: 'timestamp', type: 'time' },
                    { name: 'height', type: 'number' }
                ]
            }
        ]
    }

    // 获取数据库信息并格式化数据
    async function getAVDataByBlockId(blockId, callback) {
        // 获取块信息
        const block = await fetchSyncPost('/api/query/sql', {"stmt": `SELECT * FROM blocks WHERE id = '${blockId}'`})
        const markdown = block.data[0]?.markdown;
        // 获取数据库信息
        if(markdown){
            // 获取数据库文件地址
            const avId = getDataAvIdFromHtml(markdown);

            // 通过sy文件获取表格数据
            const av = await fetchSyncPost('/api/file/getFile', {"path":`/data/storage/av/${avId}.json`});

            if(av){
                if(typeof callback === 'function') callback(av);
            } else {
                option = "未找到av-id=" + avId + "的数据库文件";
            }
        } else {
            option = "未找到id=" + blockId + "的数据库块";
        }
    }

    // 请求api
    async function fetchSyncPost (url, data) {
        const init = {
            method: "POST",
        };
        if (data) {
            if (data instanceof FormData) {
                init.body = data;
            } else {
                init.body = JSON.stringify(data);
            }
        }
        const res = await fetch(url, init);
        return await res.json();
    }

    // 获取avid
    function getDataAvIdFromHtml(htmlString) {
        const match = htmlString.match(/data-av-id="([^"]+)"/);
        return match && match[1] ? match[1] : "";
    }

    // 现在调用函数
    await getAVDataByBlockId(avBlockId, (av) => {
        // 动态生成实际测量数据
        const rawValues = av.keyValues
            .find(kv => kv.key.name === mydata).values
            .map((dateValue, index) => {
                const heightValue = av.keyValues
                    .find(kv => kv.key.name === number).values[index].number.content;
                
                let finalDate;
                if (typeof dateValue.date.content === 'number') {
                    finalDate = new Date(dateValue.date.content);
                } else {
                    finalDate = new Date(dateValue.date.content);
                }
          
                return {
                    date: finalDate,
                    height: parseFloat(heightValue) || 0 // 确保是数字
                };
            });

        // 按日期排序
        rawValues.sort((a, b) => a.date - b.date);

        // 转换实际测量数据为时间戳格式
        let actualSeriesData;
        
        if (displayMode === 'cumulative') {
            // 累计模式:每个点是前面所有点的和
            let cumulativeSum = 0;
            actualSeriesData = rawValues.map(({date, height}) => {
                if (isNaN(date.getTime())) {
                    console.error('无效日期格式:', date);
                    return null;
                }
                
                cumulativeSum += height;
                return [date.getTime(), cumulativeSum];
            });
        } else {
            // 原始模式:直接使用每个点的值
            actualSeriesData = rawValues.map(({date, height}) => {
                if (isNaN(date.getTime())) {
                    console.error('无效日期格式:', date);
                    return null;
                }
          
                return [date.getTime(), height];
            });
        }

        // 过滤无效数据
        actualSeriesData = actualSeriesData.filter(item => item !== null);

        // 更新图表数据和标题
        option.series[1].data = actualSeriesData;
        option.title.text = av.name;
    });

    return option;
})()
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28446 引用 • 119783 回帖
4 操作
chenhao396 在 2025-07-29 00:30:53 更新了该帖
chenhao396 在 2025-07-16 02:06:47 更新了该帖
chenhao396 在 2025-07-16 02:05:28 更新了该帖
chenhao396 在 2025-07-16 01:41:04 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • image.png
    我这里有两种计算方法,一个是原始值,一个是累计值,你改一下看看

    还有就是你的 y 轴太高了,加起来的和才 13,不明显,你改小一点,再看看

    2 回复
  • 其他回帖
  • FFFFFFire

    快进 ⏩ 到数据库的图表视图

  • image.png

    我更新了代码,最新版会显示具体的记录时间,你再试试

    image.png

    还有关于时间错位问题,你这个日期列,最好也要具体到分钟;如果只显示到日,你看到列是 26 号,其实具体时间可能是 25 号 23 点,所以图标上的时间其实是对的,只是数据库列显示有问题。

    1 回复
  • Z-QL

    我是改了这个地方的 但是因为比较小白,所以我只看懂了·改这个地方,然后发现不对,比如我 23 号有 2 次,它上面不会显示,然后我添加 24 号有 2 次,会显示 23 号 共累计 4 次,然后我添加 27 号有两次,他又正常了变为 27 号累计 6 次,但是我添加 28 号次数他是直接加在 27 号上的

  • 查看全部回帖