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

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