Journal: 2026-02-28
Sentiment analysis visualisation - matplotlib version
↳ from journal/2026-01-01
fig done
Show code
from datetime import datetime
import calendar
from collections import defaultdict
import json
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.colors import LinearSegmentedColormap
def get_theme_colors(nb):
"""Fetch current CSS theme colors from settings."""
# FIX 1: query_all() takes a dict, not keyword args
blocks = nb.query_all(block_type="note",
metadata = {'css_enabled': True}
)
if blocks:
theme = blocks[0]['metadata']['variables']
return theme
return None
def sentiment_color(score, has_data=True, theme=None):
"""Map sentiment score using theme colors."""
if theme is None:
theme = {
'--bg-hover': '#f2e9e1',
'--hue-error': '#b4637a',
'--hue-success': '#286983',
}
if not has_data:
return theme['--bg-hover']
score = max(-1, min(1, score))
colors = [theme['--hue-error'], theme['--bg-hover'], theme['--hue-success']]
positions = [0, 0.5, 1]
cmap = LinearSegmentedColormap.from_list('sentiment', list(zip(positions, colors)))
normalized = (score + 1) / 2
rgba = cmap(normalized)
return '#{:02x}{:02x}{:02x}'.format(int(rgba[0]*255), int(rgba[1]*255), int(rgba[2]*255))
def generate_month(blocks, year, month, nb, figsize=(8, 7)):
"""Generate sentiment calendar using theme colors."""
theme = get_theme_colors(nb)
if theme is None:
theme = {
'--bg-app': '#faf4ed',
'--bg-hover': '#f2e9e1',
'--text-body': '#575279',
'--text-muted': '#797593',
'--hue-error': '#b4637a',
'--hue-success': '#286983',
}
# Group scores by day
daily_scores = defaultdict(list)
for block in blocks:
meta = block.get("metadata") or {}
if "sentiment" not in meta:
continue
page_title = block.get("page_title", "")
if page_title.startswith("journal/"):
try:
day = int(page_title.split("-")[-1])
score = meta["sentiment"]
if isinstance(score, str):
score = float(score)
daily_scores[day].append(score)
except (ValueError, IndexError):
continue
daily_avg = {day: sum(s)/len(s) for day, s in daily_scores.items()}
# Build calendar grid
cal = calendar.Calendar(firstweekday=6)
month_name = calendar.month_name[month]
fig, ax = plt.subplots(figsize=figsize, facecolor=theme['--bg-app'])
ax.set_facecolor(theme['--bg-app'])
ax.set_xlim(0, 7)
ax.set_ylim(-0.8, 7)
ax.set_aspect('equal')
ax.axis('off')
# Title
ax.text(3.5, 6.5, f"{month_name} {year}", ha='center', va='center',
fontsize=22, color=theme['--text-body'], fontweight='bold')
# Day headers
days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
for i, day in enumerate(days):
ax.text(i + 0.5, 5.75, day, ha='center', va='center',
fontsize=10, color=theme['--text-muted'], fontweight='medium')
# Calendar cells
row = 5
col = 0
for date in cal.itermonthdates(year, month):
if date.month != month:
col += 1
if col == 7:
col = 0
row -= 1
continue
has_data = date.day in daily_avg
score = daily_avg.get(date.day)
color = sentiment_color(score if score else 0, has_data, theme)
# Cell
rect = patches.FancyBboxPatch(
(col + 0.05, row - 0.95), 0.9, 0.9,
boxstyle="round,pad=0.02,rounding_size=0.08",
facecolor=color, edgecolor='none'
)
ax.add_patch(rect)
# Text color based on background
txt_color = theme['--text-body'] if has_data else theme['--text-muted']
# Day number
ax.text(col + 0.5, row - 0.38, str(date.day),
ha='center', va='center',
fontsize=13, color=txt_color, fontweight='bold')
# Score
if has_data:
ax.text(col + 0.5, row - 0.68, f"{score:+.1f}",
ha='center', va='center',
fontsize=8, color=txt_color, alpha=0.7)
col += 1
if col == 7:
col = 0
row -= 1
# Legend
legend_items = [
('Negative', theme['--hue-error']),
('Neutral', theme['--bg-hover']),
('Positive', theme['--hue-success'])
]
for i, (label, c) in enumerate(legend_items):
x = 1.5 + i * 2
rect = patches.FancyBboxPatch(
(x, -0.45), 0.3, 0.3,
boxstyle="round,pad=0.02,rounding_size=0.05",
facecolor=c, edgecolor='none'
)
ax.add_patch(rect)
ax.text(x + 0.45, -0.3, label, va='center', fontsize=9, color=theme['--text-muted'])
plt.tight_layout()
return fig, ax
# --- Main execution ---
date = datetime.strptime("2025-12-01", "%Y-%m-%d")
first_day = date.replace(day=1)
last_day = date.replace(day=calendar.monthrange(date.year, date.month)[1])
blocks = nb.query_all({
'metadata': {'source': 'dayone'},
'date_from': first_day.strftime("%Y-%m-%d"),
'date_to': last_day.strftime("%Y-%m-%d"),
})
fig, ax = generate_month(blocks, date.year, date.month, nb)
print("fig done")
plt.show()
Sentiment data query
February 2026
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Negative
Slightly negative
Neutral
Positive
Very positive
Hide code
-- Query all blocks that have sentiment metadata and aggregate by date
local current_page = nb.current_page
local blocks
-- Check if we're on a journal page and extract date range
if current_page and current_page:match("^journal/") then
local date_str = current_page:match("[^/]+$") -- extract "2025-10-31"
local year, month, day = date_str:match("(%d%d%d%d)%-(%d%d)%-(%d%d)")
if year and month then
year, month = tonumber(year), tonumber(month)
local first_day = string.format("%04d-%02d-01", year, month)
-- Compute last day of month
local last_day = string.format("%04d-%02d-%02d", year, month, 31)
blocks = nb.query_all({
tags = {"dayone"},
date_from = first_day,
date_to = last_day
})
end
else
-- Not on journal page, query February 2026
blocks = nb.query_all({
tags = {"dayone"},
date_from = "2026-03-01",
date_to = "2026-03-31"
})
end
local by_date = {}
local counts = {}
for _, b in ipairs(blocks) do
local d = b.metadata.date or b.date
local s = tonumber(b.metadata.sentiment)
if d and s then
local key = string.sub(d, 1, 10)
by_date[key] = (by_date[key] or 0) + s
counts[key] = (counts[key] or 0) + 1
end
end
local result = {}
for date, total in pairs(by_date) do
result[date] = total / counts[date]
end
-- Extract month label and days in month from current context
local month_label = "Monthly View"
local days_in_month = 31
if current_page and current_page:match("^journal/") then
local date_str = current_page:match("[^/]+$")
local year, month = date_str:match("(%d%d%d%d)%-(%d%d)")
if year and month then
local month_names = {"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"}
month_label = month_names[tonumber(month)] .. " " .. year
-- Calculate days in month
local days = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
local y = tonumber(year)
if tonumber(month) == 2 and (y % 4 == 0 and (y % 100 ~= 0 or y % 400 == 0)) then
days_in_month = 29
else
days_in_month = days[tonumber(month)]
end
end
end
ui.render("sentiment-calendar-heatmap-v2", {
data = result,
monthLabel = month_label,
daysInMonth = days_in_month
})