Frappe Charts网页图表库学习与应用实例

Apr 11,2020   11344 words   41 min

Tags: Web

前两天无意之间在网上看到了一个叫做Frappe Charts的HTML图表库,试用了一下觉得很棒,所以这篇博客就简单记录一下。官网是这里,Github源代码是这里。并且基于这个开源图表库实现了网站的浏览趋势图绘制,点击查看

1.基本用法

使用Frappe Charts非常简单,首先在Html网页中包含它的JS文件:

<script src="https://cdn.jsdelivr.net/npm/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
<!-- or -->
<script src="https://unpkg.com/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>

然后在Html网页中新建一个<div id="chart"></div>容器,最后在这个容器中再插入JS代码即可。稍微需要注意一下的是其实你完全可以把上面这个地址的JS文件手动下载下来到本地再在HTML文件中引入,就可以离线使用了。完整的Html文件示例代码如下:

<head>
    <script src="https://unpkg.com/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
</head>
<body>
    <div id="chart1">
        <script>
            const data1 = {
                labels: ["12am-3am", "3am-6pm", "6am-9am", "9am-12am",
                    "12pm-3pm", "3pm-6pm", "6pm-9pm", "9am-12am"
                ],
                datasets: [
                    {
                        name: "Some Data", type: "bar",
                        values: [25, 40, 30, 35, 8, 52, 17, -4]
                    },
                    {
                        name: "Another Set", type: "line",
                        values: [25, 50, -10, 15, 18, 32, 27, 14]
                    }
                ]
            }

            const chart1 = new frappe.Chart("#chart1", {  // or a DOM element,
                                                        // new Chart() in case of ES6 module with above usage
                title: "My Awesome Chart",
                data: data1,
                type: 'axis-mixed', // or 'bar', 'line', 'scatter', 'pie', 'percentage'
                height: 350,
                colors: ['#7cd6fd', '#743ee2']
            })
        </script>
    </div>
</body>

这样就可以画出来一个很好看的折线图了,如下所示。

当然,上面这个是最基本的使用方法。默认情况下Frappe Charts绘制的表格会填充满父控件,而且没有边框,有些时候会显得有点难看。可以通过Html的相关属性调整图表的轮廓和大小等等,如下代码所演示。

<head>
    <script src="https://unpkg.com/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
</head>
<body>
<!-- 利用center标签使得图表居中 -->
<center>
<!-- 外层嵌套一个div容器用于绘制轮廓,可以通过相关属性设置轮廓圆角、宽度、颜色等等 -->
<div style="border-width: 1px;border-radius: 5px; border-color:#ddd;border-style:solid;margin-bottom:1.4em; width:80%; height:auto;">
    <!-- 表格绘制的核心代码 -->
    <div id="chart2">
        <script>
            const data2 = {
                labels: ["12am-3am", "3am-6pm", "6am-9am", "9am-12am",
                    "12pm-3pm", "3pm-6pm", "6pm-9pm", "9am-12am"
                ],
                datasets: [
                    {
                        name: "Some Data", type: "bar",
                        values: [25, 40, 30, 35, 8, 52, 17, -4]
                    },
                    {
                        name: "Another Set", type: "line",
                        values: [25, 50, -10, 15, 18, 32, 27, 14]
                    }
                ]
            }

            const chart2 = new frappe.Chart("#chart2", {  // or a DOM element,
                                                        // new Chart() in case of ES6 module with above usage
                title: "My Awesome Chart",
                data: data2,
                type: 'axis-mixed', // or 'bar', 'line', 'scatter', 'pie', 'percentage'
                height: 350,
                colors: ['#7cd6fd', '#743ee2']
            })
        </script>
    </div>
</div>
</center>
</body>

利用上述代码即可以绘制出指定宽度和轮廓的表格,在某些时候会更好看一些。

2.常见图表展示

在Frappe Charts的官方文档中已经展示了一些常见的图表类型和对应属性,这里就简单再展示下,更详细内容请查阅官方文档。

(1)折线图

对应Html网页Demo代码:

<head>
    <script src="https://unpkg.com/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
