Public
Edited
Jan 24, 2024
1 fork
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
platform = window.navigator.platform
Insert cell
isMobile = width <= 888
Insert cell
margin = isMobile ? 1 : 5
Insert cell
dms = {
let dimensions = {
width: isMobile ? width : 640,
marginTop: isMobile ? margin : margin + dmsAxis.x,
marginLeft: margin,
marginRight: margin + dmsAxis.y + (isMobile ? 4 : 0),
marginBottom: margin
};

dimensions.chartWidth =
dimensions.width - dimensions.marginLeft - dimensions.marginRight;

return dimensions;
}
Insert cell
dmsHeight = {
// const height = (scaleEvents.bandwidth() / 1.5) * scalePointsMax;
const height = isMobile ? dms.width * 2 : dms.width * 1.5;
const chartHeight = height - dms.marginTop - dms.marginBottom;

return { height, chartHeight };
}
Insert cell
dmsAxis = ({ x: 20, y: 8 })
Insert cell
colors = ({
teams: {
ferrari: { primary: "#F91536", accent: "" },
mercedes: { primary: "#6CD3BF", accent: "" },
haas: { primary: "#B6BABD", accent: "" },
alfa: { primary: "#C92D4B", accent: "" },
// alpine: { primary: "#2293D1", accent: "" },
alpine: { primary: "#ff95f2", accent: "" },
racing_point: { primary: "#ff95f2", accent: "" },
alphatauri: { primary: "#5E8FAA", accent: "" },
toro_rosso: { primary: "#469BFF", accent: "" },
aston_martin: { primary: "#358C75", accent: "" },
williams: { primary: "#37BEDD", accent: "" },
mclaren: { primary: "#F58020", accent: "" },
// red_bull: { primary: "#3671C6", accent: "#e0bb12" },
red_bull: { primary: "#FFCC00", accent: "" },
renault: { primary: "#121212", accent: "" },
force_india: { primary: "#25b34b", accent: "" },
sauber: { primary: "#1f2eb5", accent: "" },
manor: { primary: "#e85538", accent: "" },
// lotus_f1: { primary: "#f7be39", accent: "" },
lotus_f1: { primary: "#121212", accent: "" },
marussia: { primary: "#e34e24", accent: "" },
caterham: { primary: "#5ae07b", accent: "" },
hrt: { primary: "#968133", accent: "" }
},
background: "#fff",
fade: "#f0f0f0"
// brand: "#e10600"
})
Insert cell
Insert cell
<style>
/* Styling the container for the graphic */
.container {
width: 100%;
display: flex;
flex-direction: ${isMobile? "column-reverse" : "row"};
justify-content: space-between;
gap: 10px;
background-color: ${colors.background};
z-index: -1;
}
.containerSVG svg {
display: block;
}
.containerTooltip {
width: 100%;
}

/* Styling the tooltip as a whole */
.tooltipBG {
margin: ${isMobile ? 0 : dms.marginTop}px ${isMobile ? dms.marginRight - dmsAxis.y : 0}px ${isMobile ? 0 : dms.marginBottom}px ${dms.marginLeft}px;
height: calc(100% - ${isMobile ? 0 : dms.marginTop + dms.marginBottom + 2}px);
width: calc(100% - ${isMobile ? dms.marginLeft + dms.marginRight + scaleEvents.bandwidth() : 0}px);
border-top: 2px solid ${colors.fade};
border-right: 2px solid ${colors.fade};
border-radius: 0 30px 0 0;
/* border-radius: ${ isMobile ? `${scaleEvents.bandwidth()/2}px` : '0px'}; */
overflow-x: hidden;
overflow-y: auto;
/* background-color: ${colors.fade}; */
background-image: url(${await FileAttachment("hash.svg").url()});
background-repeat: repeat;
background-size: 10px;
}
.tooltip {
display: block;
background-color: white;
padding: 10px 15px ${isMobile ? 10 : 15}px 0px;
/* border-bottom: ${ isMobile ? 'none' : `2px solid ${colors.fade}`}; */
/* border-radius: ${ isMobile ? `${scaleEvents.bandwidth()/2}px` : '0px'}; */
/* box-shadow: 0 0 69px 20px rgba(0,0,0,0.1); */
}
.tooltip .headingContainer {
display: flex;
justify-content: space-between;
align-items: top;
}
.tooltip .heading {
font-family: "Formula 1", sans-serif;
font-size: 1.5em;
font-weight: 900;
margin-bottom: 5px;
}
.tooltip p {
margin: 0;
}
.message {
margin: 10px auto 0 auto;
display: flex;
gap: 5px;
align-items: top;
}

