[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)

欢迎大家来到“Python从零到壹”,在这里我将分享约200篇Python系列文章,带大家一起去学习和玩耍,看看Python这个有趣的世界。所有文章都将结合案例、代码和作者的经验讲解,真心想把自己近十年的编程经验分享给大家,希望对您有所帮助,文章中不足之处也请海涵。Python系列整体框架包括基础语法10篇、网络爬虫30篇、可视化分析10篇、机器学习20篇、大数据分析20篇、图像识别30篇、人工智能40篇、Python安全20篇、其他技巧10篇。您的关注、点赞和转发就是对秀璋最大的支持,知识无价人有情,希望我们都能在人生路上开心快乐、共同成长。

本文属于番外篇,主要介绍Python可视化分析,利用Python采集每年发表博客的数据,再利用D3实现类似于Github的每日贡献统计,显示效果如下图所示,也是回答读者之前的疑惑。这十多年在CSDN坚持分享,也是一笔宝贵的财务啊!希望文章对您有所帮助,如果有不足之处,还请海涵。

文章目录

  • 一.博客数据采集
    • 1.数据采集
    • 2.翻页采集数据
    • 3.数据按年份存储
    • 4.完整代码
  • 二.D3可视化分析
  • 三.CSDN近十年内容分析
  • 四.总结

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第1张图片

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第2张图片

该部分代码在作者Github的番外篇中可以下载,下载地址如下:

  • https://github.com/eastmountyxz/Python-zero2one
  • 开源600多页电子书:https://github.com/eastmountyxz/HWCloudImageRecognition

本文参考zjw666作者的D3代码,在此感谢,其地址如下:

  • https://github.com/d3
  • https://github.com/zjw666/D3_demo

前文赏析:(尽管该部分占大量篇幅,但我舍不得删除,哈哈!)

第一部分 基础语法

  • [Python从零到壹] 一.为什么我们要学Python及基础语法详解
  • [Python从零到壹] 二.语法基础之条件语句、循环语句和函数
  • [Python从零到壹] 三.语法基础之文件操作、CSV文件读写及面向对象

第二部分 网络爬虫

  • [Python从零到壹] 四.网络爬虫之入门基础及正则表达式抓取博客案例
  • [Python从零到壹] 五.网络爬虫之BeautifulSoup基础语法万字详解
  • [Python从零到壹] 六.网络爬虫之BeautifulSoup爬取豆瓣TOP250电影详解
  • [Python从零到壹] 七.网络爬虫之Requests爬取豆瓣电影TOP250及CSV存储
  • [Python从零到壹] 八.数据库之MySQL基础知识及操作万字详解
  • [Python从零到壹] 九.网络爬虫之Selenium基础技术万字详解(定位元素、常用方法、键盘鼠标操作)
  • [Python从零到壹] 十.网络爬虫之Selenium爬取在线百科知识万字详解(NLP语料构造必备技能)

第三部分 数据分析和机器学习

  • [Python从零到壹] 十一.数据分析之Numpy、Pandas、Matplotlib和Sklearn入门知识万字详解(1)
  • [Python从零到壹] 十二.机器学习之回归分析万字总结全网首发(线性回归、多项式回归、逻辑回归)
  • [Python从零到壹] 十三.机器学习之聚类分析万字总结全网首发(K-Means、BIRCH、层次聚类、树状聚类)
  • [Python从零到壹] 十四.机器学习之分类算法三万字总结全网首发(决策树、KNN、SVM、分类算法对比)
  • [Python从零到壹] 十五.文本挖掘之数据预处理、Jieba工具和文本聚类万字详解
  • [Python从零到壹] 十六.文本挖掘之词云热点与LDA主题分布分析万字详解
  • [Python从零到壹] 十七.可视化分析之Matplotlib、Pandas、Echarts入门万字详解
  • [Python从零到壹] 十八.可视化分析之Basemap地图包入门详解
  • [Python从零到壹] 十九.可视化分析之热力图和箱图绘制及应用详解
  • [Python从零到壹] 二十.可视化分析之Seaborn绘图万字详解
  • [Python从零到壹] 二十一.可视化分析之Pyechart绘图万字详解
  • [Python从零到壹] 二十二.可视化分析之OpenGL绘图万字详解
  • [Python从零到壹] 二十三.十大机器学习算法之决策树分类分析详解(1)
  • [Python从零到壹] 二十四.十大机器学习算法之KMeans聚类分析详解(2)
  • [Python从零到壹] 二十五.十大机器学习算法之KNN算法及图像分类详解(3)
  • [Python从零到壹] 二十六.十大机器学习算法之朴素贝叶斯算法及文本分类详解(4)
  • [Python从零到壹] 二十七.十大机器学习算法之线性回归算法分析详解(5)
  • [Python从零到壹] 二十八.十大机器学习算法之SVM算法分析详解(6)
  • [Python从零到壹] 二十九.十大机器学习算法之随机森林算法分析详解(7)
  • [Python从零到壹] 三十.十大机器学习算法之逻辑回归算法及恶意请求检测应用详解(8)
  • [Python从零到壹] 三十一.十大机器学习算法之Boosting和AdaBoost应用详解(9)
  • [Python从零到壹] 三十二.十大机器学习算法之层次聚类和树状图聚类应用详解(10)

