前两天无意之间在网上看到了一个叫做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
:填充折线以下部分,默认为0hideDots
:隐藏数据点,只显示折线,默认为0hideLine
:隐藏折线,只显示散点,默认为0heatline
:热力线,位置越高颜色越深,默认为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()
本文作者原创,未经许可不得转载,谢谢配合