.versus {
margin: 5px auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 5px;
color: ${colors.fade};
font: 900 1em "Formula 1", sans-serif;
}

.divider {
width: 100%;
border-bottom: 2px solid ${colors.fade};
}

.spacer {
width: 100%;
height: 10px;
}

/* Styling the information cards for each team/driver */
.card {
padding: 10px 10px 10px 0;
border-right: 2px solid ${colors.fade};
font-family: sans-serif;
font-size: 0.8em;
position: relative;
}
.card .headingBorder {
width: max-content;
border-bottom: 2px solid;
}
.card .headingBorderCurve {
padding: 0 0 0 5px;
border-left: 5px solid;
border-radius: 0 0 0 8px;
}
.card .heading {
font-weight: normal;
font-family: "Formula 1", sans-serif;
font-size: 1.1em;
margin-bottom: 0px;
}
.card .subheading {
vertical-align: middle;
}
.card .subheading .label {
padding: 0.2em 0.6em;
font-size: 1em;
}

.card0 {
margin-top: 20px;
border-top: 2px solid ${colors.fade};
border-radius: 0 ${scaleEvents.bandwidth()}px 0 0;
}

.card1 {
border-bottom: 2px solid ${colors.fade};
border-radius: 0 0 ${scaleEvents.bandwidth()}px 0;
}

/* Styling the clear button inside the first card */
.clear {
font-family: sans-serif;
font-weight: 1.2em;
border: 2px solid #1b1e23;
border-radius: 4px;
height: max-content;
width: max-content;
padding: 2px;
position: absolute;
top: 10px;
right: 10px;
}
.clear:hover {
cursor: pointer;
}
.clear svg {
width: 15px;
height: 15px;
display: block;
}

/* Styling the buttons for the legend */
.label, .empty {
padding: 0em 0.6em;
border-radius: 1em;
font-family: sans-serif;
font-weight: bold;
white-space: nowrap;
color: #fff;
font-size: 0.8em;
}

.labelInteractive:hover {
cursor: pointer;
}

.empty {
color: black;
font-weight: normal;
border: 1px solid black;
}

.cell:hover {
cursor: pointer;
}

/* Styling other stuff */
.input {
width: max-content;
display: inline-block;
}