第四部分 Python图像处理基础

  • [Python从零到壹] 三十三.图像处理基础篇之什么是图像处理和OpenCV配置
  • [Python从零到壹] 三十四.OpenCV入门详解——显示读取修改及保存图像
  • [Python从零到壹] 三十五.图像处理基础篇之OpenCV绘制各类几何图形
  • [Python从零到壹] 三十六.图像处理基础篇之图像算术与逻辑运算详解
  • [Python从零到壹] 三十七.图像处理基础篇之图像融合处理和ROI区域绘制
  • [Python从零到壹] 三十八.图像处理基础篇之图像几何变换(平移缩放旋转)
  • [Python从零到壹] 三十九.图像处理基础篇之图像几何变换(镜像仿射透视)
  • [Python从零到壹] 四十.图像处理基础篇之图像量化处理
  • [Python从零到壹] 四十一.图像处理基础篇之图像采样处理
  • [Python从零到壹] 四十二.图像处理基础篇之图像金字塔向上取样和向下取样

第五部分 Python图像运算和图像增强

  • [Python从零到壹] 四十三.图像增强及运算篇之图像点运算和图像灰度化处理
  • [Python从零到壹] 四十四.图像增强及运算篇之图像灰度线性变换详解
  • [Python从零到壹] 四十五.图像增强及运算篇之图像灰度非线性变换详解
  • [Python从零到壹] 四十六.图像增强及运算篇之图像阈值化处理
  • [Python从零到壹] 四十七.图像增强及运算篇之腐蚀和膨胀详解
  • [Python从零到壹] 四十八.图像增强及运算篇之形态学开运算、闭运算和梯度运算
  • [Python从零到壹] 四十九.图像增强及运算篇之顶帽运算和底帽运算
  • [Python从零到壹] 五十.图像增强及运算篇之图像直方图理论知识和绘制实现
  • [Python从零到壹] 五十一.图像增强及运算篇之图像灰度直方图对比分析万字详解
  • [Python从零到壹] 五十二.图像增强及运算篇之图像掩膜直方图和HS直方图
  • [Python从零到壹] 五十三.图像增强及运算篇之直方图均衡化处理
  • [Python从零到壹] 五十四.图像增强及运算篇之局部直方图均衡化和自动色彩均衡化处理
  • [Python从零到壹] 五十五.图像增强及运算篇之图像平滑(均值滤波、方框滤波、高斯滤波)
  • [Python从零到壹] 五十六.图像增强及运算篇之图像平滑(中值滤波、双边滤波)
  • [Python从零到壹] 五十七.图像增强及运算篇之图像锐化Roberts、Prewitt算子实现边缘检测
  • [Python从零到壹] 五十八.图像增强及运算篇之图像锐化Sobel、Laplacian算子实现边缘检测
  • [Python从零到壹] 五十九.图像增强及运算篇之图像锐化Scharr、Canny、LOG实现边缘检测