</head>
<body>
<center>
<div style="border-width: 1px;border-radius: 5px; border-color:#ddd;border-style:solid;width:80%; height:auto;">
    <div id="chart3">
        <script>
            const data3 = {
                labels: ["12am-3am", "3am-6pm", "6am-9am", "9am-12am",
                    "12pm-3pm", "3pm-6pm", "6pm-9pm", "9am-12am"
                ],
                datasets: [
                    {
                        name: "Some Data", type: "bar",
                        values: [25, 40, 30, 35, 8, 52, 17, -4]
                    },
                    {
                        name: "Another Set", type: "line",
                        values: [25, 50, -10, 15, 18, 32, 27, 14]
                    }
                ]
            }

            const chart3 = new frappe.Chart("#chart3", {  // or a DOM element,
                                                        // new Chart() in case of ES6 module with above usage
                title: "My Awesome Chart",
                data: data3,
                type: 'axis-mixed', // or 'bar', 'line', 'scatter', 'pie', 'percentage'
                height: 350,
                colors: ['#7cd6fd', '#743ee2'],
                lineOptions: {
                    regionFill: 1 // default: 0
                    },
            })
        </script>
    </div>
</div>
</center>
</body>

其实仔细观察就会发现和一开始的图表相比只是加了个regionFill属性,其它一些属性简介如下:

  • regionFill:填充折线以下部分,默认为0
  • hideDots:隐藏数据点,只显示折线,默认为0
  • hideLine:隐藏折线,只显示散点,默认为0
  • heatline:热力线,位置越高颜色越深,默认为0
(2)柱状图

对应Html网页Demo代码:

<head>
    <script src="https://unpkg.com/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
</head>
<body>
<center>
<div style="border-width: 1px;border-radius: 5px; border-color:#ddd;border-style:solid;width:80%; height:auto;">
    <div id="chart4">
        <script>
            const data4 = {
                labels: ["12am-3am", "3am-6pm", "6am-9am", "9am-12am",
                    "12pm-3pm", "3pm-6pm", "6pm-9pm", "9am-12am"
                ],
                datasets: [
                    {
                        name: "Some Data", type: "bar",
                        values: [25, 40, 30, 35, 8, 52, 17, -4]
                    },
                    {
                        name: "Another Set", type: "line",
                        values: [25, 50, -10, 15, 18, 32, 27, 14]
                    }
                ]
            }

            const chart4 = new frappe.Chart("#chart4", {  // or a DOM element,
                                                        // new Chart() in case of ES6 module with above usage
                title: "My Awesome Chart",
                data: data4,
                type: 'bar', // or 'bar', 'line', 'scatter', 'pie', 'percentage'
                height: 350,
                colors: ['#7cd6fd', '#743ee2']
            })
        </script>
    </div>
</div>
</center>
</body>

仔细观察的话就会发现,其实只是修改了type一个地方,去掉了regionFill属性(因为在这里没用),其它没有任何修改。

(3)饼图

对应Html网页Demo代码:

<head>
    <script src="https://unpkg.com/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
</head>
<body>
<center>
<div style="border-width: 1px;border-radius: 5px; border-color:#ddd;border-style:solid;width:80%; height:auto;">
    <div id="chart5">
        <script>
            const data5 = {
                labels: ["12am-3am", "3am-6pm", "6am-9am", "9am-12am",
                    "12pm-3pm", "3pm-6pm", "6pm-9pm", "9am-12am"
                ],
                datasets: [
                    {
                        name: "Some Data", type: "bar",
                        values: [25, 40, 30, 35, 8, 52, 17, -4]
                    },
                    {
                        name: "Another Set", type: "line",
                        values: [25, 50, -10, 15, 18, 32, 27, 14]
                    }
                ]
            }

            const chart5 = new frappe.Chart("#chart5", {  // or a DOM element,
                                                        // new Chart() in case of ES6 module with above usage
                title: "My Awesome Chart",
                data: data5,
                type: 'pie', // or 'bar', 'line', 'scatter', 'pie', 'percentage'
                height: 350,
                colors: ['#7cd6fd', '#743ee2']
            })
        </script>
    </div>
</div>
</center>
</body>

还是只修改了type一个地方,改成了pie类型。

(4)Github热力图

对应Html网页Demo代码:

<head>
    <script src="https://unpkg.com/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
