Public
Edited
Oct 19, 2024
Insert cell
Insert cell
Insert cell
Insert cell
{
const ordering_1 = "weekStreak";
const ordering_2 = "gameCountLast30days";

const image_size = 30;
const dx_image = image_size * 0.4;

const bad = d3.schemeTableau10[2];
const good = d3.schemeTableau10[4];
const streakcolor = d3.schemeTableau10[4];

const imagetip_fun = (d) => {
const streakLine =
d.weekStreak > 0
? `Current ${d.weekStreak} week streak`
: `Not active this week :(`;
const matchesLine =
d.gameCountLast30days > 0
? `${d.gameCountLast30days} matches played in the last 30 days`
: `NO matches played in the last 30 days. Terrible`;

return `${d.profile}\n${streakLine}\n${matchesLine}`;
};

const gametip_fun = (d) => {
//{
// "id": "0278bb17-d736-4b1a-9bac-a5de46b1c580",
// "profile": "ilanvale",
// "gamedate": "2024-04-18",
// "weeksAgo": 2,
// "numplayers": 1,
// "group": [
// "ilanvale"
// ],
// "matchResult": "win",
// "overallPerformance": -0.1245,
// "personalPerformance": -0.015066666666666673,
// "map": "de_inferno",
// "teamplay": true
// }
// console.log(d);
return `${d.profile}\n${d.gamedate}\n${d.weeksAgo}`;
};

const x_domain = [today, d3.utcDay.offset(today, -timerange)];
const fy_domain = [
teamname,
...d3
.sort(
profiledata,
(a, b) =>
d3.descending(a[ordering_1], b[ordering_1]) ||
d3.descending(a[ordering_2], b[ordering_2])
)
.map((d) => d.profile)
];

const color_config = {
"Week Streak": {
domain: ["team", "offstreak", "onstreak"],
range: [d3.schemeTableau10[6], "darkgrey", streakcolor],
fun: (d) =>
d.profile == teamname
? "team"
: d.weeksAgo <=
profiledata.find((f) => f.profile == d.profile)?.weekStreak
? "onstreak"
: "offstreak"
},
"Win/Loss": {
type: "categorical",
domain: ["win", "loss", "tie"],
range: [
d3.schemeTableau10[4],
d3.schemeTableau10[2],
d3.schemeTableau10[0]
],
unknown: "darkgrey",
fun: "matchResult"
},
"Overall Perf.": {
type: "diverging",
scheme: "RdYlGn",
fun: "overallPerformance"
// legend: true
},
"Personal Perf.": {
type: "diverging",
scheme: "RdYlGn",
fun: "personalPerformance"
// legend: true
},
Map: {
domain: [
"de_dust2",
"de_overpass",
"de_inferno",
"de_mirage",
"cs_office",
"de_anubis",
"de_ancient"
],
range: [
d3.schemePastel1[4], // "de_dust2",
d3.schemeCategory10[1], // "de_overpass"
d3.schemeCategory10[3], // "de_inferno"
d3.schemeCategory10[5], // "de_mirage"
d3.schemeCategory10[0], // "cs_office"
d3.schemePaired[0], // "de_anubis"
d3.schemeCategory10[2] // "de_ancient"
],
unknown: "darkgrey",
fun: "map"
}
};
const color_range = ["team", "offstreak", "onstreak"];
const color_domain = ["navy", "darkgrey", streakcolor];

const weekLabelFilterFun = (d) => {
const lastWeekOnStreak =
d.weeksAgo + 1 ==
profiledata.find((f) => f.profile == d.profile)?.weekStreak;
const weekInsideStreak =
d.weeksAgo < profiledata.find((f) => f.profile == d.profile)?.weekStreak;
const firstProfile = fy_domain[0] == d.profile;

return lastWeekOnStreak || (weekInsideStreak && firstProfile);
};

const graph = Plot.plot({
width: width,
height: 700,
marginLeft: 150,
insetLeft: 50,
x: {
grid: true,
ticks: "weeks",
domain: x_domain,
type: "utc",
// label: "Date (reversed)",
// labelOffset: -5,
axis: "top"
},
fy: {
marginTop: 25,
label: null,
domain: fy_domain,
tickSize: 0
},
color: {
...color_config[coloring]
},
marks: [
// Profile image
Plot.image(
profiledata,
Plot.selectFirst({
fy: "profile",
frameAnchor: "left",
src: "steamavatar",
dx: dx_image,
height: image_size
})
),
Plot.tickX(d3.unixDay.range(x_domain[1], x_domain[0], 1), {
strokeOpacity: 0.05,
filter: (f) => d3.timeDay.count(f, today) % 7 > 0
}),
// Gameday dots
Plot.dot(
allgamesdata.filter((f) => f.teamplay),
Plot.dodgeY({
x: "gamedate",
r: 3,
fy: "profile",
fill: color_config[coloring].fun,
// dx: -5, // This is to fit better inside the weeks
// dy: -7
anchor: "middle",
tip: true,
title: gametip_fun
})
),
// Non team gameplay
Plot.dot(
allgamesdata.filter((f) => !f.teamplay),
Plot.dodgeY({
x: "gamedate",
r: 3,
fy: "profile",
strokeOpacity: 0.2,
anchor: "middle"
})
),
// Weeks streak text
// Plot.text(
// daysdata,
// // Plot.selectFirst(
// Plot.binX(
// {
// interval: "week",
// text: "first",
// profile: "first"
// },
// {
// x: "date",
// fy: "profile",
// filter: weekLabelFilterFun,
// text: (d) =>
// d.profile == fy_domain[0] &&
// d.weeksAgo + 1 ==
// profiledata.find((f) => f.profile == d.profile)?.weekStreak
// ? `${d.weeksAgo + 1} weeks streak`
// : `${d.weeksAgo + 1}`,
// fontWeight: "bold",
// fill: streakcolor,
// textAnchor: "start",
// fontStyle: "italic",
// dx: -2,
// dy: -18
// }
// )
// ),
// Plot.text(
// // Dirty juggling here
// [
// fy_domain
// .slice(1)
// .map((fy) => ({
// ...profiledata.find((p) => p.profile == fy)
// }))
// .filter((f) => f.weekStreak == 0)[0]
// ],
// {
// frameanchor: "left",
// fy: (d) =>
// d3.max(profiledata, (d) => d.weekStreak) == 0
// ? teamname
// : d.profile,
// x: (d) => x_domain[0],
// text: (d) => "No week\nstreak :'(",
// fontWeight: "bold",
// fill: "darkgrey",
// textAnchor: "start",
// fontStyle: "italic",
// dx: 4
// // dy: -18
// }
// ),
// Today marker
Plot.dot([0], {
frameAnchor: "top-left",
fy: (d) => fy_domain[0],
height: 2,
x: (d) => x_domain[0],
symbol: "star",
r: 2,
dy: -3
}),
Plot.text([0], {
frameAnchor: "top-left",
fy: (d) => fy_domain[0],
height: 2,
x: (d) => x_domain[0],
text: (d) => (todayIsSelected ? "Today" : "Ref Date"),
lineAnchor: "bottom",
textAnchor: "middle",
fontStyle: "italic",
dy: -13
}),
// Tips
Plot.tip(
profiledata,
Plot.pointer(
Plot.selectFirst({
fy: "profile",
frameAnchor: "left",
anchor: "bottom-left",
dx: dx_image + image_size / 2 + 5,
dy: 0, //-image_size / 4,
title: imagetip_fun
})
)
)
]
});

return html`${graph}${Plot.legend({
color: { ...color_config[coloring], label: coloring }
})}`;
}
Insert cell
test = ({
a: 1,
b: 2,
c: 3,
d: { a: 1, b: 2, c: 3 }
})
Insert cell
{
const al = ["a", "c", "d"];
const al2 = ["a", "b"];
const fun = (z) =>
Object.assign({}, ...al.map((a) => ({ [a]: z[a] })), {
d: Object.assign({}, ...al2.map((a) => ({ [a]: z.d[a] })))
});

return fun(test);
}
Insert cell
Insert cell
infofordebuggingZip = FileAttachment("infoForDebugging.json.zip").zip()
Insert cell
Insert cell
function overviewFeatures(
profileBaseWithMatches,
{ refDate, sessionDateColumn }
) {
const sessionDate = ({ games }) => ({
games: games.map((g) => ({
...g,
sessionDate: dateFixer(new Date(g[sessionDateColumn]))
}))
});
const weekStreak = ({ games }) => ({
weekStreak: getWeekStreak(
games.map((g) => g.sessionDate),
refDate
)
});
const weeksAgo = ({ games }) => ({
games: games.map((g) => ({
...g,
weeksAgo: getWeeksAgo(g.sessionDate, refDate) + 1
}))
});
const gameCountLast5weeks = ({ games }) => ({
gameCountLast5weeks: games.filter((g) => g.weeksAgo <= 5).length
});

const features = [sessionDate, weekStreak, weeksAgo, gameCountLast5weeks];

return features.reduce(
(profilesDF, fun) =>
profilesDF.map((profile) => ({ ...profile, ...fun(profile) })),
profileBaseWithMatches
);
}
Insert cell
Insert cell
overview = {
const gprofiledata = infofordebuggingJson.profileBaseWithMatches;

const ordering_1 = "weekStreak";
const ordering_2 = "gameCountLast5weeks";

const params = {
ordering_1,
ordering_2,
width: width,
height: 1000,
marginLeft: 150,
insetLeft: 50,
refDate: today,
maxWeeksAgo,
timerange: maxWeeksAgo * 7,
sessionDateColumn: "finishedAt"
};

const overviewData = overviewFeatures(gprofiledata, params);
const overviewPlot = overviewGraph(overviewData, params);

// return overviewData;
return overviewPlot;
}
Insert cell
function overviewGraph(
overviewData,
{
ordering_1,
ordering_2,
width,
height,
marginLeft,
insetLeft,
refDate,
timerange
}
) {
const fy_domain = [
teamname,
...d3
.sort(
overviewData,
(a, b) =>
d3.descending(a[ordering_1], b[ordering_1]) ||
d3.descending(a[ordering_2], b[ordering_2])
)
.map((d) => d.name)
];

const x_domain = [refDate, d3.utcDay.offset(refDate, -timerange)];

const plot = Plot.plot({
width,
height,
marginLeft,
insetLeft,
x: {
type: "utc",
axis: "top",
grid: true,
domain: x_domain
},
fy: {
label: null,
domain: fy_domain
},
marks: [
Plot.dot(
overviewData
.map(
(profile) =>
profile.games.map((g) => ({ ...g, name: profile.name }))
// .filter(g => isDateWithinRange(g.finishedAt, ))
)
.flat(),
Plot.dodgeY({
fy: "name",
x: "finishedAt"
})
)
]
});

return plot;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
raw_profiledata = rawdata
// First, handling multi profiles (filtering mirrors and adding games to main profile)
.filter((f) => !Object.keys(multi_profiles).includes(f.meta.name))
.map((d) => ({
...d,
games: [
d.games,
...Object.entries(multi_profiles)
.filter((f) => f[1] == d.meta.name)
.map((d) => rawdata.find((f) => f.meta.name == d[0]).games)
]
.flat()
.sort((a, b) =>
a.gameFinishedAt == b.gameFinishedAt
? 0
: a.gameFinishedAt < b.gameFinishedAt
? 1
: -1
)
}))
// Now the base features
.map((d) => ({
profile: d.meta.name,
steamId: d.meta.steam64Id,
totalgamecount: d.games.length,
recentgames: d.recentGameRatings.gamesPlayed,
steamavatar: d.meta.steamAvatarUrl,
games: d.games.map((g) => ({
date_orig: new Date(g.gameFinishedAt),
date: new Date(
`${g.gameFinishedAt.slice(0, -1)}+${d3.format("02")(hours_offset)}:00`
),
id: g.gameId,
...g
})),
gamesDates_orig: d.games.map((g) => new Date(g.gameFinishedAt)),
gamesDates: d.games.map(
(g) =>
new Date(
`${g.gameFinishedAt.slice(0, -1)}+${d3.format("02")(hours_offset)}:00`
)
),
gameIDs: d.games.map((g) => g.gameId)
}))
// Now calculated, so on and so forth
.map((d) => ({
...d,
lastGame: d.gamesDates[0],
gamesLast30days_orig: d.gamesDates_orig.filter(
(f) => d3.timeDay.count(f, new Date()) <= 30
),
gamesLast30days: d.gamesDates.filter(
(f) => d3.timeDay.count(f, new Date()) <= 30
),
weekStreak: getWeekStreak(d.gamesDates)
}))
.map((d) => ({
...d,
daysSinceLastGame: d3.timeDay.count(d.lastGame, new Date()),
gameCountLast30days: d.gamesLast30days.length,
personalPerfAverage: d3.mean(
d.games
.filter(
(f) =>
isDateWithinRange(
f.date,
d3.utcDay.offset(today, -timerange),
today
) && typeof f.ownTeamTotalLeetifyRatings[d.steamId] != "undefined"
)
.map((ig) => ig.ownTeamTotalLeetifyRatings[d.steamId])
)
}))
Insert cell
profiledata = raw_profiledata.map((d) => ({
...d
}))
Insert cell
raw_allgamesdata = raw_profiledata
.map((p) => [
...p.games.map((g) => ({
id: g.id,
profile: p.profile,
gamedate: d3.timeFormat("%Y-%m-%d")(g.date),
weeksAgo: getWeeksAgo(g.date),
numplayers: 1,
group: [p.profile],
matchResult: g.matchResult,
overallPerformance: g.ownTeamTotalLeetifyRatings[p.steamId],
personalPerformance:
g.ownTeamTotalLeetifyRatings[p.steamId] - p.personalPerfAverage,
map: g.mapName
}))
])
.flat()
.filter((f) => new Date(f.gamedate) <= new Date(today))
Insert cell
allgamesdata = [
...teamgamesdata,
...raw_allgamesdata.map((g) => ({
...g,
teamplay: teamgamesdata.map((tg) => tg.id).includes(g.id)
}))
]
Insert cell
teamgamesdata = {
const games = d3
.flatRollup(
raw_allgamesdata,
(v) => ({
profile: teamname,
gamedate: v[0].gamedate,
weeksAgo: getWeeksAgo(v[0].gamedate),
numplayers: v.length,
group: v.map((g) => g.profile),
teamplay: true,
matchResult: v[0].matchResult,
personalPerformance: d3.mean(v, (p) => p.performance),
overallPerformance: d3.mean(v, (p) => p.performance),
map: v[0].map
}),
(d) => d.id
)
.map((d) => ({
id: d[0],
...d[1]
}));

return games.filter((f) => f.numplayers > 1).filter((f) => f.id.length == 36);
}
Insert cell
Insert cell
Insert cell
[1, 2, 3, 4].slice(1)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function getWeekStreak(gameDates, refDate) {
const weeksAgoArray = gameDates
.filter((f) => f <= today)
.map((g) => getWeeksAgo(g, refDate));
const currentStreak =
weeksAgoArray.reduce((acc, cur) => (cur - acc <= 1 ? cur : acc), -1) + 1;

return currentStreak;
}
Insert cell
getWeeksAgo = (date, refDate) =>
// Math.floor(d3.timeDay.count(new Date(date), d3.utcMonday.floor(today)) / 7)
d3.timeWeek.count(date, d3.utcMonday.floor(refDate))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
multMatchTest = fetchMatches(multMatchInputTest)
Insert cell
Insert cell
Insert cell
async function fetchMatchDetails(matchId) {
const url = `https://api.leetify.com/api/games/${matchId}`;
// debugger;
try {
const response = await fetch(url);
const data = await response.json();
return data; // Return the fetched data
} catch (error) {
console.error("Error fetching match details:", matchId, error);
throw error; // Throw an error if something goes wrong
}
}
Insert cell
async function fetchMatches(matches) {
try {
// Map over profiles and create a fetch promise for each
const promises = matches.map((matchId) => fetchMatchDetails(matchId));

// Wait for all promises to resolve
const results = await Promise.all(promises);
return results; // Return the array of results
} catch (error) {
console.error("Error fetching matches:", error);
throw error; // Handle errors appropriately
}
}
Insert cell
{
const t1 = [0, 1, 2, 3, 5, 6, 7, 8];
const t2 = [0, 1, 2, 3, 4, 5, 6, 7, 8];

return t1.reduce(
(acc, s, i) => {
console.log(s, acc, i);
return s == t2[i] && acc.onStreak
? { current: acc.current + 1, onStreak: true }
: { current: acc.current, onStreak: false };
},
{ current: 0, onStreak: true }
);
}
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