第六部分 Python图像识别和图像高阶案例

  • [Python从零到壹] 六十.图像识别及经典案例篇之基于阈值及边缘检测的图像分割
  • [Python从零到壹] 六十一.图像识别及经典案例篇之基于纹理背景和聚类算法的图像分割
  • [Python从零到壹] 六十二.图像识别及经典案例篇之基于均值漂移算法和分水岭算法的图像分割
  • [Python从零到壹] 六十三.图像识别及经典案例篇之图像漫水填充分割应用

第七部分 NLP与文本挖掘

第八部分 人工智能入门知识

第九部分 网络攻防与AI安全

第十部分 知识图谱构建实战

扩展部分 人工智能高级案例

  • [Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)

作者新开的“娜璋AI安全之家”将专注于Python和安全技术,主要分享Web渗透、系统安全、人工智能、大数据分析、图像识别、恶意代码检测、CVE复现、威胁情报分析等文章。虽然作者是一名技术小白,但会保证每一篇文章都会很用心地撰写,希望这些基础性文章对你有所帮助,在Python和安全路上与大家一起进步。


一.博客数据采集

首先介绍博客数据采集方法,为保护CSDN原创,这里仅以方法为主。建议读者结合自己的方向去抓取文本知识。

1.数据采集

核心扩展包:

  • import requests
  • from lxml import etree

核心流程:

  • 解决headers问题
  • 解决翻页问题
  • 审查元素分析DOM树结构
  • 定位节点采用Xpath分析
  • 分别采集标题、URL、时间、阅读和评论数量

审查元素如下图所示:

对应HTML代码如下:

<div class="article-list">
    <div class="article-item-box csdn-tracking-statistics">
      <h4 class="">
        <a href="https://blog.csdn.net/Eastmount/article/details/124587047">
            <span class="article-type type-1 float-none">原创span>
          [Python人工智能] 三十六.基于Transformer的商品评论情感分析 (2)keras构建多头自注意力(Transformer)模型
        a> 
      h4>
      <p class="content">
        本专栏开始,作者正式研究Python深度学习、神经网络及人工智能相关知识。
        前一篇文章利用Keras构建深度学习模型并实现了情感分析。这篇文章将介绍
        Transformer基础知识,并通过Keras构建多头自注意力(Transformer)模型,
        实现IMDB电影影评数据情感分析。基础性文章,希望对您有所帮助!...
      p>
      <div class="info-box d-flex align-content-center">
        <p>
          <span class="date">2022-08-28 18:35:44span>
          <span class="read-num"><img src="" alt="">2852span>
          <span class="read-num"><img src="" alt="">4span>
        p>

博客均是按照如下div进行布局。

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第3张图片

该部分的关键代码如下:

# coding:utf-8
# By:Eastmount
import requests
from lxml import etree

#设置浏览器代理,它是一个字典
headers = {
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
        AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}
num = 2
url = 'https://xxxx/list/' + str(num)

#向服务器发出请求
r = requests.get(url = url, headers = headers).text

#解析DOM树结构
count = 0
html_etree = etree.HTML(r)
div = html_etree.xpath('//*[@class="article-list"]/div')
for item in div:
    #标题
    value = item.xpath('./h4/a/text()')
    title = value[1].strip()
    count += 1
    print(title)
    #时间
    blog_time = item.xpath('./div/p/span[1]/text()')
    print(blog_time)

print("博客总数:", count)

输出结果如下图所示:

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第4张图片

如果需要进一步提取时间,则修改的代码如下所示:

#向服务器发出请求
r = requests.get(url = url, headers = headers).text

#解析DOM树结构
count = 0
title_list = []
time_list = []
year_list = []
html_etree = etree.HTML(r)
div = html_etree.xpath('//*[@class="article-list"]/div')
for item in div:
    #标题
    value = item.xpath('./h4/a/text()')
    title = value[1].strip()
    count += 1
    print(title)
    title_list.append(title)
    
    #日期和时间
    data = item.xpath('./div/p/span[1]/text()')
    data = str(data[0])
    #print(data)

    #提取日期
    year = data.split("-")[0]
    month = data.split("-")[1]
    day = data.split("-")[2].split(" ")[0]
    blog_time = str(year) + "-" + month + "-" + day
    print(blog_time, year)
    time_list.append(blog_time)
    year_list.append(year)
