Public
Edited
Feb 23, 2023
Insert cell
Insert cell
Insert cell
<br>
Insert cell
Insert cell
<br>
Input dates are structured as JSON arrays. The structure of the file might evolve to take advantage of the possibility to have non flat information (some field for some events that are not in others) but I think even with tabular information JSON is a nice starting point.

<br>
Insert cell
events = [
create_event("2023-02-24", "Table ronde sur l'anonymisation de données", "Etalab", "https://www.numerique.gouv.fr/agenda/table-ronde-sur-lanonymisation-de-donnees/"),
create_event("2023-03-10", "Masterclass Datascientest : Réseaux de neurone", "SSPHub", "https://framaforms.org/participation-aux-masterclass-datascientest-1675096179"),
create_event("2023-03-21", "Data Visualization Fundamentals and Best Practices : Interaction", "observablehq", "https://observablehq.com/@observablehq/datavizcourse?utm_medium=video&utm_campaign=datavizcourse&utm_source=videoembed"),
create_event("2023-03-16", "Data Visualization Fundamentals and Best Practices : Transformations", "observablehq", "https://observablehq.com/@observablehq/datavizcourse?utm_medium=video&utm_campaign=datavizcourse&utm_source=videoembed"),
create_event("2023-03-14", "Data Visualization Fundamentals and Best Practices : Comparisons", "observablehq", "https://observablehq.com/@observablehq/datavizcourse?utm_medium=video&utm_campaign=datavizcourse&utm_source=videoembed"),
create_event("2023-03-09", "Data Visualization Fundamentals and Best Practices : Data representation", "observablehq", "https://observablehq.com/@observablehq/datavizcourse?utm_medium=video&utm_campaign=datavizcourse&utm_source=videoembed"),
create_event("2023-03-07", "Data Visualization Fundamentals and Best Practices : Introduction", "observablehq", "https://observablehq.com/@observablehq/datavizcourse?utm_medium=video&utm_campaign=datavizcourse&utm_source=videoembed"),
create_event("2023-02-13", "Packages Opendata", "SSPHub", "https://ssphub.netlify.app/talk/presentation-des-packages-r-et-python-pour-acceder-a-lopen-data-de-linsee/"),
create_event("2023-02-14", "Journée lancement saison 2 du programme 10%", "Etalab", "https://www.10pourcent.etalab.gouv.fr/"),
create_event("2023-04-04", "Breizh Data Days", "LA FRENCH TECH SAINT-BRIEUC BAY / INNOZH ", "https://meetu.ps/e/LDrDT/JgYdy/i")
]
Insert cell
parser_month = d3.timeParse("%Y-%m-%d")
Insert cell
parser_month_reverse = d3.timeParse("%d-%m-%Y")
Insert cell
function utc_to_string(d){
return d3.timeFormat("%Y-%m-%d")(d)
}
Insert cell
format_month = d3.timeFormat("%m-%Y")
Insert cell
date = "2018-01-16"
Insert cell
events.filter(d => parser_month(d.date) > firstDayOfMonth("2023-01-01"))
Insert cell
function lastDayOfMonth(date) {
var input = new Date(date)
return new Date(input.getFullYear(), input.getMonth()+1, 0);
}
Insert cell
function firstDayOfMonth(date) {
var input = new Date(date)
return new Date(input.getFullYear(), input.getMonth(), 1);
}
Insert cell
input_date = new Date(Date.now())
Insert cell
function addMonths(input_date, months) {
const date = new Date(input_date);
date.setMonth(date.getMonth() + months);

return date;
}
Insert cell
firstDayOfMonth(input_date)
Insert cell
html`<link rel="stylesheet" href="${await require.resolve(
`tippy.js/themes/${tippytheme}.css`
)}">`
Insert cell
html`<style>
.highlight { stroke: #444; stroke-width: 2px }
`
Insert cell
import {addTooltips} from "@mkfreeman/plot-tooltip"
Insert cell
import {mdPlus} from "@tmcw/bonus-markdown-flavor"
Insert cell
viewof start = Inputs.date({label: "Date de début", value: utc_to_string(firstDayOfMonth(input_date))})
Insert cell
viewof end = Inputs.date({label: "Date de fin", value: utc_to_string(lastDayOfMonth(addMonths(input_date, 3)))})
Insert cell
chart = addTooltips(
Calendar2(
events,
{
fill: d => d.organizer == "SSPHub",
dayFormat: d => d.toLocaleString("fr", { weekday: "short", timeZone: "UTC" }),
title: d => `${d.name} \nOrganisateur: ${d.organizer}`,
StartDate: start,
EndDate: end,
color : {
type: "categorical",
scheme: "set1"
}
},
{ fill: "gray", opacity: 0.5, "stroke-width": "3px", stroke: "red" }
)
)
Insert cell
// add tippy to the chart's dots
hover(d3.select(chart).selectAll("rect"), invalidation)
Insert cell
html`<link rel="stylesheet" href="${await require.resolve(
`tippy.js/themes/${tippytheme}.css`
)}">`
Insert cell
html`<style>
.highlight { stroke: #444; stroke-width: 2px }
`
Insert cell
tippytheme
Insert cell
import { viewof tippytheme, hover } with { tipcontent } from "@fil/hello-tippy"
Insert cell
function tipcontent(i) {
return html`<div>
<strong>${events[i].name}</strong>
<div style="max-height: 11em; overflow: auto">
<div><img src="${events[i].reference}featured.png" height=120 /></div>
<div href="${events[i].reference}">Link to event</div>
</div>
</div>`;
}
Insert cell
tipcontent(0)
Insert cell
//function tipcontent(i) {
// const d = events[i];
//
// return md`<div style="max-height: 14em; overflow: auto; width: 340px;">
// <strong>${d.name}</strong>
//<div>${d.organizer}</div></div>` ;
//}
Insert cell
function create_event(date, name, organizer, reference){
let d = {
"date": date, "name": name, "organizer": organizer, "reference": reference
} ;
return d;
}
Insert cell
Calendar2 = {
// default accessors
function valueAccessor(d) {
return typeof d === "object" && ("value" in d ? d.value : d[1]);
}

function dateAccessor(d) {
return typeof d === "object" && ("date" in d ? d.date : d[0]);
}

// https://github.com/d3/d3-time/blob/main/src/utcWeek.js#L4-L13
function utcWeekday(i) {
return d3.timeInterval(
(date) => {
date.setUTCDate(date.getUTCDate() - ((date.getUTCDay() + 7 - i) % 7));
date.setUTCHours(0, 0, 0, 0);
},
(date, step) => date.setUTCDate(date.getUTCDate() + step * 7),
(start, end) => (end - start) / 604800000 // durationWeek;
);
}

return function (
data = [],
{
date = dateAccessor,
value = valueAccessor,
reduce = (d) => d[0],
width = 726,
gap = 0.15,
color,
fill = value || "steelblue",
textFill = "white",
title,
colors = {
base: "#eee",
today: "red"
},
StartDate = null,
EndDate = null,
weekStart = 0, // 1 for Monday-based weeks.
daysToShow = d3.range(7).map((d) => (d + +weekStart) % 7),
weekNumber,
locale = "en-US",
weekNumberFormat = +weekStart === 0 ? "%U" : "%W",
dayFormat = (d) =>
d.toLocaleString(locale, { weekday: "narrow", timeZone: "UTC" }),
monthFormat = (d) =>
d.toLocaleString(locale, { month: "short", timeZone: "UTC" }),
fy, // options for fy, e.g. reverse: false
} = {}
) {
// rollup the data into days
data = Array.from(data, (d) => (typeof d === "string" ? [d, ""] : d));
const dates = Plot.valueof(data, date);
const marked = d3.rollup(data, reduce, (d, i) =>
dates[i] instanceof Date ? dates[i] : d3.isoParse(dates[i])
);
const days = [...marked.keys()].filter((d) => !isNaN(d.getTime())); // filter out invalid dates
if (days.length === 0) days.push(new Date());
const e = d3.extent(days);

// responsive
const W = width < 726 ? "H" : "Y";

// sort all days, with days containing information put at the beginning of the
// array (so we can pass channels as arrays, e.g. fill: [1, 2, 3] for three events)
const fullExtent = [
d3.utcYear.floor(e[0]),
d3.utcYear.offset(d3.utcYear.floor(e[1]))
];
// filter out empty semesters
if (W === "H") {
if (e[0].getUTCMonth() >= 6)
fullExtent[0] = d3.utcMonth.offset(fullExtent[0], 6);
if (e[1].getUTCMonth() < 6)
fullExtent[1] = d3.utcMonth.offset(fullExtent[1], -6);
}
const alldays = new Set([...days, ...d3.utcDays(...fullExtent)]);

// copy the rolled-up data into the days array
data = Array.from(alldays, (date) => ({
date,
...(marked.has(date)
? { ...marked.get(date), date, foreground: true }
: { background: true })
}));



// weekStart and weekNumber
const utcWeek = utcWeekday((weekStart = +weekStart));
if (typeof weekNumberFormat === "string")
weekNumberFormat = d3.utcFormat(weekNumberFormat);
if (![0, 1].includes(weekStart))
throw new Error("unsupported weekStart value");

const weekX =
W === "H"
? (d) =>
+utcWeek.count(d3.utcYear(d), d) -
26.2 * (d.getUTCMonth() >= 6) +
gap * d.getUTCMonth()
: (d) => +utcWeek.count(d3.utcYear(d), d) + gap * +d.getUTCMonth();
const height =
(d3.utcMonths(...d3.extent(alldays)).length / 12) *
(daysToShow.length + 2) *
17 *
(W === "H" ? 2 : 1);

// We want the UTC date that corresponds to our local calendar date
const now = new Date();
const today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());

// formats
if (typeof dayFormat !== "function") dayFormat = d3.utcFormat(dayFormat);
if (typeof monthFormat !== "function")
monthFormat = d3.utcFormat(monthFormat);

// positions
const barOptions = {
x1: (d) => -0.45 + weekX(d.date),
x2: (d) => 0.5 + weekX(d.date),
y: (d) => d.date.getUTCDay(),
insetBottom: 1
};
const textOptions = {
x: (d) => weekX(d.date),
y: (d) => d.date.getUTCDay(),
text: (d) => d.date.getUTCDate(),
fontSize: 8,
pointerEvents: "none"
};

// default title
if (title === undefined) {
const values = Plot.valueof(data, value);
const format = d3.format("~f");
const formatValue = (d) => (typeof d === "number" ? format(d) : d);
title = Plot.valueof(data, (d, i) =>
d.foreground
? `${new Intl.DateTimeFormat(locale, { timeZone: "UTC" }).format(
d.date
)}: ${formatValue(values[i])}`
: undefined
);
}

if (StartDate != null){
data = data.filter(d => d.date > firstDayOfMonth(StartDate))
}
if (EndDate != null){
data = data.filter(d => d.date <= lastDayOfMonth(EndDate))
}

const p = Plot.plot({
width,
marginTop: 0,
marginBottom: 0,
marginLeft: W === "H" ? 70 : 40,
height,
facet: {
data,
y:
W === "H"
? (d) =>
`${d.date.getUTCFullYear()} H${
d.date.getUTCMonth() < 6 ? "1" : "2"
}`
: (d) => `${d.date.getUTCFullYear()}`
},
y: {
// -2/-1 is for the legend/week number, 0=Sun, 1=Mon… 6=Sat
domain: weekNumber ? [-2, -1, ...daysToShow] : [-1, ...daysToShow],
tickFormat: (day) =>
day < 0 ? "" : dayFormat(d3.isoParse(`2000-02-2${day}`)),
tickSize: 0
},
x: { axis: null },
fy: { reverse: true, axis: null, ...fy },
color,
marks: [
// cells
[
colors.base &&
Plot.barX(data, {
filter: "background",
...barOptions,
fill: colors.base
}),
Plot.barX(data, {
filter: "foreground",
...barOptions,
fill,
title
}),
colors.today &&
Plot.barX(data, {
filter: (d) => +d.date === +today,
...barOptions,
fill: "none",
stroke: colors.today
})
],

// labels
[
Plot.text(data, {
filter: "background",
...textOptions,
fill: "black"
}),
Plot.text(data, {
filter: "foreground",
...textOptions,
fill: textFill
})
],

// years and months
[
Plot.text(
data,
Plot.selectMinX({
filter: (d) => d.date.getUTCDay() === weekStart,
x: (d) => weekX(d.date),
y: weekNumber ? -2 : -1,
text: (d) => monthFormat(d.date),
z: (d) => d.date.getUTCMonth()
})
),
Plot.text(
data,
Plot.selectFirst({
sort: "date",
x: 0,
y: weekNumber ? -2 : -1,
text:
W === "H"
? (d) =>
d.date.getUTCFullYear() +
(d.date.getUTCMonth() < 6 ? " H1" : "H2")
: (d) => `${d.date.getUTCFullYear()}`,
textAnchor: "end",
fontWeight: "bold",
dx: -14
})
)
],

// week numbers
weekNumber
? Plot.text(
data,
Plot.selectFirst({
filter: (d) => d.date.getUTCDay() === (weekStart + 6) % 7,
x: (d) => weekX(d.date),
y: -1,
text: (d) => weekNumberFormat(d.date),
fontSize: 7,
fill: "grey",
z: (d) => weekNumberFormat(d.date)
})
)
: null
]
});

p.appendChild(html`<style>.plot text { pointer-events: none }`);
return p;
};
}
Insert cell
To use, import the function in a cell:
~~~js
import {Calendar2} from "@linogaliana/calendar"
~~~