</head>
<body>
<center>
<!-- overflow属性可以控制当内容超过容器宽度时出现滚动条,auto表示不超过不显示滚动条,超过才显示 -->
<div style="border-width: 1px;border-radius: 5px; border-color:#ddd;border-style:solid;width:80%; height:auto;overflow-x: auto;">
    <div id="chart6">
        <script>
            // 你需要指定时间范围以及数据
            // 比如以当前时间为开始向前推150天
            var start_time = new Date();
            miliseconds_day = 24*60*60*1000;
            start_time.setTime(start_time.getTime()-(150*miliseconds_day));
            // 以当前时间为开始向后推150天
            var end_time = new Date();
            end_time.setTime(end_time.getTime()+(150*miliseconds_day));
            
            // 利用JS随机生成一些点用于展示
            Points = {};
            for (var i = 0; i < 100; i++) {
                var date_range = (end_time.getTime() - start_time.getTime())/miliseconds_day;
                var value_range = 256;
                var Rand = Math.random();
                base_time = start_time.getTime();
                rand_time = Math.round(Rand * date_range)*miliseconds_day;
                // 需要注意的是上面的运算单位都是毫秒,但绘图需要的单位是秒,所以再转换一下
                tmp_date = Math.round((base_time + rand_time)/1000);
                var tmp_date_str = tmp_date.toString();
                var tmp_value = Math.round(Rand * value_range);
                Points[tmp_date_str] = tmp_value;
            }

            let data6 = {
            dataPoints:Points,  // 数据
            // 如果没有指定起始与结束时间,默认从当前天开始往前推一年
            start:start_time,   // 起始时间,是一个JS的Date对象
            end:end_time    // 终止时间,一个JSDate对象
            }

            let chart6 = new frappe.Chart("#chart6", {
                type: 'heatmap',
                data: data6,
                discreteDomains: 0, // default 1
                colors: ['#ebedf0', '#c0ddf9', '#73b3f3', '#3886e1', '#17459e'] // 默认是Github的绿色
            })            
        </script>
    </div>
</div>
</center>
</body>

相比于前面的图表,Github热力图相对复杂一些,但总体流程还是比较好理解的。不得不说Frappe Charts确实是一个很方便的前端图表库,基本可以满足所有需求。此外Frappe Charts结合JS还可以进行动态交互、数据编辑、数据输出等操作,功能十分强大,如果有更多需求建议详细阅读文档。

3.应用实例

基于这个JS图表库,实现了网页浏览量的可视化折线趋势图,点击查看 和前面的Demo相比,这个应用就比较实际了。涉及到数据的获取、处理以及绘制。具体而言,在打开网页的时候由JS基于WebSocket发送一个请求给服务器,服务器接受到请求以后,读取由这篇博客生成的访问历史记录文件,返回近期一段时间的访问数据。网页端解析接受到的数据并最终交由Frappe Charts绘图。关于WebSocket与服务器通信,可以参考之前的这篇博客这篇博客。为了减小服务器压力,不用每次访问都请求数据,也使用了Cookies。将接收的数据保存到Cookies中,如果没有过期(如一天)则直接读取,否则再下载。关于Cookies的使用可以参考这篇博客

为了方便大家学习以及记录,贴出所有源码,但请不要用它来做坏事或攻击我的服务器哦。网页端如下:

<script src="https://unpkg.com/frappe-charts@1.2.4/dist/frappe-charts.min.iife.js"></script>
<center>
<div style="border-width: 1px;border-radius: 5px; border-color:#ddd;border-style:solid;margin-bottom: 1.4em; width:100%; height:auto;">
    <div id="chart">
        <script>
        function getCookie(cname){
        var name = cname + "=";
        var ca = document.cookie.split(';');
        for(var i=0; i<ca.length; i++) {
            var c = ca[i].trim();
            if (c.indexOf(name)==0) { return c.substring(name.length,c.length); }
        }
        return "";
        }

        function setCookie(cname,cvalue,exdays){
        var d = new Date();
        d.setTime(d.getTime()+(exdays*24*60*60*1000));
        var expires = "expires="+d.toGMTString();
        document.cookie = cname+"="+cvalue+"; "+expires;
        }

        function parseData(received_msg){
            date = received_msg.split("-")[0];
            uv = received_msg.split("-")[1];
            pv = received_msg.split("-")[2];
            dates = date.split(",");
            tmp_uv = uv.split(",");
            tmp_pv = pv.split(",");
            uvs = new Array();
            pvs = new Array();
            for(var i=0;i<tmp_uv.length;i++){
                uvs[i]=parseInt(tmp_uv[i]);
                pvs[i]=parseInt(tmp_pv[i]);
            }
            return [dates,uvs,pvs];
        }

        function saveAndDraw() {
        var requestInfo = "histdata";

        var ws = new WebSocket("ws://178.128.102.152:1086");

        ws.onopen = function () {
        ws.send(requestInfo);
        };

        ws.onmessage = function (evt) {
        var received_msg = evt.data;
        ws.close();

        res = parseData(received_msg);
        drawPlot(res[0],res[1],res[2]);

        setCookie("hist_data",received_msg,1);
        };

        ws.onclose = function () {
        };
        }

        function drawPlot(dates,uvs,pvs){
            const data = {
            labels: dates,
            datasets: [
            {
            name: "Daily PV", type: "line",
            values: pvs
            },
            {
            name: "Daily UV", type: "line",
            values: uvs
            }
            ]
            }

            const chart = new frappe.Chart("#chart", {
            title: "UV & PV Trends",
            data: data,
            type: 'line',
            height: 350,
            colors: ['#7cd6fd', '#743ee2'],
            lineOptions:{regionFill: 1}
            })
        }

        function mainFunction(){
            var content = getCookie("hist_data");
            if(content == ""){
                console.info("no history data,download");
                saveAndDraw();
            }else{
                console.info("have history data,load");
                res = parseData(content);
                drawPlot(res[0],res[1],res[2]);
            }
        }

        mainFunction();
        </script>
    </div>
