- Obsidian插件Dataview —— 安装与设置(一)
- Obsidian插件Dataview —— YAML简介(二)
- Obsidian插件Dataview —— 认识属性(三)
- Obsidian插件Dataview —— 数据查询(四)
- Obsidian插件Dataview —— DQL查询语言详解(五)
- Obsidian插件DataviewJS —— TypeScript速成(六)
- Obsidian插件Dataview —— 深入理解DataviewJS(七)
- Obsidian插件Dataview —— JavaScript API 快速入门(八)
- Obsidian插件Dataview —— DataArray接口介绍(九)
- Obsidian插件Dataview —— Dataviewjs JavaScript API 进阶用法(十)
- Obsidian插件Dataview —— Luxon库介绍(十一)
- Obsidian插件Dataview —— 实用案例讲解(初级篇)(十二)
- Obsidian插件Dataview —— 实用案例讲解(中级篇)(十三)
- Obsidian插件Dataview —— 实用案例讲解(高级篇)(十四)
- Obsidian插件Dataview —— 函数合集(十五)
目录
[!quote] 背景
主要梳理了 Dataview 示例库 Dataview Example Valut 中的一些案例
三、高级篇:Dataview 高级技巧与探索
高级篇的内容主要是一些不常用,但是实用,需要更多的代码的内容,或者需要结合第三方插件的内容。
1. 表格列求和
在电子表格中,我们可以对列数据进行运算,如求和、未平均值等。下面我们来看一下如何在 Dataview 查询的结果中实现对列数据的求和。
const query = `TABLE praying, training, situps, steps
FROM "10 Example Data/dailys"
WHERE file.day.month = 2`
const nameOfTotalRow = "Sums";
let DQL = await dv.tryQuery(query);
const sums = [nameOfTotalRow];
// 如果在 DQL 查询语句中添加了 `WITHOUT ID`,这里就需要改成从 `0` 开始遍历
for (let i = 1; i < DQL.headers.length; i++) {
let sum = 0;
const dataType = getDatatypeOfColumn(i, DQL.values)
// 只有数字和持续时间的数据类型才会被计算
if (!["number", "duration"].includes(dataType)) {
sums.push("")
continue;
}
// 计算每一列的总和
for (let k = 0; k < DQL.values.length; k++) {
// 行 `k`, 列 `i` 的值
let currentValue = DQL.values[k][i];
if (currentValue) sum += currentValue
}
if (!sum) sum = ""
sums.push(dataType === "duration" ? dv.luxon.Duration.fromMillis(sum) : sum);
}
function getDatatypeOfColumn(columnNo, values) {
let i = 0;
let datatype;
while (i < DQL.values[0].length && (!datatype || datatype === "null")) {
datatype = dv.func.typeof(DQL.values[i][columnNo])
i++;
}
return datatype;
}
// 添加分隔线
let hrArray = Array(DQL.headers.length).fill('<hr style="padding:0; margin:0 -10px;">');
DQL.values.push(hrArray)
DQL.values.push(sums)
dv.table(DQL.headers, DQL.values)
结果:
2. 在文档中搜索文字
要在文档中搜索指定的单词,我们首先需要将文件读到内容中,可使用 dv.io.load()
方法,然后再通过正则去匹配文本。
const word = "but"
const regex = new RegExp("(\\S+\\s?){0,2}(\\b"+word+"\\b)(\\s\\S+){0,2}", "gi")
const pages = await Promise.all(
dv.pages('"30 Dataview Resources"')
.map(async (page) => {
const content = await dv.io.load(page.file.path);
const matches = content.match(regex);
return {
link: page.file.link,
count: ( matches || []).length,
matches
};
})
)
dv.table(
["Note", "Count", `Matches for "${word}"`],
pages
.filter(p => p.count)
.sort((a, b) => b.count - a.count)
.map(p => [p.link, p.count, p.matches])
);
结果:
上面的正则表达式中 \b
用于匹配单词边界(英文句子单词之间以空格分隔),然后最左边的 (\\S+\\s?){0,2}
和右边的 (\\s\\S+){0,2}
用于匹配目标单词前后的两个相邻单词。基中 +
符表示匹配 1 次或多次,?
表示匹配 0 次或多次,\s
表示匹配一个空白字符(包括空格、制表符、换页符和换行符),\S
表示匹配一个非空白符。dv.io.load()
方法用于将文件加载到内存中。
3. 使用选项卡切换数据
在查询数据时,有的数据不同的状态会有不同的结果,我们可以按状态来进行条件显示。将状态作为选项卡,而其关联的内容作为选项卡内容。
const createButton = name => {
const btn = dv.el('button', name)
btn.addEventListener('click', () => {
event.preventDefault()
removeTable()
renderTable(name)
})
return btn
}
const buttons = ['Watching', 'Going to watch', 'Watched all', 'Stopped watching']
const renderTable = name => {
const pages = dv.pages('"10 Example Data/shows"').where(p => p.status === name)
dv.header(2, name)
dv.table(
['Title', 'Rating', 'Runtime', 'Seasons', 'Episodes'],
pages.map(p => {
let watchedEp = 0
const totalEp = p.episodes
p.file.tasks.forEach(t => {
if (t.checked) {
watchedEp++
}
})
return [p.file.link, p.rating, p.runtime, p.seasons, `${watchedEp}/${totalEp}`]
})
)
}
const removeTable = () => {
this.container.lastChild.remove()
this.container.lastChild.remove()
}
buttons.forEach(button => createButton(button))
renderTable('Watching')
结果:
上面的代码中,我们使用 dv.el()
来创建了按钮并添加了事件处理逻辑。在选项卡被选中时,根据选项卡名去过滤查询结果,并将上一次渲染的 HTML 节点移除掉。
进一步,我们还可以实现同一份数据结果以不同的方式渲染:
const views = ['Table', 'List', 'Tasks']
const changeView = viewName => {
removeView()
if (viewName === 'Table') {
dv.header(2, 'Some table')
dv.table(['File', 'Day'], dv.pages('"10 Example Data/dailys"').limit(7).map(p => [p.file.link, p.day]))
}
if (viewName == 'List') {
dv.list(dv.pages('"10 Example Data/dailys"').limit(7).file.name)
}
if (viewName == 'Tasks') {
dv.taskList(dv.page("10 Example Data/projects/project_2").file.tasks)
}
}
const createButtons = () => {
const buttonContainer = dv.el('div', '', {cls: 'tabButtons'})
views.forEach(view => {
const button = dv.el('button', view)
button.addEventListener('click', event => {
event.preventDefault()
changeView(view)
})
buttonContainer.append(button)
})
}
const removeView = () => {
Array.from(this.container.children).forEach(el => {
if (!el.classList.contains('tabButtons')) {
el.remove()
}
})
}
createButtons()
4. 使用不同的表情符来显示时间缀
这个案例我们查询任务计划数据,来获取未完成的任务距离现在过去了多长时间,并对其按时间长度自定义不同的表情符来显示得分。
- 如果月数超过6个月,则添加 “🥳” 表情符号。
- 如果剩余的月数(在超过6个月后)超过3个月,则添加 “🎉” 表情符号。
- 如果剩余的月数(在超过9个月后)仍然有剩余,则添加 “🎁” 表情符号。
const projects = dv.pages('"10 Example Data/projects"')
.where(p => p.status !== undefined && p.status !== "finished")
.mutate(p => {
p.age = p.started && p.started instanceof dv.luxon.DateTime ? dv.luxon.Duration.fromMillis(Date.now() - p.started.toMillis()) : null
p.emojiAgeScore = getEmojiScore(p)
})
dv.table(["Score", "Project", "Started", "Age"], projects.map(p => [p.emojiAgeScore, p.file.link, p.started, p.age ? p.age.toFormat("y'年' M'个月' w'周'") : 'N/A']))
function getEmojiScore(p) {
const age = p.age.shiftTo('months').toObject()
let score = ""
score += addEmojis("🥳", age.months / 6)
score += addEmojis("🎉", (age.months % 6) / 3)
score += addEmojis("🎁", age.months % 6 % 3)
return score
}
function addEmojis(emoji, max) {
let emojis = ""
for (let i = 0; i < Math.floor(max); i++) emojis += emoji
return emojis
}
结果:
代码中 shiftTo('months').toObject()
函数用于将时间缀转换成类似:xx个月
的形式。
关于 Luxon 的使用可以阅读系列中的 Luxon 章节。
5. 将数据渲染成日历
下面这个案例可以好好研究一下如何运用 Luxon 填充每天的数据和构造 HTML 结构。
const values = dv.pages('"10 Example Data/dailys"').where(p => p.wellbeing?.mood)
const year = 2022
const color = "green"
const emptyColor = "#e4e4e4"
const dt = dv.luxon.DateTime
// 创建日历数据
let date = dt.utc(year)
const calendar = []
S
for (let i = 1; i <= 12; i++) {
calendar[i] = []
}
// 填充日历数据
while (date.year === year) {
calendar[date.month].push(getDayEl(date, determineColor(date)))
date = addOneDay(date);
}
// 渲染日历
calendar.forEach((month, i) => {
const monthEl = `<span style="display:inline-block;width:30px;font-size:small">${dt.utc(year, i).monthShort}</span>`
dv.el("div", monthEl + month.reduce((acc, cur) => `${acc} ${cur}`, ""))
})
function addOneDay(date) {
return dt.fromMillis(date + dv.duration("1d"))
}
function getDayEl(date, color) {
const sizeOfDays = "12px"
return `<span style="width:${sizeOfDays};height:${sizeOfDays};border-radius:2px;background-color:${color};display:inline-block;font-size:4pt;" title="${date.toFormat('yyyy-MM-dd')}"></span>`
}
function determineColor(date) {
const page = values.find(p => p.file.day.startOf('day').equals(date.startOf('day')));
if (!page) return emptyColor;
let opacity = (page.wellbeing.mood / 4) ;
return `rgba(177, 200, 51, ${opacity})`;
}
结果:
进一步阅读:Render a year overview for your data – Dataview Example Vault (s-blu.github.io)
6. 使用 Chart. Js 渲染图表
要在 Obsidian 中渲染图表,我们需要用到 Obsidian-Charts 这个插件。
let chartType = 'bar'; //bar or line
let xAxis = "xAxis: {type:'time', time: {unit: 'day'}}"; // {type:'category'}";
let yAxis = "yAxis: {suggestedMin: 0, ticks: {stepSize: 1}";
let autoLabels = true; // 自动设置标签
var labels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; // autoLabels 为 false 时,手动设置标签
var colors = [['#ff6384'],['#36a2eb'],['#ffce56'],['#4bc0c0'],['#9966ff'],['#ff9f40']];
let sDQL = '\
TABLE WITHOUT ID \
file.name as "Date", \
wellbeing.mood as "Mood", \
wellbeing.health as "Health", \
wellbeing.pain as "Pain" \
FROM "10 Example Data/dailys" \
WHERE date(file.name).year = 2022 \
SORT file.name'
let DQL = await dv.tryQuery(sDQL);
var allRows = DQL.values;
var allLabels = allRows.map(r => r[0]);
var allSeries = DQL.headers.slice(1);
if (autoLabels) {
labels = allLabels;
}
var datasets = [];
for (let i = 0; i < allSeries.length; i++) {
let seriesName = allSeries[i];
let backCol = colors[i%colors.length];
let bordCol = colors[i%colors.length];
let bWidth = 1;
var dataPoints = [];
if (!autoLabels) {
dataPoints = labels.map(l => {
let labelIndex = allLabels.indexOf(l);
if (labelIndex < 0) { return 0 }
else { return allRows[labelIndex][i+1] }
})
} else {
dataPoints = allRows.map(r => r[i+1]);
}
let chartDataset = {label: seriesName,
data: dataPoints,
backgroundColor: backCol,
borderColor: bordCol,
borderWidth: bWidth};
datasets.push(chartDataset);
}
// 如果我们使用固定的标签数组,请使xAxis基于类别
xAxis = (autoLabels ? xAxis : "xAxis: {type:'category'}");
// chart.js 选项配置
const chartData = {
type: chartType,
data: {
labels: labels,
datasets: datasets
},
options: {
scales: { xAxis, yAxis }
}
}
window.renderChart(chartData, this.container);
7. 使用 Heatmap Calendar 插件显示热力圈
这个案例我们来使用插件 Richardsl/heatmap-calendar-obsidian: An Obsidian plugin for displaying data in a calendar similar to the github activity calendar 渲染一个步数的热力图。
const calendarData = {
year: 2022,
entries: []
}
for (let page of dv.pages('"10 Example Data/dailys"').filter(p => p.steps)) {
calendarData.entries.push({
date: page.file.name,
intensity: page.steps,
content: await dv.span(`[](${page.file.name})`) // 用于预览
})
}
renderHeatmapCalendar(this.container, calendarData)