then call it:
~~~js
Calendar2(data, options)
~~~

where **data** is an *iterable* of calendar items.

The **options** are:

* **date** — the date accessor; defaults to the first element of the item. The date is expected to be set in UTC—otherwise a viewer who is not in the same timezone as the author might see 1-day shifts. You can pass the date as an ISO string format (like "2022-03-01"), which will be converted via [d3.isoParse](https://github.com/d3/d3-time-format#isoParse)—this is the recommended (risk-free) approach.

* **value** — value accessor; defaults to the second element of the item.

* **reduce** — a function that decides what happens when multiple events happen on the same day. Defaults to “first”.

* **width** — the width of the calendar in pixels; defaults to 726; if smaller than 726, the calendar will be displayed by semester—allowing a responsive layout.

* **gap** — the gap between months, as a percentage of the cell’s width (defaults to 0.15).

* **color** — a color scale options object to pass to Plot.

* **fill** — controls the color of the marked dates; defaults to *value* if a value was specified, steelblue otherwise.

* **textFill** — text color for marked days; defaults to white.

* **title** — a title attribute.

* **colors** — an object with specific color constants: { base: "#eee", today: "red" }. colors.base will be used to display the background of unmarked days. colors.today is the outline of the current day.

* **weekStart** - 0 for Sunday-based weeks (default); 1 for Monday-based weeks.

* **daysToShow** — which days of the week to show, as an array of weekday numbers (defaults to [0, 1, 2, 3, 4, 5, 6], the whole week—[1, 2, 3, 4, 5, 6, 0] for Monday-based weeks).

* **weekNumber** — should we display the week number? Defaults to false.

* **locale** — the locale for date, days and months formatting; defaults to en-US.

* **dayFormat** - A formatter function for the days; if specified as a string, it is passed to d3.utcFormat (in the default English locale). Defaults to the initial of the day’s name.

* **monthFormat** - A formatter function for the months; if specified as a string, it is passed to d3.utcFormat (in the default English locale). Defaults to the short month name.

* **weekNumberFormat** - A formatter function to receive the date of the last day of the week; if specified as a string, it is passed to d3.utcFormat. %V formats the week as [ISO 8601 week of the year](https://en.wikipedia.org/wiki/ISO_week_date); defaults to [%U for Sunday-based, %W for Monday-based weeks](https://github.com/d3/d3-time-format). Week numbers are expressed as a decimal number [01, 53].

Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more