print("博客总数:", count)

输出结果如下图所示,每页共计40篇博客。

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第5张图片


2.翻页采集数据

博客翻页主要通过循环实现,下图展示了翻页的网址。

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第6张图片

此时增加翻页后的代码如下所示:

# coding:utf-8
# By:Eastmount
import requests
from lxml import etree

#设置浏览器代理,它是一个字典
headers = {
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
        AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}

num = 1
count = 0
title_list = []
time_list = []
year_list = []
while num<=18:
    url = 'https://xxx/list/' + str(num)

    #向服务器发出请求
    r = requests.get(url = url, headers = headers).text

    #解析DOM树结构
    html_etree = etree.HTML(r)
    div = html_etree.xpath('//*[@class="article-list"]/div')
    for item in div:
        #标题
        value = item.xpath('./h4/a/text()')
        title = value[1].strip()
        count += 1
        print(title)
        title_list.append(title)
        
        #日期和时间
        data = item.xpath('./div/p/span[1]/text()')
        data = str(data[0])

        #提取日期
        year = data.split("-")[0]
        month = data.split("-")[1]
        day = data.split("-")[2].split(" ")[0]
        blog_time = str(year) + "-" + month + "-" + day
        print(blog_time, year)
        time_list.append(blog_time)
        year_list.append(year)
    num += 1
print("博客总数:", count)
print(len(title_list), len(time_list), len(year_list))

输出结果如下图所示,作者711篇博客均采集完成。

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第7张图片


3.数据按年份存储

将所有博客时间存储至列表中,再按年份统计每天的发表次数,最终存储至Json文件,按照如下格式。

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第8张图片

第一步,生成一年的所有日期,以2021年为例。

import arrow
 
#判断闰年并获取一年的总天数
def isLeapYear(years):
    #断言:年份不为整数时抛出异常
    assert isinstance(years, int), "请输入整数年,如 2018"
    #判断是否是闰年
    if ((years % 4 == 0 and years % 100 != 0) or (years % 400 == 0)):  
        days_sum = 366
        return days_sum
    else:
        days_sum = 365
        return days_sum
 
#获取一年的所有日期
def getAllDayPerYear(years):
    start_date = '%s-1-1' % years
    a = 0
    all_date_list = []
    days_sum = isLeapYear(int(years))
    print()
    while a < days_sum:
        b = arrow.get(start_date).shift(days=a).format("YYYY-MM-DD")
        a += 1
        all_date_list.append(b)
    return all_date_list

all_date_list = getAllDayPerYear("2021")
print(all_date_list)

输出结果如下所示:

['2021-01-01', '2021-01-02', '2021-01-03', ...,  '2021-12-29', '2021-12-30', '2021-12-31']

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第9张图片

第二步,遍历一年所有日期发表博客的数量。

#--------------------------------------------------------------------------
#遍历一年所有日期发表博客的数量
k = 0
res = {}
while k<len(all_date_list):
    tt = all_date_list[k]
    res[tt] = 0
    m = 0
    while m<len(time_list):
        tt_lin = time_list[m]
        if tt==tt_lin:
            res[tt] += 1
        m += 1
    k += 1
print("\n------------------------统计结束-------------------------\n")
print(res)
#统计发表博客数
count_blog = 0
for i in res.keys():
    if res[i] > 0:
        count_blog += res[i]
print(count_blog)

输出结果可以看到2021年作者发表博客106篇,并且形成该年的每日发表博客数。

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第10张图片

第三步,存储至Json文件。

#--------------------------------------------------------------------------
#存储至Json文件
import json
print(type(res), len(res))
with open("data-2021.json", "w", encoding='utf-8') as f:
    json.dump(res, f)
    print("文件写入....")

最终生成结果如下图所示:

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第11张图片

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第12张图片


4.完整代码

完整代码如下所示,修改近十年的年份,将生成不同的博客数量。

  • 2022年:61篇
  • 2021年:106篇
  • 2020年:132篇
  • 2019年:87篇
  • 2018年:53篇
  • 2017年:35篇
  • 2016年:48篇
  • 2015年:92篇
  • 2014年:57篇
  • 2013年:34篇