.formula1 {
font-family: "Formula 1", sans-serif;
font-weight: bold;
text-transform: none;
font-size: 0.9em;
color: ${colors.brand};
}
</style>
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
dateParser = d3.timeParse("%Y-%m-%d")
Insert cell
dateFormatter = function (date) {
const months = [
"Jan.",
"Feb.",
"March",
"April",
"May",
"June",
"July",
"Aug.",
"Sept.",
"Oct.",
"Nov.",
"Dec."
];
const monthIndex = +d3.timeFormat("%m")(date) - 1;
const day = +d3.timeFormat("%e")(date);
let remainingDate = "";

if (day < 9) {
remainingDate = d3.timeFormat(`${months[monthIndex]}%e`)(date);
} else {
remainingDate = d3.timeFormat(`${months[monthIndex]} %e`)(date);
}

return remainingDate + ", 2022";
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
dataRawArchive = {
{
const results2021 = await FileAttachment("2021@2.json").json();
const results2020 = await FileAttachment("2020@2.json").json();
const results2019 = await FileAttachment("2019@2.json").json();
const results2018 = await FileAttachment("2018@2.json").json();
const results2017 = await FileAttachment("2017@2.json").json();
const results2016 = await FileAttachment("2016@2.json").json();
const results2015 = await FileAttachment("2015@2.json").json();
const results2014 = await FileAttachment("2014@2.json").json();
const results2013 = await FileAttachment("2013@2.json").json();
const results2012 = await FileAttachment("2012@2.json").json();

const obj = {
2021: results2021,
2020: results2020,
2019: results2019,
2018: results2018,
2017: results2017,
2016: results2016,
2015: results2015,
2014: results2014,
2013: results2013,
2012: results2012
};

return obj;
}
}
Insert cell
Insert cell
// dataRawAPI = {
// const rounds = [];

// // const roundsCount = schedule["Races"].filter(
// // (f) => dateParser(f["date"]) < d3.timeDay.offset(Date.now(), -1)
// // ).length;

// const roundsCount = schedule["Races"].length;

// for (let i = 1; i <= roundsCount; i++) {
// const drivers = await d3.json(
// `https://ergast.com/api/f1/${year}/${i}/driverStandings.json`
// );
// const constructors = await d3.json(
// `https://ergast.com/api/f1/${year}/${i}/constructorStandings.json`
// );

// const driversTable =
// drivers["MRData"]["StandingsTable"]["StandingsLists"][0];
// const constructorsTable =
// constructors["MRData"]["StandingsTable"]["StandingsLists"][0];

// const obj = {
// season: drivers["MRData"]["StandingsTable"]["season"],
// round: drivers["MRData"]["StandingsTable"]["round"],
// DriverStandings: !driversTable ? [] : driversTable["DriverStandings"],
// ConstructorStandings: !constructorsTable
// ? []
// : constructorsTable["ConstructorStandings"]
// };

// rounds.push(obj);
// }

// return rounds.filter((f) => f["DriverStandings"].length > 0);
// }

Insert cell
dataRaw = {
if (year == 2022) {
const attachment = await FileAttachment("dataRawAPI@1.json").json();
return attachment;

// return dataRawAPI;
} else {
return dataRawArchive[year];
}
}
Insert cell
dataAllEvents = schedule["Races"].map((obj) => {
const d = { ...obj };

const standings = dataRaw.filter((f) => f["round"] == d["round"])[0];

const standingsProcessedDrivers =
!standings == false
? standings["DriverStandings"].map((e) => ({
driverId: e["Driver"]["driverId"],
constructorId: e["Constructors"][0]["constructorId"],
pointsTotal: +e["points"]
}))
: [];

const standingsProcessedConstructors =
!standings == false
? standings["ConstructorStandings"].map((e) => ({
constructorId: e["Constructor"]["constructorId"],
pointsTotal: +e["points"]
}))
: [];

d["StandingsDrivers"] = standingsProcessedDrivers;
d["StandingsConstructors"] = standingsProcessedConstructors;

return d;
})
Insert cell
dataPointsPerEvent = dataAllEvents
.map((obj, i, arr) => {
const d = { ...obj };

const pointsCalc = function (array, arrayName, idName) {
if (i > 0) {
return array.map((subobj, index) => {
const e = { ...subobj };
const pointsPrevious = arr[i - 1][arrayName].filter(
(f) => f[idName] == e[idName]
)[0];

e["standing"] = index + 1;

e["points"] = !pointsPrevious
? e["pointsTotal"]
: pointsPrevious["pointsTotal"] <= e["pointsTotal"]
? e["pointsTotal"] - pointsPrevious["pointsTotal"]
: e["pointsTotal"];

return e;
});
} else {
return array.map((subobj, index) => {
const e = { ...subobj };

e["standing"] = index + 1;

e["points"] = e["pointsTotal"];

return e;
});
}
};

if (
d["StandingsDrivers"].length > 0 &&
d["StandingsConstructors"].length > 0
) {
d["StandingsDrivers"] = pointsCalc(
d["StandingsDrivers"],
"StandingsDrivers",
"driverId"
);
d["StandingsConstructors"] = pointsCalc(
d["StandingsConstructors"],
"StandingsConstructors",
"constructorId"
);
}

return d;
})
.map((obj) => {
const d = { ...obj };

d["StandingsDrivers"].sort((a, b) =>
selector.sort == "total"
? d3.ascending(a["standing"], b["standing"])
: d3.descending(a["points"], b["points"])
);

d["StandingsConstructors"].sort((a, b) =>
selector.sort == "total"
? d3.ascending(a["standing"], b["standing"])
: d3.descending(a["points"], b["points"])
);

return d;
})
Insert cell
Insert cell
legendData = {
const array = [
{ label: "1", points: 1 },
// { label: "2", points: 2 },
{ label: "4", points: 4 },
// { label: "6", points: 6 },
{ label: "8", points: 8 },
// { label: "10", points: 10 },
{ label: "12 points", points: 12 }
// { label: "15", points: 15 },
// { label: "18", points: 18 },
// { label: "25", points: 25 }
];

const padding = 6;

return array
.sort((a, b) => d3.descending(a["points"], b["points"]))
.map((d, i, arr) => {
d["width"] = d["points"] == 0 ? 1 : d["points"];

if (i > 0) {
d["offset"] = d3.sum(arr.slice(0, i), (d) => d["width"]) + padding * i;
} else {
d["offset"] = 0;
}

return d;
})
.map((d, i, arr) => {
d["heightTotal"] = d3.sum(arr, (d) => d["width"] + d["offset"]);

return d;
});
}
Insert cell
Insert cell
Insert cell
dataLayout = {
const LayoutValues = function (standings) {
const layout = standings
.map((obj) => {
const d = { ...obj };
const width = d["points"] == 0 ? 1 : d["points"];
d["width"] = width;

return d;
})
.map((obj, i, arr) => {
const d = { ...obj };
d["offset"] = d3.sum(arr.slice(0, i), (e) => e["width"]);
d["padding"] = padding * i;
d["paddingTotal"] = padding * (arr.length - 1);

return d;
});

const widthTotal =
d3.sum(layout, (e) => e["width"]) + padding * (layout.length - 1);

const layoutWithPercent = layout.map((obj, i, arr) => {
const d = { ...obj };

d["widthPercent"] = (d["width"] / widthTotal) * 100;
d["paddingPercent"] = (padding / widthTotal) * i * 100;
d["offsetPercent"] = (d["offset"] / widthTotal) * 100;

return d;
});

return layoutWithPercent;
};

return dataPointsPerEvent.map((obj) => {
const x = { ...obj };

x["StandingsConstructors"] = LayoutValues(x["StandingsConstructors"]);
x["StandingsDrivers"] = LayoutValues(x["StandingsDrivers"]);

return x;
});
}
Insert cell
dataLayoutLineConstructors = [...constructors.keys()].map((d) => {
return dataLayout
.map((obj) => {
const e = { ...obj };

return e["StandingsConstructors"]
.map((x) => {
x["round"] = e["round"];
return x;
})
.filter((f) => f["constructorId"] == d)[0];
})
.filter((f) => !f == false);
})
Insert cell
dataLayoutLineDrivers = [...drivers.keys()].map((d) => {
return dataLayout
.map((obj) => {
const e = { ...obj };

return e["StandingsDrivers"]
.map((x) => {
x["round"] = e["round"];
return x;
})
.filter((f) => f["driverId"] == d)[0];
})
.filter((f) => !f == false);
})
Insert cell
line = d3.line().curve(curveCustom.x)
Insert cell
area = d3
.area()
.x((d) => scaleEvents(+d["round"]) + scaleEvents.bandwidth() / 2)
.y0((d) => scalePoints(d["offset"] + d["padding"]))
.y1((d) => scalePoints(d["offset"] + d["padding"] + d["width"]))
Insert cell
Insert cell
scaleEvents = d3
.scaleBand()
.domain(d3.range(1, schedule["Races"].length + 2))
.range([0, dms.chartWidth])
.paddingInner(0.6)
Insert cell
scalePercent = d3
.scaleLinear()
.domain([0, 100])
.range([0, dmsHeight.chartHeight])
Insert cell
scalePointsMax = d3.max(
dataLayout.map((d) => {
const standings =
selector.type == "Drivers"
? d["StandingsDrivers"]
: d["StandingsConstructors"];

return (
d3.sum(standings, (e) => e["width"]) +
(!standings[0] ? 0 : standings[0]["paddingTotal"])
);
})
)
Insert cell
scalePoints = d3
.scaleLinear()
.domain([0, scalePointsMax])
.range([0, dmsHeight.chartHeight])
Insert cell
Insert cell
mutable selected = []
Insert cell
select = function (id) {
if (selected.includes(id)) {
mutable selected = mutable selected.filter((f) => f != id);
} else {
mutable selected =
selected.length > 1 ? mutable selected.slice(0, 1) : mutable selected;
mutable selected.push(id);
}
}
Insert cell
clear = function (event, data) {
mutable selected = [];
}
Insert cell
format = function () {
if (selected.length > 0) {
d3.selectAll(".labelInteractive").style("opacity", 0.1);
d3.selectAll(".cell").attr("opacity", 0.1);
d3.selectAll(".line").attr("opacity", 0);

selected.forEach((d, i, arr) => {
d3.select(".labelInteractive" + d).style("opacity", 1);
d3.selectAll(".cell" + d).attr("opacity", 1);

i < arr.length - 1
? d3
.select(".line" + d)
.attr("opacity", 1)
.attr("stroke-dasharray", "1 3")
: d3
.select(".line" + d)
.attr("opacity", 1)
.attr("stroke-dasharray", "none")
.raise();
});
} else {
d3.selectAll(".labelInteractive").style("opacity", 1);
d3.selectAll(".cell").attr("opacity", 1);
d3.selectAll(".line").attr("opacity", 0).attr("stroke-dasharray", "none");
}
}
Insert cell
Insert cell
{
if (selector.year || selector.type) {
clear();
}

return selector;
}
Insert cell
Insert cell
tooltip = html`
<div class="tooltip">
<div class="headingContainer"><p class="heading">F1 ${year}</p></div>
${tooltipOverview}
${
selected.length > 0
? [...selected].map((d, i, arr) => tooltipSelection(d, i, arr))
: html`<div class="message"><p>👆</p><p><i>Select names to see more info and compare standings.</i></p></ div>`
}
</div>
`
Insert cell
tooltipOverview = {
const results = dataPointsPerEvent.filter(
(f) => f["StandingsConstructors"].length > 0
);
const resultsFirst =
selector.type == "Drivers"
? [...results[0]["StandingsDrivers"]].sort((a, b) =>
d3.ascending(a["standing"], b["standing"])
)
: [...results[0]["StandingsConstructors"]].sort((a, b) =>
d3.descending(a["pointsTotal"], b["pointsTotal"])
);
const resultsLatest =
selector.type == "Drivers"
? [...results[results.length - 1]["StandingsDrivers"]].sort((a, b) =>
d3.ascending(a["standing"], b["standing"])
)
: [...results[results.length - 1]["StandingsConstructors"]].sort((a, b) =>
d3.descending(a["pointsTotal"], b["pointsTotal"])
);

// return resultsLatest;

const labels = function (event) {
return event.map((obj, i, arr) => {
const d = { ...obj };

const color = colors.teams[d["constructorId"]].primary;

const id =
selector.type == "Drivers" ? d["driverId"] : d["constructorId"];
const name =
selector.type == "Drivers"
? drivers.get(d["driverId"])["familyName"]
: constructors.get(d["constructorId"])["name"];

const label = `<span dataId="${id}" class="label labelInteractive labelInteractive${id}" style="background-color:${color}; border:2px solid ${color}">${name}</span>`;

return i < arr.length - 1 ? label + " " : "and " + label;
});
};

if (results.length < schedule["Races"].length) {
return html`This year ${partyTime()} ${
labels(resultsLatest)[0]
} ${partyTime()} leads the championship, followed by ${labels(
resultsLatest.slice(1)
)} in last.`;
} else {
return html`${partyTime()} ${
labels(resultsLatest)[0]
} ${partyTime()} came out on top, followed by ${labels(
resultsLatest.slice(1)
)} in last.`;
}
}
Insert cell
tooltipSelection = function (id, index, array) {
let color = "";
let content = "";

if (selector.type == "Drivers") {
const info = drivers.get(id);
color = colors.teams[info["teams"][0]["constructorId"]].primary;
const story = dataLayout
.filter((f) => f["StandingsDrivers"].length > 0)
.map((obj, i) => {
const d = { ...obj };

d["DriverStats"] = d["StandingsDrivers"].filter(
(f, i) => f["driverId"] == id
)[0];

return d;
});

content = `
<p class="heading">${info["givenName"]} ${info["familyName"]}</p>
<div class="headingBorder headingBorderCurve" style="border-color: ${color}">
<p class="subheading">
${
info["nationality"]
} • <span style="color: ${color}; font-weight: bold">${
info["teams"][0]["name"]
}</span>
</p>
</div>
<div class="spacer"></div>
<p><b style="color:${color}">${formatPosition(
story[story.length - 1]["DriverStats"]["standing"],
true
)}</b> in driver's championship, <b style="color:${color}">${
story[story.length - 1]["DriverStats"]["pointsTotal"]
}</b> total points${
info["driverId"] == "sainz"
? ' for the <a href="https://www.youtube.com/watch?v=mGkrbzJZCoE&ab_channel=JayLadva" target="_blank">smooth operator<a>'
: ""
}</p>
`;
} else {
const info = constructors.get(id);
const drivers = info["drivers"].sort((a, b) =>
d3.ascending(a["familyName"], b["familyName"])
);

color = colors.teams[info["constructorId"]].primary;
const story = dataLayout
.filter((f) => f["StandingsConstructors"].length > 0)
.map((obj, i) => {
const d = { ...obj };

let standing = 0;
let position = 0;

d["ConstructorStats"] = d["StandingsConstructors"].filter(
(f, i) => f["constructorId"] == id
)[0];

return d;
});

content = `
<div class="" style="border-color: ${color}">
<p class="heading">${info["name"]}</p>
</div>
<div class="headingBorder headingBorderCurve" style="border-color: ${color}">
<p>${drivers[0]["familyName"]} & ${drivers[1]["familyName"]}</p>
</div>
<div class="spacer"></div>
<p><b style="color:${color}">${formatPosition(
story[story.length - 1]["ConstructorStats"]["standing"],
true
)}</b> in constructor's championship, <b style="color:${color}">${
story[story.length - 1]["ConstructorStats"]["pointsTotal"]
}</b> total points</p>
`;
}

if (index == 0) {
return html`
<div class="card card0" style="border-color: ${color}; border-style: ${
array.length > 1 ? "dotted dotted none none" : ""
}">
<div class="clear">${symbolX}</div>
${content}
</div>`;
} else if (index == 1) {
return html`
<div class="versus">
<div class="divider"></div><span>V.S.</span><div class="divider"></div>
</div>
<div class="card card1" style="border-color: ${color}">${content}</div>
`;
}
}
Insert cell
Insert cell
Insert cell
curveCustom = {
class Bump {
constructor(context, c, direction) {
this._context = context;
this._c = c;
this._direction = direction;
}
areaStart() {}
areaEnd() {}
lineStart() {
this._point = 0;
}
lineEnd() {}
point(x, y) {
(x = +x), (y = +y);

const x0 = this._x0;
const y0 = this._y0;

if (this._direction == "y") {
const c = this._c;
const s = scaleEvents.bandwidth() / 2;
const h = (y - y0 - s * 2) / 2;
const f =
Math.abs(x - x0) > h * 2 ? (x - x0 > 0 ? 1 : -1) : (x - x0) / (h * 2);

switch (this._point) {
case 0: {
this._point = 1;
this._context.moveTo(x, y);
break;
}
case 1:
this._point = 2;
// I was wondering why the above falls through,
// cause of @fil's comment. It's because there's
// no break! Learning every day.
default:
this._context.lineTo(x0, y0 + s);

this._context.bezierCurveTo(
x0,
y0 + s + h * c,
x0 + h * f - h * f * c,
y0 + s + h,
x0 + h * f,
y0 + s + h
);

this._context.lineTo(x - h * f, y - s - h);

this._context.bezierCurveTo(
x - h * f + h * f * c,
y - s - h,
x,
y - s - h * c,
x,
y - s
);

this._context.lineTo(x, y);
}
} else if (this._direction == "x") {
const c = this._c;
const s = scaleEvents.bandwidth() / 2;
const h = (x - x0 - s * 2) / 2;
const f =
Math.abs(y - y0) > h * 2 ? (y - y0 > 0 ? 1 : -1) : (y - y0) / (h * 2);

switch (this._point) {
case 0: {
this._point = 1;
this._context.moveTo(x, y);
break;
}
case 1:
this._point = 2;
// I was wondering why the above falls through,
// cause of @fil's comment. It's because there's
// no break! Learning every day.
default:
this._context.lineTo(x0 + s, y0);

this._context.bezierCurveTo(
x0 + s + h * c,
y0,
x0 + s + h,
y0 + h * f - h * f * c,
x0 + s + h,
y0 + h * f
);

this._context.lineTo(x - s - h, y - h * f);

this._context.bezierCurveTo(
x - s - h,
y - h * f + h * f * c,
x - s - h * c,
y,
x - s,
y
);

this._context.lineTo(x, y);
}
} else {
throw "Invalid direction";
}

(this._x0 = x), (this._y0 = y);
}
}

return {
x: function (context) {
return new Bump(context, curveFactor, "x");
},
y: function (context) {
return new Bump(context, curveFactor, "y");
}
};
}
Insert cell
Insert cell
partyTime = function () {
const emojis = ["🏁", "🍾", "🎊", "💸", "🎶", "🎆", "💃", "🎉", "💰", "🏆"];

return emojis[Math.round(Math.random() * (emojis.length - 1))];
}
Insert cell
hash = function (defs, id, color) {
return defs
.append("pattern")
.attr("id", id)
.attr("width", "4")
.attr("height", "4")
.attr("patternUnits", "userSpaceOnUse")
.attr(
"patternTransform",
`rotate(45) scale(${scaleEvents.bandwidth() / 4 / 2})`
)
.append("path")
.attr("d", "M 0 0 V 4")
.attr("stroke-width", 2.5)
.attr("transform", "translate(0,0)")
.attr("stroke", color);
}
Insert cell
formatPosition = function (number, isHtml) {
const string = String(number);
let stringFormatted = "";

if (
string.substring(string.length - 1, string.length) == 1 &&
string != "11"
) {
stringFormatted = number + (isHtml ? "<sup>st</sup>" : "st");
} else if (
string.substring(string.length - 1, string.length) == 2 &&
string != "12"
) {
stringFormatted = number + (isHtml ? "<sup>nd</sup>" : "nd");
} else if (
string.substring(string.length - 1, string.length) == 3 &&
string != "13"
) {
stringFormatted = number + (isHtml ? "<sup>rd</sup>" : "rd");
} else {
stringFormatted = number + (isHtml ? "<sup>th</sup>" : "th");
}

return stringFormatted;
}
Insert cell
Insert cell
f1Party = function (text) {
// const colorsArray = Object.values(colors.teams).map((d) => d["primary"]);
const colorsArray = [...constructors.keys()].map(
(d) => colors.teams[d].primary
);

// The Fisher-Yates Shuffle
function shuffle(array) {
let m = array.length,
t,
i;

while (m) {
i = Math.floor(Math.random() * m--);
t = array[m];
array[m] = array[i];
array[i] = t;
}

return array;
}

const colorsArrayShuffled = shuffle(colorsArray);

const textModified = text
.split("")
.map((d, i) => {
const colorIndex =
i >= colorsArrayShuffled.length - 1
? (i % colorsArrayShuffled.length) - 1
: i;
return `<span style="color:${colorsArrayShuffled[colorIndex]}">${d}</span>`;
})
.join("");

return `<span class="formula1">${textModified}</span>`;
}
Insert cell
symbolX = html`
<svg viewBox="0 0 20 20">
<path d="M3,3L17,17" stroke="#1b1e23" stroke-width="3" stroke-linecap="round" fill="none"></path>
<path d="M17,3L3,17" stroke="#1b1e23" stroke-width="3" stroke-linecap="round" fill="none"></path>
</svg>
`
Insert cell
Insert cell
chartStatic = {
// This block of code sets up an SVG container and
// elements of the graphic that don't require updating
// or changing. By separating out these elements from
// the dynamic ones, only elements that need to be drawn
// again are updated and the notebook is more efficient.

const svg = d3
.create("svg")
.attr("viewBox", [0, 0, dms.width, dmsHeight.height])
.attr("width", dms.width)
.attr("height", dmsHeight.height);

const defs = svg.append("defs");

defs.selectAll("pattern").remove();

hash(defs, "hash", colors.fade);
hash(defs, "hashBlack", "black");

svg
.append("g")
.attr("class", "detection")
.append("rect")
.attr("width", dms.width)
.attr("height", dmsHeight.height)
.attr("opacity", 0)
.on("pointerdown", (event, data) => (isMobile ? null : clear()));

const chart = svg
.append("g")
.attr("transform", `translate(${dms.marginLeft} ${dms.marginTop})`)
.attr("class", "chart");

chart.append("g").attr("class", "lines");

chart.append("g").attr("class", "events");

chart.append("g").attr("class", "letters");

chart.append("g").attr("class", "flags");

chart.append("g").attr("class", "legend");

chart
.append("g")
.attr("class", "axis axisY")
.append("path")
.attr("class", "dividingLine");

return svg;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more