如何在 d3 日历热图中显示 github 热图之类的日期?
How to show dates like github heatmap in d3 calendar heatmap?
我正在使用 D3 v6 创建一个类似于 GitHub 热图的日历热图,这是我的起点 https://observablehq.com/@d3/calendar-view,但相反,我只想要一个与 [=36] 完全一样的 1 年地图=] 热图,一年前的今天。
这是我目前能做到的https://codesandbox.io/s/heatmap-d3-tzpnu?file=/src/App.js
如果您查看上面的沙盒热图,它首先显示 2021 年 1 月至 2021 年 8 月,然后开始显示 2020 年 8 月至 2020 年 12 月。
如何让它从 2020 年 8 月开始并在今天(2021 年 8 月)结束?
像 github:
我正在使用 dayjs 进行日期操作
这是 React 中的热图代码:
import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
// import { legend } from '@d3/color-legend';
const Heatmap = ({ data }) => {
const [fullYearData, setFullYearData] = useState([]);
const [originalData, setOriginalData] = useState([]);
dayjs.extend(utc);
let chartRef = useRef(null);
const now = dayjs();
const today = now.format("YYYY/MM/DD");
useEffect(() => {
setOriginalData(data);
}, [data]);
useEffect(() => {
const yearBackFromNow = now.subtract(1, "year").format("YYYY/MM/DD");
const firstDate = yearBackFromNow;
const lastDate = today;
// fill the missing dates
if (data && originalData.length > 0) {
const dates = [
...Array(
Date.parse(lastDate) / 86400000 - Date.parse(firstDate) / 86400000 + 1
).keys()
].map(
(k) =>
new Date(86400000 * k + Date.parse(firstDate))
.toISOString()
.slice(0, 10)
// .replace(/-0(\d)$/, '-')
);
// console.log(dates);
let response = [];
for (let i = 0, j = 0; i < dates.length; i++) {
response[i] = {
date: dates[i],
contributions:
dates[i] === originalData[j]?.date
? originalData[j++].contributions
: 0
};
}
setFullYearData(response);
}
}, [originalData]);
useEffect(() => {
if (chartRef && fullYearData) {
let chart = chartRef?.current;
// remove existing svg before showing chart:
//Prevent showing multiple charts
d3.select(".heatmap").remove();
const years = d3.groups(fullYearData, (d) =>
new Date(d.date).getUTCFullYear()
);
// const years = data;
var margin = { top: 80, right: 25, bottom: 30, left: 40 };
// width = 650 - margin.left - margin.right,
// height = 400 - margin.top - margin.bottom;
const weekday = "sunday";
const cellSize = 13;
let width = 730;
const height = cellSize * 9;
// const height = cellSize * (weekday === 'weekday' ? 7 : 9);
// append the svg object to the body of the page
var svg = d3
.select(chart)
.append("svg")
.attr("class", "heatmap")
.style("width", width);
// create a tooltip
var tooltip = d3
.select(chart)
.append("div")
.style("opacity", 0)
.attr("class", "tooltip")
.style("background-color", "#1f1f1f")
.style("padding", "12px 20px")
.style("color", "#ffffff")
.style("width", "250px")
.style("z-index", "10")
.style("line-height", "19px")
.style("position", "absolute");
// Three function that change the tooltip when user hover / move / leave a cell
const mouseover = function (event, d) {
tooltip.style("opacity", 1);
d3.select(this).style("stroke", "black").style("opacity", 1);
};
var mousemove = function (event, d) {
const formatDate = d3.utcFormat("%d/%m/%Y");
const date = formatDate(new Date(d.date));
tooltip
.style(
"left",
`${event.pageX > 1600 ? event.pageX - 200 : event.pageX}px`
)
.style("top", `${event.pageY + 20}px`)
.html("Date: " + date)
.append("div")
.html(`Value: ${d.contributions}`);
// .style('position', 'absolute');
// .html('The exact value of<br>this cell is: ' + d.value)
};
var mouseleave = function (event, d) {
tooltip.style("opacity", 0);
d3.select(this).style("stroke", "none").style("opacity", 0.8);
};
const timeWeek = weekday === "sunday" ? d3.utcSunday : d3.utcMonday;
const countDay = weekday === "sunday" ? (i) => i : (i) => (i + 6) % 7;
// const formatValue = d3.format('+.2%');
// const formatClose = d3.format('$,.2f');
// const formatDate = d3.utcFormat('%x');
// const formatDay = i => 'SMTWTFS'[i];
const formatDay = (i) => "MWFS"[i];
const formatMonth = d3.utcFormat("%b");
// const max = d3.quantile(data, 0.9975, d => Math.abs(d.value));
// const color = d3.scaleSequential(d3.interpolatePiYG).domain(['white', 'red']);
const color = d3
.scaleLinear()
.domain([0, d3.max(fullYearData, (d) => Math.abs(d.value))])
.range(["#EFCFCE", "#F0524D"]);
const year = svg
.selectAll("g")
.data(years)
.join("g")
// .attr('transform', (d, i) => `translate(40.5,${height * i + cellSize * 1.5})`);
.attr("transform", (d, i) => {
return `translate(40.5,${"30"})`;
});
year
.append("g")
.attr("text-anchor", "end")
.selectAll("text")
.data(d3.range(7))
// .data(weekday === 'weekday' ? d3.range(1, 6) : d3.range(4))
.join("text")
.attr("x", -5)
.attr("y", (i) => (countDay(i) + 0.5) * cellSize)
.attr("dy", (d, i) => `${1.15 * i}em`)
.attr("class", "week")
.style("font-size", "12px")
// .text('')
.text(formatDay);
const now = dayjs();
const today = now.format("YYYY/MM/DD");
const yearBackFromNow = now.subtract(1, "year").format("YYYY/MM/DD");
console.log(
"utcsun",
d3.utcSunday(),
d3.utcSunday.count(new Date(yearBackFromNow), new Date(today))
);
year
.append("g")
.style("position", "relative")
.selectAll("rect")
.data(([, values]) => {
// filter to show only selected months data
// return values.filter(d => showMonths.includes(new Date(d.date).getUTCMonth()));
// return new Date(values.date).getUTCMonth();
console.log(values.reverse());
return values.reverse();
})
// .data(
// weekday === 'weekday'
// ? ([, values]) => values.filter(d => ![0, 6].includes(new Date(d.date).getUTCDay()))
// : ([, values]) => values
// )
.join("rect")
.attr("width", cellSize - 3)
.attr("height", cellSize - 3)
// .attr('x', d => {
// console.log('d===', d);
// return timeWeek.count(d3.utcYear(yearBackFromNow, new Date(d.date))) * cellSize + 0.5;
// })
// .attr('x', d => timeWeek.count(new Date(yearBackFromNow), new Date(today)) * cellSize + 0.5)
.attr(
"x",
(d) =>
timeWeek.count(d3.utcYear(new Date(d.date)), new Date(d.date)) *
cellSize +
0.5
)
.attr(
"y",
(d) => countDay(new Date(d.date).getUTCDay()) * cellSize + 0.5
)
.attr("fill", (d) => {
if (d.contributions) {
return color(d.contributions);
} else {
return "#E7E7E7";
}
})
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseleave", mouseleave)
.append("title");
// console.log(today);
// Initialising start and end date
var start = yearBackFromNow;
var end = today;
// Calling the utcMonths() function
// without step value
// var a = d3.utcMonths(start, end);
// Getting the months values
// console.log(a);
const month = year
.append("g")
.selectAll("g")
// .data(([, values]) => {
// console.log(new Date(yearBackFromNow).getUTCMonth(), new Date(today).getUTCMonth());
// // console.log(new Date(data[0].date));
// return d3.utcMonths(start, end);
// // return d3.utcMonths('Feb', 'Dec');
// })
.data(([, values]) => {
return d3.utcMonths(
d3.utcMonth(new Date(values[0].date)),
new Date(values[values.length - 1].date)
// d3.utcMonth(new Date(values[0].date)),
// isXL ? endMonthText : new Date(values[values.length - 1].date)
);
})
.join("g");
month
.append("text")
.attr("x", (d) => {
return timeWeek.count(d3.utcYear(d), timeWeek.ceil(d)) * cellSize + 2;
})
.attr("y", -5)
.attr("class", "month")
.style("font-size", "12px")
.text(formatMonth);
}
}, [fullYearData]);
return (
<>
<div id="chart" ref={chartRef}></div>
</>
);
};
export default Heatmap;
这是我传递的示例数据:
const data = [
{
date: "2021-01-01",
contributions: 10,
details: {
visits: 16,
submissions: 5,
notebooks: 1,
discussions: 4
}
},
{
date: "2021-01-02",
contributions: 10,
details: {
visits: 16,
submissions: 5,
notebooks: 1,
discussions: 4
}
},
{
date: "2021-01-05",
contributions: 5,
details: {
visits: 16,
submissions: 5,
notebooks: 1,
discussions: 4
}
},
{
date: "2021-02-05",
contributions: 3,
details: {
visits: 16,
submissions: 5,
notebooks: 1,
discussions: 4
}
}
];
从https://codesandbox.io/s/heatmap-d3-tzpnu?file=/src/Heatmap.js开始
您将 yearBackFromNow
定义为指向您感兴趣的第一天的字符串。
在第 209 行中,您可以使用 Date.parse(yearBackFromNow)
而不是 d3.utcYear(d.date)
作为时间段的开始,这会将图块放置在您想要的位置。
.attr(
"x",
(d) =>
timeWeek.count(Date.parse(yearBackFromNow), new Date(d.date)) *
cellSize +
0.5
)
在第 265 行应用相同的内容
.attr("x", (d) => {
return timeWeek.count(Date.parse(yearBackFromNow), timeWeek.ceil(d)) * cellSize + 2;
})
如果你想要更有意义的颜色
第 145 行
.domain([0, d3.max(fullYearData, (d) => Math.abs(d.contributions))])
App.js 第 6 行
{
date: "2020-10-01",
contributions: 20,
details: {
visits: 16,
submissions: 5,
notebooks: 1,
discussions: 4
}
},
这是今天(2021 年 8 月 13 日)的样子
分叉到
https://codesandbox.io/s/heatmap-d3-forked-ut8jg
还有一件事,请保持一周从周日开始,到周六结束。
而不是
const timeWeek = weekday === "sunday" ? d3.utcSunday : d3.utcMonday;
const countDay = weekday === "sunday" ? (i) => i : (i) => (i + 6) % 7;
你有
const timeWeek = d3.utcSunday;
const countDay = (i) => i;
我正在使用 D3 v6 创建一个类似于 GitHub 热图的日历热图,这是我的起点 https://observablehq.com/@d3/calendar-view,但相反,我只想要一个与 [=36] 完全一样的 1 年地图=] 热图,一年前的今天。
这是我目前能做到的https://codesandbox.io/s/heatmap-d3-tzpnu?file=/src/App.js
如果您查看上面的沙盒热图,它首先显示 2021 年 1 月至 2021 年 8 月,然后开始显示 2020 年 8 月至 2020 年 12 月。
如何让它从 2020 年 8 月开始并在今天(2021 年 8 月)结束? 像 github:
我正在使用 dayjs 进行日期操作
这是 React 中的热图代码:
import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
// import { legend } from '@d3/color-legend';
const Heatmap = ({ data }) => {
const [fullYearData, setFullYearData] = useState([]);
const [originalData, setOriginalData] = useState([]);
dayjs.extend(utc);
let chartRef = useRef(null);
const now = dayjs();
const today = now.format("YYYY/MM/DD");
useEffect(() => {
setOriginalData(data);
}, [data]);
useEffect(() => {
const yearBackFromNow = now.subtract(1, "year").format("YYYY/MM/DD");
const firstDate = yearBackFromNow;
const lastDate = today;
// fill the missing dates
if (data && originalData.length > 0) {
const dates = [
...Array(
Date.parse(lastDate) / 86400000 - Date.parse(firstDate) / 86400000 + 1
).keys()
].map(
(k) =>
new Date(86400000 * k + Date.parse(firstDate))
.toISOString()
.slice(0, 10)
// .replace(/-0(\d)$/, '-')
);
// console.log(dates);
let response = [];
for (let i = 0, j = 0; i < dates.length; i++) {
response[i] = {
date: dates[i],
contributions:
dates[i] === originalData[j]?.date
? originalData[j++].contributions
: 0
};
}
setFullYearData(response);
}
}, [originalData]);
useEffect(() => {
if (chartRef && fullYearData) {
let chart = chartRef?.current;
// remove existing svg before showing chart:
//Prevent showing multiple charts
d3.select(".heatmap").remove();
const years = d3.groups(fullYearData, (d) =>
new Date(d.date).getUTCFullYear()
);
// const years = data;
var margin = { top: 80, right: 25, bottom: 30, left: 40 };
// width = 650 - margin.left - margin.right,
// height = 400 - margin.top - margin.bottom;
const weekday = "sunday";
const cellSize = 13;
let width = 730;
const height = cellSize * 9;
// const height = cellSize * (weekday === 'weekday' ? 7 : 9);
// append the svg object to the body of the page
var svg = d3
.select(chart)
.append("svg")
.attr("class", "heatmap")
.style("width", width);
// create a tooltip
var tooltip = d3
.select(chart)
.append("div")
.style("opacity", 0)
.attr("class", "tooltip")
.style("background-color", "#1f1f1f")
.style("padding", "12px 20px")
.style("color", "#ffffff")
.style("width", "250px")
.style("z-index", "10")
.style("line-height", "19px")
.style("position", "absolute");
// Three function that change the tooltip when user hover / move / leave a cell
const mouseover = function (event, d) {
tooltip.style("opacity", 1);
d3.select(this).style("stroke", "black").style("opacity", 1);
};
var mousemove = function (event, d) {
const formatDate = d3.utcFormat("%d/%m/%Y");
const date = formatDate(new Date(d.date));
tooltip
.style(
"left",
`${event.pageX > 1600 ? event.pageX - 200 : event.pageX}px`
)
.style("top", `${event.pageY + 20}px`)
.html("Date: " + date)
.append("div")
.html(`Value: ${d.contributions}`);
// .style('position', 'absolute');
// .html('The exact value of<br>this cell is: ' + d.value)
};
var mouseleave = function (event, d) {
tooltip.style("opacity", 0);
d3.select(this).style("stroke", "none").style("opacity", 0.8);
};
const timeWeek = weekday === "sunday" ? d3.utcSunday : d3.utcMonday;
const countDay = weekday === "sunday" ? (i) => i : (i) => (i + 6) % 7;
// const formatValue = d3.format('+.2%');
// const formatClose = d3.format('$,.2f');
// const formatDate = d3.utcFormat('%x');
// const formatDay = i => 'SMTWTFS'[i];
const formatDay = (i) => "MWFS"[i];
const formatMonth = d3.utcFormat("%b");
// const max = d3.quantile(data, 0.9975, d => Math.abs(d.value));
// const color = d3.scaleSequential(d3.interpolatePiYG).domain(['white', 'red']);
const color = d3
.scaleLinear()
.domain([0, d3.max(fullYearData, (d) => Math.abs(d.value))])
.range(["#EFCFCE", "#F0524D"]);
const year = svg
.selectAll("g")
.data(years)
.join("g")
// .attr('transform', (d, i) => `translate(40.5,${height * i + cellSize * 1.5})`);
.attr("transform", (d, i) => {
return `translate(40.5,${"30"})`;
});
year
.append("g")
.attr("text-anchor", "end")
.selectAll("text")
.data(d3.range(7))
// .data(weekday === 'weekday' ? d3.range(1, 6) : d3.range(4))
.join("text")
.attr("x", -5)
.attr("y", (i) => (countDay(i) + 0.5) * cellSize)
.attr("dy", (d, i) => `${1.15 * i}em`)
.attr("class", "week")
.style("font-size", "12px")
// .text('')
.text(formatDay);
const now = dayjs();
const today = now.format("YYYY/MM/DD");
const yearBackFromNow = now.subtract(1, "year").format("YYYY/MM/DD");
console.log(
"utcsun",
d3.utcSunday(),
d3.utcSunday.count(new Date(yearBackFromNow), new Date(today))
);
year
.append("g")
.style("position", "relative")
.selectAll("rect")
.data(([, values]) => {
// filter to show only selected months data
// return values.filter(d => showMonths.includes(new Date(d.date).getUTCMonth()));
// return new Date(values.date).getUTCMonth();
console.log(values.reverse());
return values.reverse();
})
// .data(
// weekday === 'weekday'
// ? ([, values]) => values.filter(d => ![0, 6].includes(new Date(d.date).getUTCDay()))
// : ([, values]) => values
// )
.join("rect")
.attr("width", cellSize - 3)
.attr("height", cellSize - 3)
// .attr('x', d => {
// console.log('d===', d);
// return timeWeek.count(d3.utcYear(yearBackFromNow, new Date(d.date))) * cellSize + 0.5;
// })
// .attr('x', d => timeWeek.count(new Date(yearBackFromNow), new Date(today)) * cellSize + 0.5)
.attr(
"x",
(d) =>
timeWeek.count(d3.utcYear(new Date(d.date)), new Date(d.date)) *
cellSize +
0.5
)
.attr(
"y",
(d) => countDay(new Date(d.date).getUTCDay()) * cellSize + 0.5
)
.attr("fill", (d) => {
if (d.contributions) {
return color(d.contributions);
} else {
return "#E7E7E7";
}
})
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseleave", mouseleave)
.append("title");
// console.log(today);
// Initialising start and end date
var start = yearBackFromNow;
var end = today;
// Calling the utcMonths() function
// without step value
// var a = d3.utcMonths(start, end);
// Getting the months values
// console.log(a);
const month = year
.append("g")
.selectAll("g")
// .data(([, values]) => {
// console.log(new Date(yearBackFromNow).getUTCMonth(), new Date(today).getUTCMonth());
// // console.log(new Date(data[0].date));
// return d3.utcMonths(start, end);
// // return d3.utcMonths('Feb', 'Dec');
// })
.data(([, values]) => {
return d3.utcMonths(
d3.utcMonth(new Date(values[0].date)),
new Date(values[values.length - 1].date)
// d3.utcMonth(new Date(values[0].date)),
// isXL ? endMonthText : new Date(values[values.length - 1].date)
);
})
.join("g");
month
.append("text")
.attr("x", (d) => {
return timeWeek.count(d3.utcYear(d), timeWeek.ceil(d)) * cellSize + 2;
})
.attr("y", -5)
.attr("class", "month")
.style("font-size", "12px")
.text(formatMonth);
}
}, [fullYearData]);
return (
<>
<div id="chart" ref={chartRef}></div>
</>
);
};
export default Heatmap;
这是我传递的示例数据:
const data = [
{
date: "2021-01-01",
contributions: 10,
details: {
visits: 16,
submissions: 5,
notebooks: 1,
discussions: 4
}
},
{
date: "2021-01-02",
contributions: 10,
details: {
visits: 16,
submissions: 5,
notebooks: 1,
discussions: 4
}
},
{
date: "2021-01-05",
contributions: 5,
details: {
visits: 16,
submissions: 5,
notebooks: 1,
discussions: 4
}
},
{
date: "2021-02-05",
contributions: 3,
details: {
visits: 16,
submissions: 5,
notebooks: 1,
discussions: 4
}
}
];
从https://codesandbox.io/s/heatmap-d3-tzpnu?file=/src/Heatmap.js开始
您将 yearBackFromNow
定义为指向您感兴趣的第一天的字符串。
在第 209 行中,您可以使用 Date.parse(yearBackFromNow)
而不是 d3.utcYear(d.date)
作为时间段的开始,这会将图块放置在您想要的位置。
.attr(
"x",
(d) =>
timeWeek.count(Date.parse(yearBackFromNow), new Date(d.date)) *
cellSize +
0.5
)
在第 265 行应用相同的内容
.attr("x", (d) => {
return timeWeek.count(Date.parse(yearBackFromNow), timeWeek.ceil(d)) * cellSize + 2;
})
如果你想要更有意义的颜色 第 145 行
.domain([0, d3.max(fullYearData, (d) => Math.abs(d.contributions))])
App.js 第 6 行
{
date: "2020-10-01",
contributions: 20,
details: {
visits: 16,
submissions: 5,
notebooks: 1,
discussions: 4
}
},
这是今天(2021 年 8 月 13 日)的样子
分叉到 https://codesandbox.io/s/heatmap-d3-forked-ut8jg
还有一件事,请保持一周从周日开始,到周六结束。
而不是
const timeWeek = weekday === "sunday" ? d3.utcSunday : d3.utcMonday;
const countDay = weekday === "sunday" ? (i) => i : (i) => (i + 6) % 7;
你有
const timeWeek = d3.utcSunday;
const countDay = (i) => i;