# coding:utf-8
# By:Eastmount
import requests
from lxml import etree

#--------------------------------------------------------------------------
#采集CSDN博客数据
headers = {
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
        AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
}

num = 1
count = 0
title_list = []
time_list = []
year_list = []
while num<=18:
    url = 'https://xxx/list/' + str(num)

    #向服务器发出请求
    r = requests.get(url = url, headers = headers).text

    #解析DOM树结构
    html_etree = etree.HTML(r)
    div = html_etree.xpath('//*[@class="article-list"]/div')
    for item in div:
        #标题
        value = item.xpath('./h4/a/text()')
        title = value[1].strip()
        count += 1
        #print(title)
        title_list.append(title)
        
        #日期和时间
        data = item.xpath('./div/p/span[1]/text()')
        data = str(data[0])

        #提取日期
        year = data.split("-")[0]
        month = data.split("-")[1]
        day = data.split("-")[2].split(" ")[0]
        blog_time = str(year) + "-" + month + "-" + day
        #print(blog_time, year)
        time_list.append(blog_time)
        year_list.append(year)
    num += 1
print("博客总数:", count)
print(len(title_list), len(time_list), len(year_list))

#--------------------------------------------------------------------------
#生成一年所有日期
import arrow
 
#判断闰年并获取一年的总天数
def isLeapYear(years):
    #断言:年份不为整数时抛出异常
    assert isinstance(years, int), "请输入整数年,如 2018"
    #判断是否是闰年
    if ((years % 4 == 0 and years % 100 != 0) or (years % 400 == 0)):  
        days_sum = 366
        return days_sum
    else:
        days_sum = 365
        return days_sum
 
#获取一年的所有日期
def getAllDayPerYear(years):
    start_date = '%s-1-1' % years
    a = 0
    all_date_list = []
    days_sum = isLeapYear(int(years))
    print()
    while a < days_sum:
        b = arrow.get(start_date).shift(days=a).format("YYYY-MM-DD")
        a += 1
        all_date_list.append(b)
    return all_date_list

all_date_list = getAllDayPerYear("2021")
print(all_date_list)

#--------------------------------------------------------------------------
#遍历一年所有日期发表博客的数量
k = 0
res = {}
while k<len(all_date_list):
    tt = all_date_list[k]
    res[tt] = 0
    m = 0
    while m<len(time_list):
        tt_lin = time_list[m]
        if tt==tt_lin:
            res[tt] += 1
        m += 1
    k += 1
print("\n------------------------统计结束-------------------------\n")
print(res)
#统计发表博客数
count_blog = 0
for i in res.keys():
    if res[i] > 0:
        count_blog += res[i]
print(count_blog)

#--------------------------------------------------------------------------
#存储至Json文件
import json
print(type(res), len(res))
with open("data-2021.json", "w", encoding='utf-8') as f:
    json.dump(res, f)
    print("文件写入....")

二.D3可视化分析

可视化主要通过D3实现,推荐读者学习D3可视化知识。本文参考了zjw666作者的代码。

  • https://github.com/d3
  • https://github.com/zjw666/D3_demo

读者可以在我的github下载本文的所有代码及数据。

  • https://github.com/eastmountyxz/Python-zero2one

该项目代码框架如下:

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第13张图片

部分的关键代码如下:

index.html

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>calendartitle>
    <link rel="stylesheet" type="text/css" href="./index.css" />
head>
<body>
<script src="./lib/d3.js">script>
<script src="./index.js" type='module'>script>
body>
html>

index.css

.box{
    margin: 10% auto;
    width: 1500px;
    height: 400px;
    background-color: #fff;
}

.title{
    font-size: 20px;
}

.text{
    font-size: 10px;
}

.label{
    font-size: 12px;
}

chart-headmap.js

export default class Chart {
    constructor(){
        this._width = 1300;
        this._height = 400;
        this._margins = {top:30, left:30, right:30, bottom:30};
        this._data = [];
        this._scaleX = null;
        this._scaleY = null;
        this._colors = d3.scaleOrdinal(d3.schemeCategory10);
        this._box = null;
        this._svg = null;
        this._body = null;
        this._padding = {top:10, left:10, right:10, bottom:10};
    }

