Files
applications-tracker/site/src/routes/graphs/+page.svelte

439 lines
11 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { applicationStore } from '$lib/ApplicationsStore.svelte';
import HasUser from '$lib/HasUser.svelte';
import { onMount } from 'svelte';
import NavBar from '../NavBar.svelte';
import Pie from './Pie.svelte';
import { statusStore } from '$lib/Types.svelte';
import { get } from '$lib/utils';
import PayRange from './PayRange.svelte';
import LineGraphs, { type LineGraphData } from './LineGraph.svelte';
import * as d3 from 'd3';
import { countReducer } from './utils';
onMount(() => {
applicationStore.loadAll();
statusStore.load();
});
let flairStats: 'loading' | Record<string, number> = $state('loading');
let createGraph: { xs: number[]; ys: number[] } = $state({ xs: [], ys: [] });
let viewGraph: { xs: number[]; ys: number[] } = $state({ xs: [], ys: [] });
let statusGraph: LineGraphData = $state([]);
let width: number = $state(300);
onMount(async () => {
(async () => {
const items: any[] = await get('flair/stats');
flairStats = items.reduce(
(acc, a) => {
acc[a.name] = a.count;
return acc;
},
{} as Record<string, number>
);
})();
(async () => {
type EventsStat = {
// application_id
a: string;
// created time
c: number;
// id
i: string;
// new_status_id
s?: string;
// Type
t: number;
};
const events: EventsStat[] = await get('events/');
const pre_created_graph = events
.filter((a) => a.t === 0)
.reduce(countReducer, {} as Record<number | string, number>);
const pre_created_graph_xs = Object.keys(pre_created_graph);
createGraph = {
xs: pre_created_graph_xs.map((a) => Number(a)),
ys: pre_created_graph_xs.map((a) => pre_created_graph[a])
};
const pre_views_graph = events
.filter((a) => a.t === 2)
.reduce(countReducer, {} as Record<number | string, number>);
const pre_view_graph_xs = Object.keys(pre_views_graph);
viewGraph = {
xs: pre_view_graph_xs.map((a) => Number(a)),
ys: pre_view_graph_xs.map((a) => pre_views_graph[a])
};
statusGraph = statusStore.nodes
.map((a, i) => {
const pre = events
.filter((e) => e.t === 1 && e.s === a.id)
.reduce(countReducer, {} as Record<number | string, number>);
const pre_xs = Object.keys(pre);
if (pre_xs.length === 0) return undefined;
return {
name: a.name,
color: d3.interpolateRainbow(i / statusStore.nodes.length),
xs: pre_xs.map((a) => Number(a)),
ys: pre_xs.map((a) => pre[a])
};
})
.filter((a) => a) as LineGraphData;
})();
});
const seniorities = ['intern', 'entry', 'junior', 'mid', 'senior', 'staff', 'lead'];
</script>
<HasUser redirect="/cv">
<div class="flex flex-col h-[100vh]">
<NavBar />
<div class="p-1 px-5">
<div class="bg-white p-3 rounded-lg gap-5 flex flex-col">
<div class="flex gap-5 flex-wrap">
<Pie
title={'Origin'}
data={applicationStore.all.reduce(
(acc, item) => {
if (item.url.includes('linkedin')) {
acc.linkedin += 1;
} else if (item.url.includes('glassdoor')) {
acc.glassdoor += 1;
} else {
acc.other += 1;
}
return acc;
},
{
linkedin: 0,
glassdoor: 0,
other: 0
}
)}
/>
<Pie
title={'Status'}
data={[...statusStore.nodes, 'Created' as const].reduce(
(acc, item) => {
if (item === 'Created') {
const count = applicationStore.all.filter(
(a) => a.status_id === null
).length;
if (count === 0) return acc;
acc[item] = count;
return acc;
}
const count = applicationStore.all.filter(
(a) => item.id === a.status_id
).length;
if (count === 0) return acc;
acc[item.name] = count;
return acc;
},
{} as Record<string, number>
)}
/>
<Pie
title={'Higher range Pay Range'}
data={applicationStore.all
.filter((a) => a.payrange.match(/\d/))
.map((a) => {
const payrange = a.payrange
.replace(/[kK]/g, '000')
.replace(/[^\d\-]/g, '')
.replace(//g, '-')
.split('-');
return Number(payrange[payrange.length - 1]);
})
.reduce(
(acc, a) => {
const f = Math.floor(a / 10000);
let name = `${f * 10}K-${(f + 1) * 10}K`;
if (f == 0) {
name = '<10K';
}
if (acc[name]) {
acc[name] += 1;
} else {
acc[name] = 1;
}
return acc;
},
{} as Record<string, number>
)}
/>
<Pie
title={'Lower range Pay Range'}
data={applicationStore.all
.filter((a) => a.payrange.match(/\d/))
.map((a) => {
const payrange = a.payrange
.replace(/[kK]/g, '000')
// The first is a - the other is unicode 8211
.replace(/[^\d\-]/g, '')
.replace(//g, '-')
.split('-');
return Number(payrange[0]) + Number(payrange[1] ?? payrange[0]);
})
.reduce(
(acc, a) => {
const f = Math.floor(a / 10000);
let name = `${f * 10}K-${(f + 1) * 10}K`;
if (f == 0) {
name = '<10K';
}
if (acc[name]) {
acc[name] += 1;
} else {
acc[name] = 1;
}
return acc;
},
{} as Record<string, number>
)}
/>
<Pie
title={'Company'}
sensitivity={0.01}
data={applicationStore.all.reduce(
(acc, item) => {
if (acc[item.company]) {
acc[item.company] += 1;
} else {
acc[item.company] = 1;
}
return acc;
},
{} as Record<string, number>
)}
/>
<Pie
title={'Location'}
sensitivity={0.01}
data={applicationStore.all.reduce(
(acc, item) => {
const l = item.location === '' ? 'Unknown' : item.location;
if (acc[l]) {
acc[l] += 1;
} else {
acc[l] = 1;
}
return acc;
},
{} as Record<string, number>
)}
/>
<Pie
title={'In Person type'}
sensitivity={0.01}
data={applicationStore.all.reduce(
(acc, item) => {
const l =
item.inperson_type === '' ? 'Unknown' : item.inperson_type;
if (acc[l]) {
acc[l] += 1;
} else {
acc[l] = 1;
}
return acc;
},
{} as Record<string, number>
)}
/>
<Pie
title={'Job Level'}
data={applicationStore.all.reduce(
(acc, a) => {
const job_level = a.job_level ? a.job_level : 'Unknown';
if (acc[job_level]) {
acc[job_level] += 1;
} else {
acc[job_level] = 1;
}
return acc;
},
{} as Record<string, number>
)}
sensitivity={0.02}
/>
<Pie
title={'Agency'}
data={applicationStore.all.reduce(
(acc, a) => {
const i = a.agency ? 'Agency' : 'Direct';
if (acc[i]) {
acc[i] += 1;
} else {
acc[i] = 1;
}
return acc;
},
{} as Record<string, number>
)}
sensitivity={0.02}
/>
</div>
{#if flairStats !== 'loading'}
<div class="flex gap-5">
<Pie title={'Flair stats'} data={flairStats} sensitivity={0.02} />
<div class="max-h-[500px] overflow-auto">
<ul>
{#each Object.keys(flairStats).toSorted((a, b) => flairStats[b] - flairStats[a]) as flair}
<li>{flair}: {flairStats[flair]}</li>
{/each}
</ul>
</div>
</div>
{/if}
<div bind:clientWidth={width}>
<LineGraphs
data={[
{ name: 'Created Time', color: 'red', ...createGraph },
{ name: 'Views', color: 'blue', ...viewGraph }
]}
xIsDates
width={width - 50}
height={300}
/>
</div>
<div bind:clientWidth={width}>
<h1>Status Graph</h1>
<LineGraphs data={statusGraph} xIsDates width={width - 50} height={300} />
</div>
<h1>Payrange</h1>
<PayRange />
<div>
<h1>Per Seniority</h1>
{#each seniorities as level}
{@const fapps = applicationStore.all.filter(
(a) => a.payrange.match(/\d/) && a.job_level === level
)}
<h2 class="font-bold text-lg">
{level} (AVG pay: {(
fapps
.map((a) => {
const payrange = a.payrange
.replace(/[kK]/g, '000')
.replace(/[^\d\-]/g, '')
.replace(//g, '-')
.split('-');
return (
Number(payrange[0]) + Number(payrange[1] ?? payrange[0])
);
})
.reduce((acc, a) => acc + a, 0) /
(fapps.length * 2)
).toLocaleString('en-GB', {
notation: 'compact',
style: 'currency',
currency: 'GBP'
})})
</h2>
<div class="flex gap-2">
<Pie
title={'Higher range Pay Range'}
data={fapps
.map((a) => {
const payrange = a.payrange
.replace(/[kK]/g, '000')
.replace(/[^\d\-]/g, '')
.replace(//g, '-')
.split('-');
return Number(payrange[payrange.length - 1]);
})
.reduce(
(acc, a) => {
const f = Math.floor(a / 10000);
let name = `${f * 10}K-${(f + 1) * 10}K`;
if (f == 0) {
name = '<10K';
}
if (acc[name]) {
acc[name] += 1;
} else {
acc[name] = 1;
}
return acc;
},
{} as Record<string, number>
)}
/>
<Pie
title={'Lower range Pay Range'}
data={fapps
.map((a) => {
const payrange = a.payrange
.replace(/[kK]/g, '000')
// The first is a - the other is unicode 8211
.replace(/[^\d\-]/g, '')
.replace(//g, '-')
.split('-');
return Number(payrange[0]);
})
.reduce(
(acc, a) => {
const f = Math.floor(a / 10000);
let name = `${f * 10}K-${(f + 1) * 10}K`;
if (f == 0) {
name = '<10K';
}
if (acc[name]) {
acc[name] += 1;
} else {
acc[name] = 1;
}
return acc;
},
{} as Record<string, number>
)}
/>
<Pie
title={'AVG Pay'}
data={fapps
.map((a) => {
const payrange = a.payrange
.replace(/[kK]/g, '000')
// The first is a - the other is unicode 8211
.replace(/[^\d\-]/g, '')
.replace(//g, '-')
.split('-');
return (
(Number(payrange[0]) +
Number(payrange[1] ?? payrange[0])) /
2
);
})
.reduce(
(acc, a) => {
const f = Math.floor(a / 10000);
let name = `${f * 10}K-${(f + 1) * 10}K`;
if (f == 0) {
name = '<10K';
}
if (acc[name]) {
acc[name] += 1;
} else {
acc[name] = 1;
}
return acc;
},
{} as Record<string, number>
)}
/>
</div>
{/each}
</div>
</div>
</div>
</div>
</HasUser>