楔子
下面我們來進行高級圖表的繪制,說是高級圖表,其實也不算高級,主要是一些子圖的繪制。以下代碼在 jupyter notebook 上運行,先導入幾個模塊:
import datetime
import plotly.graph_objs as go
import numpy as np
import pandas as pd
時間序列
plotly 對時間的支持特別友好,即支持字符串格式、也支持日期格式,而且使用起來比較簡單。
x0 = [datetime.date(2018, 1, 1),
datetime.date(2018, 2, 1),
datetime.date(2018, 3, 1),
datetime.date(2018, 4, 1),
datetime.date(2018, 5, 1)]
x1 = ["2018-1-1", "2018-2-1", "2018-3-1", "2018-4-1", "2018-5-1"]
trace0 = go.Scatter(x=x0, y=[1, 2, 3, 4, 5], name="trace_date")
trace1 = go.Scatter(x=x0, y=[2, 3, 4, 5, 6], name="trace_string")
fig = go.Figure(data=[trace0, trace1])
fig
從代碼中可以看出,只要是日期、或者符合時間格式的字符串,plotly 就會自動識別為日期格式。
表格
還記得我們之前創建甘特圖嗎?我們用到了 plotly.figure_factory,創建表格也是如此。
import plotly.figure_factory as ff
data= [
["姓名", "年齡", "性別"],
["古明地覺", 17, "女"],
["古明地戀", 16, "女"],
["椎名真白", 18, "女"],
["坂上智代", 19, "女"],
["雨宮優子", 16, "女"],
]
fig = ff.create_table(data)
fig
接收一個嵌套的二維列表,里面的第一個列表會被識別為表頭,並且在里面還可以添加一些 html 屬性。
import plotly.figure_factory as ff
data= [
["姓名", "年齡", "性別"],
['<a href="http://www.baidu.com">古明地覺</a>', 17, "女"],
["古明地戀", 16, "女"],
["椎名真白", 18, "女"],
["坂上智代", 19, "女"],
["雨宮優子", 16, "女"],
]
fig = ff.create_table(data)
fig
點擊 古明地覺 即可跳轉到百度頁面,會在新標簽頁中打開百度頁面。
使用 pandas
除了二維列表,我們還可以傳遞一個 DataFrame。
import plotly.figure_factory as ff
from sqlalchemy import create_engine
engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
df = pd.read_sql("select * from t_case", engine)
# 直接將DataFrame傳進去即可
fig = ff.create_table(df)
fig
如果是 DataFrame,那么列名就是這里的表頭,但是我們看到索引貌似沒了,那么如何將索引也顯示在上面呢?
import plotly.figure_factory as ff
from sqlalchemy import create_engine
engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
df = pd.read_sql("select * from t_case", engine)
# 直接將DataFrame傳進去即可
fig = ff.create_table(df, index=True, index_title="自增數字")
fig
多圖表
多圖表其實在前面博客中已經說了,這里再簡單提一嘴,就是畫布上顯示多條軌跡而已。
x = list("abcde")
y0 = [1, 2, -2, 2, -3]
y1 = [1, 2, 3, 4, 5]
trace0 = go.Bar(x=x, y=y0, name="bar")
trace1 = go.Scatter(x=x, y=y1, name="line")
fig = go.Figure(data=[trace0, trace1])
fig
多個坐標軸
多坐標軸是最復雜的地方,那為什么會有多個坐標軸呢。首先我們上面的多個圖表是顯示在相同坐標軸上面的,但是如果兩個軌跡的數值相差比較大、還使用這種方法,那么 plotly 為了顯示所有的軌跡,就會把坐標軸往大的方向調整,從而導致數值小的軌跡不明顯。我們舉個栗子看一下就很直觀了:
x0 = np.linspace(1, 100, 5)
x1 = np.linspace(0, 1000, 5)
y0 = x0 ** 2 + x0 + 1
y1 = x1 ** 2 + x1 + 1
trace0 = go.Scatter(x=x0, y=y0)
trace1 = go.Scatter(x=x1, y=y1)
fig = go.Figure(data=[trace0, trace1])
fig
一條軌跡我們指定的數據是 0 到 1000,一條是 0 到 100。但是 plotly 為了展示所有的軌跡,坐標軸上刻度會按照軌跡的數值大的一方來顯示,因此這就造成了數值小的軌跡的局部細節展示不出。所以在軌跡的數值相差比較大的時候,我們需要繪制第二個坐標軸。
繪制第二個坐標軸非常簡單,只需要在 layout 中指定一個 xaxis2 和 yaxis2 即可,同理第三個坐標軸就是 xaxis3 和 xaxis3,以此類推。
x0 = np.linspace(1, 100, 5)
x1 = np.linspace(0, 1000, 5)
y0 = x0 ** 2 + x0 + 1
y1 = x1 ** 2 + x1 + 1
trace0 = go.Scatter(x=x0, y=y0)
# 我們雖然在 layout 中傳入 xaxis2 和 yaxis2 創建了第二坐標軸,但是還要指明到底是哪個軌跡使用
# 因此要在軌跡上顯式地通過 xaxis 和 yaxis 指明,到底是哪個軌跡使用哪個坐標軸
# 但是在指定的時候,第二坐標軸直接寫 x2、y2 即可。默認的坐標軸是第一坐標軸、也就是最原始的那個坐標軸
trace1 = go.Scatter(x=x1, y=y1, yaxis="y2", xaxis="x2")
fig = go.Figure(data=[trace0, trace1],
layout={
# 由於在同一個畫布上,所以網格會重疊,我們將其中的一個軸的網格給隱藏掉
# 以及zeroline,這些參數都是關於視覺上的,對於表達數據則無影響
# 具體如何影響圖表的模樣,可以自己去試一下
"xaxis": {"title": "trace0的x軸", "showgrid": False, "zeroline": False},
"yaxis": {"title": "trace0的y軸", "showgrid": False},
# 這里就是第二坐標軸的x軸了
"xaxis2": {"title": "trace1的x軸",
"side": "top", # 這個很重要,要設置方向,顯然設置為上方
# 必須要寫,否則圖像無法顯示,同理下面的yaxis也是類似
"overlaying": "x"},
"yaxis2": {"title": "trace1的y軸",
# 第二y軸要設置在右邊
"side": "right",
"overlaying": "y"}
}
)
fig
此時就繪制出來了,因為現在兩個軌跡的坐標不是同一個坐標了。但是它們之間的規律是一樣的,再加上又顯示在同一個畫布上,所以軌跡是差不多重合的。
繪制子圖(方式一)
說實話,共享坐標軸的方式展示多圖表還不是很常見,常見的是一個畫布分為多個區域,每個區域展示各自的圖表。就可以想象成之前的一張畫布顯示一個軌跡,然后把多個畫布放在一起顯示。這樣,它們各自的坐標軸、標題等屬性都是獨立的。
# 創建子圖使用make_subplots
from plotly.subplots import make_subplots
trace0 = go.Scatter(x=[1, 2, 3, 4, 5], y=[1, 2, 3, 4, 5])
trace1 = go.Scatter(x=[2, 3, 4, 5, 6], y=[2, 3, 4, 5, 6])
trace2 = go.Scatter(x=[1, 2, 3, 4, 5], y=[1, 2, 3, 4, 5])
trace3 = go.Scatter(x=[2, 3, 4, 5, 6], y=[2, 3, 4, 5, 6])
fig = make_subplots(rows=2, # 將畫布分為兩行
cols=2, # 將畫布分為兩列
subplot_titles=["trace0的標題",
"trace1的標題",
"trace3的標題",
"trace4的標題"], # 子圖的標題
x_title="x軸標題",
y_title="y軸標題"
)
# 添加軌跡
fig.append_trace(trace0, 1, 1) # 將trace0添加到第一行第一列的位置
fig.append_trace(trace1, 1, 2) # 將trace1添加到第一行第二列的位置
fig.append_trace(trace2, 2, 1) # 將trace2添加到第二行第一列的位置
fig.append_trace(trace3, 2, 2) # 將trace3添加到第二行第二列的位置
fig
此時我們就成功繪制出來了子圖,但是有一點不完美的地方是,它的這個坐標軸的標題是針對整體的。我可不可以對不同的坐標軸施加不同的描述呢?顯然是可以的。
但是我們這里需要再補充一下,我們之前創建畫布的時候是通過 go.Figure,然后將畫布參數通過字典的方式先寫好,然后傳給 layout。但是事實上我們也可以后續在進行添加的,比如 fig["layout"]["xaxis"].update({"title": "x坐標軸"})
,這樣做也是可以的。fig["data"] 拿到的就是所有的軌跡,我們也可以往里面加入新的軌跡,fig["layout"] 則是拿到的畫布的屬性,一個字典,我們同樣可以往里面添加、刪除屬性。
之所以說這些,就是因為 make_subplots 里面沒辦法直接傳遞 layout,只能通過創建出來的畫布、手動獲取里面的 layout、然后添加。
from plotly.subplots import make_subplots
trace0 = go.Scatter(x=[1, 2, 3, 4, 5], y=[1, 2, 3, 4, 5])
trace1 = go.Scatter(x=[2, 3, 4, 5, 6], y=[2, 3, 4, 5, 6])
trace2 = go.Scatter(x=[1, 2, 3, 4, 5], y=[1, 2, 3, 4, 5])
trace3 = go.Scatter(x=[2, 3, 4, 5, 6], y=[2, 3, 4, 5, 6])
fig = make_subplots(rows=2,
cols=2,
subplot_titles=["trace0的標題",
"trace1的標題",
"trace2的標題",
"trace3的標題"],
)
fig.append_trace(trace0, 1, 1)
fig.append_trace(trace1, 1, 2)
fig.append_trace(trace2, 2, 1)
fig.append_trace(trace3, 2, 2)
# 創建了四個子圖,自動就會有四個坐標軸。
# 每個軌跡占一個,因此這種情況我們是不需要在軌跡里面通過xaxis和yaxis來指定到底使用哪一個坐標軸的,因為已經分配好了
fig["layout"]["xaxis"].update({"title": "trace0的x軸", "titlefont": {"color": "red"}})
fig["layout"]["yaxis"].update({"title": "trace0的y軸", "titlefont": {"color": "red"}})
fig["layout"]["xaxis2"].update({"title": "trace1的x軸", "titlefont": {"color": "green"}})
fig["layout"]["yaxis2"].update({"title": "trace1的y軸", "titlefont": {"color": "green"}})
fig["layout"]["xaxis3"].update({"title": "trace2的x軸", "titlefont": {"color": "pink"}})
fig["layout"]["yaxis3"].update({"title": "trace2的y軸", "titlefont": {"color": "pink"}})
fig["layout"]["xaxis4"].update({"title": "trace3的x軸", "titlefont": {"color": "yellow"}})
fig["layout"]["yaxis4"].update({"title": "trace3的y軸", "titlefont": {"color": "yellow"}})
fig["layout"]["template"] = "plotly_dark"
fig
繪制子圖(方式二)
我們下面這種繪制子圖的方式有點類似於共享坐標軸的方式,主要在於內部的一個 domain 參數。
trace0 = go.Scatter(x=[1, 2, 3, 4, 5], y=[1, 2, 3, 4, 5])
trace1 = go.Scatter(x=[2, 3, 4, 5, 6], y=[2, 3, 4, 5, 6], xaxis="x2", yaxis="y2")
fig = go.Figure(data=[trace0, trace1],
layout={"xaxis": {"domain": [0, 0.6]}, # 指定第一幅圖的范圍是0到百分之60
"xaxis2": {"domain": [0.7, 1]}, # 第二幅圖的范圍是百分之70到百分之百
# 這個參數可能有些費解,它是指定y軸位置的,如果是通過"side": "right"的話,那么這個軸會在第一幅圖的右邊
# 通過"anchor": "x2",那么y軸就是像現在這樣出現在第二幅圖的左邊。
"yaxis2": {"anchor": "x2"}
}
)
fig
這種繪制子圖的方式可能不是容易讓人理解,主要就在於里面的 domain 參數。我們之前通過這種方式可以認為是兩張子圖(都占據全部位置)重合了,現在通過 domain 將兩幅子圖分開了。因此相比 make_subplots,這種繪制子圖的方式有點讓人費解,而且我們還要計算好位置。
但是這種繪制子圖的方式的好處就在於我們可以自定義子圖的位置,我們使用 make_subplots 的時候,多個子圖大小的是一樣的,並且間隔什么的是等分的,但是 domain 這種方式就不同了。
trace0 = go.Scatter(x=[1, 2, 3, 4, 5], y=[1, 2, 3, 4, 5])
trace1 = go.Scatter(x=[2, 3, 4, 5, 6], y=[2, 3, 4, 5, 6], xaxis="x2", yaxis="y2")
fig = go.Figure(data=[trace0, trace1],
layout={# xaxis不指定,則默認是占據全部
# 我們只指定xaxis2和yaxis2
# 我們之前好像在xaxis里面沒有指定anchor,那是因為當時x軸就是在底部,或者說y軸占據了整個垂直方向
# 但是現在不一樣了,我們還要指定y軸的范圍
"xaxis2": {"domain": [0.6, 0.95], "anchor": "y2"},
"yaxis2": {"domain": [0.1, 0.4], "anchor": "x2"}
}
)
fig
我們看到一張圖出現在了另一張圖的里面,這是因為第一坐標軸、也就是默認的軸它的范圍是百分之百的,而我們指定了第二坐標軸的 x 方向和 y 方向只占據了一部分,那么使用該坐標的軸的軌跡就會顯示在局部,而不是像之前一樣占滿全部范圍。而繪制出類似於 make_subplots 的方式,則是讓每個軸 domain 不重合即可,然后計算好位置。所以這種方式應該說更強大,只不過對於繪制標准的、一個子圖占一個坑、規規整整的排列的多個子圖的話,還是推薦使用 make_subplots。而 domain 這種方式這是讓我們自定義的。
而最后再說說那個軸里面的 anchor,這個老鐵主要是空值坐標軸的刻度顯示在什么地方的。我們之前在繪制多個坐標軸的時候,直接使用的 side,那是因為坐標軸所在的兩幅圖的大小是一樣的,但是現在不一樣了。所以我們需要指定第 n 坐標軸的 x 軸的 "anchor" 為 yn、第 n 坐標軸的 y 軸的 "anchor" 為 xn。