    width(w){
        if (arguments.length === 0) return this._width;
        this._width = w;
        return this;
    }

    height(h){
        if (arguments.length === 0) return this._height;
        this._height = h;
        return this;
    }

    margins(m){
        if (arguments.length === 0) return this._margins;
        this._margins = m;
        return this;
    }

    data(d){
        if (arguments.length === 0) return this._data;
        this._data = d;
        return this;
    }

    scaleX(x){
        if (arguments.length === 0) return this._scaleX;
        this._scaleX = x;
        return this;
    }

    scaleY(y){
        if (arguments.length === 0) return this._scaleY;
        this._scaleY = y;
        return this;
    }

    svg(s){
        if (arguments.length === 0) return this._svg;
        this._svg = s;
        return this;
    }

    body(b){
        if (arguments.length === 0) return this._body;
        this._body = b;
        return this;
    }

    box(b){
        if (arguments.length === 0) return this._box;
        this._box = b;
        return this;
    }

    getBodyWidth(){
        let width = this._width - this._margins.left - this._margins.right;
        return width > 0 ? width : 0;
    }

    getBodyHeight(){
        let height = this._height - this._margins.top - this._margins.bottom;
        return height > 0 ? height : 0;
    }

    padding(p){
        if (arguments.length === 0) return this._padding;
        this._padding = p;
        return this;
    }

    defineBodyClip(){

        this._svg.append('defs')
                 .append('clipPath')
                 .attr('id', 'clip')
                 .append('rect')
                 .attr('width', this.getBodyWidth() + this._padding.left + this._padding.right)
                 .attr('height', this.getBodyHeight() + this._padding.top  + this._padding.bottom)
                 .attr('x', -this._padding.left)
                 .attr('y', -this._padding.top);
    }

    render(){
        return this;
    }

    bodyX(){
        return this._margins.left;

    }

    bodyY(){
        return this._margins.top;
    }

    renderBody(){
        if (!this._body){
            this._body = this._svg.append('g')
                            .attr('class', 'body')
                            .attr('transform', 'translate(' + this.bodyX() + ',' + this.bodyY() + ')')
                            .attr('clip-path', "url(#clip)");
        }

        this.render();
    }

    renderChart(){
        if (!this._box){
            this._box = d3.select('body')
                            .append('div')
                            .attr('class','box');
        }

        if (!this._svg){
            this._svg = this._box.append('svg')
                            .attr('width', this._width)
                            .attr('height', this._height);
        }

        this.defineBodyClip();

        this.renderBody();
    }
}

最重要的代码是index.js,如下所示:

index.js

import Chart from "./chart-headmap.js";