</div>
</center>

服务器端如下。

# coding=utf-8
import urllib
import os
import time
import random
from websocket_server import WebsocketServer
import sys
import logging
import chardet

# 因为考虑到传入的字符串有非英文字符,
# 所以手动设置编码,否则可能会报编码错误
reload(sys)
sys.setdefaultencoding('utf-8')


def readFile(file_path,index_range=7):
    items = []
    f = open(file_path, 'r')
    content = f.readlines()
    hist_len = len(content)
    part_items = content[-(index_range+1):]
    f.close()
    return part_items

def parseData(items):
    date_str = ""
    daily_uv_str = ""
    daily_pv_str = ""
    for i in range(1,len(items)):
        cur_item = items[i].strip()
        cur_date = cur_item.split("\t")[0]
        cur_date = cur_date[cur_date.find(".")+1:]
        cur_uv = int(cur_item.split("\t")[1].split(":")[1])
        cur_pv = int(cur_item.split("\t")[2].split(":")[1])
        
        lst_item = items[i-1].strip()
        lst_date = lst_item.split("\t")[0]
        lst_date = lst_date[lst_date.find(".")+1:]
        lst_uv = int(lst_item.split("\t")[1].split(":")[1])
        lst_pv = int(lst_item.split("\t")[2].split(":")[1])

        d_uv = str(cur_uv - lst_uv)
        d_pv = str(cur_pv - lst_pv)

        if(i==1):
            date_str = cur_date
            daily_uv_str = d_uv
            daily_pv_str = d_pv
        else:
            date_str = date_str + "," + cur_date
            daily_uv_str = daily_uv_str + "," + d_uv
            daily_pv_str = daily_pv_str + "," + d_pv
    final_str = date_str + "-" + daily_uv_str + "-" + daily_pv_str
    return final_str


def select(urls):
    index = random.randint(0, len(urls) - 1)
    return urls[index]


def new_client(client, server):
    # print "Time:", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    print("Client(%d) has joined." % client['id'])


def client_left(client, server):
    print("Client(%d) disconnected\n" % client['id'])


def message_back(client, server, message):
    # 这里的message参数就是客户端传进来的内容
    # print("Client(%d) said: %s" % (client['id'], message))
    print("Client(%d) connected" % client['id'])
    # 这里可以对message进行各种处理
    result = handle_login(message)
    # 将处理后的数据再返回给客户端
    server.send_message(client, result)


def handle_login(text):
    # 根据传入的参数处理
    if text == "histdata":
        items = readFile("history.txt", index_range=8)
        final_str = parseData(items)
        # 控制台输出有时会出现错误,所以不输出也可以
        # print "Return:", url
        return final_str
    else:
        print("error param")
        return "error"


if __name__ == '__main__':
    server = WebsocketServer(1086, host='', loglevel=logging.INFO)
    server.set_fn_new_client(new_client)
    server.set_fn_client_left(client_left)
    server.set_fn_message_received(message_back)
    server.run_forever()

本文作者原创,未经许可不得转载,谢谢配合

返回顶部