d3.json('./data/data-2021.json').then(function(data){

    /* ----------------------------配置参数------------------------  */
    const chart = new Chart();
    const config = {
        margins: {top: 80, left: 50, bottom: 50, right: 50},
        textColor: 'black',
        title: '2021年博客创作热力图(106篇)',
        hoverColor: 'red',
        startTime: '2021-01-01',
        endTime: '2021-12-31',
        cellWidth: 20,
        cellHeight: 20,
        cellPadding: 1,
        cellColor1: '#F5F5F5',
        cellColor2: 'green',
        lineColor: 'yellow',
        lineWidth: 2
    }

    chart.margins(config.margins);

    /* ----------------------------初始化常量------------------------  */
    const startTime = new Date(config.startTime);
    const endTime = new Date(config.endTime);
    const widthOffset = config.cellWidth + config.cellPadding;
    const heightOffset = config.cellHeight + config.cellPadding;

    /* ----------------------------颜色转换------------------------  */
    chart.scaleColor = d3.scaleLinear()
                            .domain([0, d3.max(Object.values(data))])
                            .range([config.cellColor1, config.cellColor2]);

    /* ----------------------------渲染矩形------------------------  */
    chart.renderRect = function(){
        let currentYear, currentMonth;
        let yearGroup, monthGroup;
        const initDay = startTime.getDay();
        let currentDay = initDay;
        const totalDays = getTotalDays(startTime, endTime) + initDay;	
	console.info(totalDays);

        const mainBody = chart.body()
                                .append('g')
                                .attr('class', 'date')
                                .attr('transform', 'translate(' + 35 + ',' + 50 + ')')

        while(currentDay <= totalDays){
            let currentDate = getDate(startTime, currentDay).split('-');

            if(!currentYear || currentDate[0] !== currentYear){
                currentYear = currentDate[0];

                yearGroup = mainBody
                                .append('g')
                                .attr('class', 'year ' + currentYear);
            }

            if (!currentMonth || currentDate[1] !== currentMonth){
                currentMonth = currentDate[1];

                monthGroup = yearGroup.append('g').attr('class', 'month ' + currentMonth);
            }

            monthGroup
                 .append('g')
                 .attr('class', 'g ' + currentDate.join('-'))
                 .datum(currentDate.join('-'))
                 .append('rect')
                 .attr('width', config.cellWidth)
                 .attr('height', config.cellHeight)
                 .attr('x', Math.floor(currentDay / 7) * widthOffset)
                 .attr('y', currentDay % 7 * heightOffset);

            currentDay++;
        }

        d3.selectAll('.g')
            .each(function(d){
                d3.select(this)
                    .attr('fill', chart.scaleColor(data[d] || 0))
                    .datum({time: d, value: data[d] || 0});
            });

        function getTotalDays(startTime, endTime){
            return Math.floor((endTime.getTime() - startTime.getTime()) / 86400000);
        }

        function getDate(startTime, day){
            const date =  new Date(startTime.getTime() + 86400000 * (day - initDay));
            return d3.timeFormat("%Y-%m-%d")(date);
        }
    }

    /* ----------------------------渲染分隔线------------------------  */
    chart.renderLine = function(){
        const initDay = startTime.getDay();
        const days = [initDay-1];
        const linePaths = getLinePath();

        d3.select('.date')
                .append('g')
                .attr('class', 'lines')
                .selectAll('path')
                .data(linePaths)
                .enter()
                .append('path')
                .attr('stroke', config.lineColor)
                .attr('stroke-width', config.lineWidth)
                .attr('fill', 'none')
                .attr('d', (d) => d);

        function getLinePath(){
            const paths = [];

            d3.selectAll('.month')
                .each(function(d,i){
                    days[i+1] = days[i] + this.childNodes.length;
                });

            days.forEach((day,i) => {
                let path = 'M';
                let weekDay = day < 0 ? 6 : day % 7;

                if (weekDay !== 6) {
                    path += Math.floor(day / 7) * widthOffset + ' ' + 7 * heightOffset;
                    path +=  ' l' + '0' + ' ' + (weekDay - 6) * heightOffset;
                    path += ' l' + widthOffset + ' ' + '0';
                    path += ' l' + '0' + ' ' + (-weekDay - 1) * heightOffset;
                } else {
                    path += (Math.floor(day / 7) + 1) * widthOffset + ' ' + 7 * heightOffset;
                    path +=  ' l' + '0' + ' ' + (-7) * heightOffset;
                }

                paths.push(path);
            });

            return paths;
        }

    }

    /* ----------------------------渲染文本标签------------------------ */
    chart.renderText = function(){
        let week = ['Sun', 'Mon', 'Tue', 'Wed', 'Tur', 'Fri', 'Sat'];

        d3.select('.year')
            .append('g')
            .attr('class', 'week')
            .selectAll('.label')
            .data(week)
            .enter()
            .append('text')
            .attr('class', 'label')
            .attr('x', -40)
            .attr('y', heightOffset/2)
            .attr('dy', (d,i) => i * heightOffset + 4)
            .text((d)=>d);

        let months = d3.timeMonth.range(new Date(startTime.getFullYear(), startTime.getMonth(), startTime.getDate()), new Date(endTime.getFullYear(), endTime.getMonth(), endTime.getDate()));

        months = months.map((d) => d3.timeFormat("%b")(d));

        d3.select('.year')
            .append('g')
            .attr('class', 'month-label')
            .selectAll('text')
            .data(months)
            .enter()
            .append('text')
            .attr('x', (d,i) => i*widthOffset*4.25 + widthOffset*2)
            .attr('y', -10)
            .text((d) => d)

    }

    /* ----------------------------渲染图标题------------------------  */
    chart.renderTitle = function(){

        chart.svg().append('text')
                .classed('title', true)
                .attr('x', chart.width()/2)
                .attr('y', 0)
                .attr('dy', '2em')
                .text(config.title)
                .attr('fill', config.textColor)
                .attr('text-anchor', 'middle')
                .attr('stroke', config.textColor);

    }

    /* ----------------------------绑定鼠标交互事件------------------------  */
    chart.addMouseOn = function(){
        //防抖函数
        function debounce(fn, time){
            let timeId = null;
            return function(){
                const context = this;
                const event = d3.event;
                timeId && clearTimeout(timeId)
                timeId = setTimeout(function(){
                    d3.event = event;
                    fn.apply(context, arguments);
                }, time);
            }
        }

        d3.selectAll('.g')
            .on('mouseenter', function(d){
                const e = d3.event;
                const position = d3.mouse(chart.svg().node());

                d3.select(e.target)
                    .attr('fill', config.hoverColor);

                chart.svg()
                    .append('text')
                    .classed('tip', true)
                    .attr('x', position[0]+5)
                    .attr('y', position[1])
                    .attr('fill', config.textColor)
                    .text(d.time);
            })
            .on('mouseleave', function(d){
                const e = d3.event;

                d3.select(e.target)
                    .attr('fill', chart.scaleColor(d.value));

                d3.select('.tip').remove();
            })
            .on('mousemove', debounce(function(){
                    const position = d3.mouse(chart.svg().node());
                    d3.select('.tip')
                    .attr('x', position[0]+5)
                    .attr('y', position[1]-5);
                }, 6)
            );
    }

    chart.render = function(){

        chart.renderTitle();

        chart.renderRect();

        chart.renderLine();

        chart.renderText();

        chart.addMouseOn();

    }

    chart.renderChart();
});

由于该网页需要通过HTTP访问,因此需要搭建服务器访问。否则可能报错:

Access to script at 'file:///C:/Users/xiuzhang/Desktop/headmap_date/index.js

VS Code在扩展Extensions中搜索Live Server,选择并安装它。

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第14张图片

打开网站文件夹,选择具体的页面,右击文件选择 Open with Live Server。

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第15张图片

服务器就启动了,所选择浏览的页面也会在浏览器打开

  • http://127.0.0.1:5500/index.html

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第16张图片

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第17张图片


三.CSDN近十年内容分析

最后按年爬取数据并分别显示即可,修改关键代码为年份。

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第18张图片

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第19张图片

最终显示结果如下图所示:

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第20张图片


四.总结

写到这里,这篇文章就介绍结束。最后,希望这篇基础性文章对您有所帮助,如果文章中存在错误或不足之处,还请海涵~作为人工智能的菜鸟,我希望自己能不断进步并深入,后续将它应用于图像识别、网络安全、对抗样本等领域,指导大家撰写简单的学术论文,一起加油!

转眼就过年了,2022年简单总结:很多遗憾,很多不足,勉强算是分秒必争,只争朝夕,但愧对家人,陪伴太少,论文、科研、分享和家庭都做得不好,这一年勉强给个65分吧。最最感恩的永远是女神,回家的感觉真好,平平淡淡,温温馨馨,虽然这辈子科研、事业和职称上没有太大的追求,和大佬们的差距如鸿沟,但能做自己喜欢的事,爱自己喜欢的人,每天前进一小步(人生勿比),一家人健康幸福,足矣。提前祝大家新春快乐,阖家幸福。小珞珞是真的逗,陪伴的感觉真好,女神是真的好,爱你们喔,晚安娜继续加油!

[Python从零到壹] 番外篇之可视化利用D3库实现CSDN博客每日统计效果(类似github)_第21张图片

(By:Eastmount 2023-01-29 夜于贵阳 http://blog.csdn.net/eastmount/ )


你可能感兴趣的:(Python从零到壹,Python学习系列,Python网络爬虫,python,可视化分析,博客统计,D3,日历统计)