Skip to content

Commit

Permalink
[feat] Line graph in stats dashboard (#84)
Browse files Browse the repository at this point in the history
* Added the line graph to the stats dashboard

* [fix] Show popup on top of other elements

* Change grouping with dropdown

* Change graph group style

* [feat] Map Display in Statistics Dashboard (#86)

* feat: added a toggle map button to stats dashboard

* feat: added a simpler map in statics dashboard

* feat: statistics dashboard map showing only filtered incidents now

* [feat] Piechart (#87)

* Pie chart percentages + text labels

* Added pie chart

* Apply formatting

* Fix TS build errors

* Sort categories in descending order

---------

Co-authored-by: TetraTsunami <[email protected]>

* Fix duplicated import statement (how???)

---------

Co-authored-by: TetraTsunami <[email protected]>
adityamachiroutu and TetraTsunami authored Dec 3, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 3feb16e commit a242a29
Showing 2 changed files with 136 additions and 5 deletions.
132 changes: 132 additions & 0 deletions src/components/graphs/LineGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { useRef, useEffect, useState } from 'react'
import * as d3 from 'd3'
import { Incident } from '@/types'
import { calculateBounds } from '@/utils'

export default function LineGraph({ incidents, bounds }: { incidents: [string, Incident][]; bounds: ReturnType<typeof calculateBounds> }) {
const containerRef = useRef<HTMLDivElement | null>(null)
const d3Ref = useRef<SVGSVGElement | null>(null)
const [groupBy, setGroupBy] = useState<'year' | 'quarter' | 'month' | 'week' | 'day'>('year')

useEffect(() => {
function render() {
if (d3Ref.current) {
const svg = d3.select(d3Ref.current)

svg.selectAll('*').remove()

const width = 400
const height = 200
const margin = { top: 45, right: 30, bottom: 30, left: 40 }

svg.attr('preserveAspectRatio', 'xMinYMin meet').attr('viewBox', `0 0 ${width} ${height}`)

// Modify the grouping logic based on groupBy
const groupedData = Array.from(
d3.group(incidents, (d) => {
const date = new Date(d[1].dateString)
switch (groupBy) {
case 'year':
return date.getFullYear()
case 'quarter':
return `${date.getFullYear()}-T${Math.floor(date.getMonth() / 3) + 1}`
case 'month':
return `${date.getFullYear()}-${date.getMonth() + 1}`
case 'week':
return d3.timeFormat('%Y-%W')(date)
case 'day':
return date.toISOString().split('T')[0]
}
}),
([key, incidents]) => ({
key,
date: parseDateKey(key as string, groupBy),
count: incidents.length,
})
).sort((a, b) => a.date.getTime() - b.date.getTime())

//Create scales
const x = d3
.scaleTime()
.domain(d3.extent(groupedData, (d) => d.date) as [Date, Date])
.range([margin.left, width - margin.right])

const y = d3
.scaleLinear()
.domain([0, d3.max(groupedData, (d) => d.count) || 0])
.nice()
.range([height - margin.bottom, margin.top])

const line = d3
.line<(typeof groupedData)[0]>()
.x((d) => x(d.date))
.y((d) => y(d.count))
.curve(d3.curveMonotoneX)

//Create axes
const xTicks = width / 100
const yTicks = height / 50

svg
.append('g')
.attr('transform', `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(xTicks))

svg.append('g').attr('transform', `translate(${margin.left}, 0)`).call(d3.axisLeft(y).ticks(yTicks))
svg.append('path').datum(groupedData).attr('fill', 'none').attr('stroke', 'steelblue').attr('stroke-width', 1.5).attr('d', line)

svg
.selectAll('.dot')
.data(groupedData)
.join('circle')
.attr('class', 'dot')
.attr('cx', (d) => x(d.date))
.attr('cy', (d) => y(d.count))
.attr('r', 4)
.attr('fill', 'steelblue')
}
}

addEventListener('resize', render)
render()
return () => removeEventListener('resize', render)
}, [incidents, bounds, groupBy])

return (
<div ref={containerRef} className="relative aspect-[2/1] min-w-[300px] flex-grow overflow-hidden rounded-lg bg-neutral-100">
<svg className="absolute inset-0" ref={d3Ref}></svg>
<h2 className="m-2">Incidentes a lo largo del tiempo</h2>
<select
value={groupBy}
className="absolute right-2 top-2 rounded-full border border-black bg-transparent px-2"
onChange={(e) => setGroupBy(e.target.value as any)}
>
<option value="year">Año</option>
<option value="quarter">Trimestre</option>
<option value="month">Mes</option>
<option value="week">Semana</option>
<option value="day">Día</option>
</select>
</div>
)
}

// Add helper function to parse date keys
function parseDateKey(key: string, groupBy: string): Date {
switch (groupBy) {
case 'year':
return new Date(Number(key), 0, 1)
case 'quarter':
const [yearQ, q] = key.split('-T')
return new Date(Number(yearQ), (Number(q) - 1) * 3, 1)
case 'month':
const [yearM, month] = key.split('-')
return new Date(Number(yearM), Number(month) - 1, 1)
case 'week':
return d3.timeParse('%Y-%W')(key) || new Date()
case 'day':
return new Date(key)
default:
return new Date()
}
}
9 changes: 4 additions & 5 deletions src/pages/StatsDashboard.tsx
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import IncidentTable from '@/components/IncidentTable'
import StatisticsFilterBar from '@/components/StatisticsFilterBar'
import { calculateBounds } from '@/utils'
import DummyGraph from '@/components/graphs/DummyGraph'
import LineGraph from '@/components/graphs/LineGraph'
import PieChart from '@/components/graphs/PieChart'
import StatisticsFilterMap from '@/components/StatisticsFilterMap'

@@ -72,23 +73,21 @@ const StatsDashboard: React.FC<StatsDashboardProps> = ({ data }) => {
const filteredBounds = calculateBounds(Object.fromEntries(filteredIncidents))

return (

<div className="h-full p-4">
<div className="flow-row flex items-center justify-between">
<h1 className="text-2xl font-semibold">Estadísticas</h1>
<button className="m-1 rounded-md px-2 py-1 hover:bg-black hover:bg-opacity-10" onClick={() => setIsShowingMap(!isShowingMap)}>
{isShowingMap ? 'Ocultar Mapa' : 'Mostrar Mapa'}
</button>

</div>
<StatisticsFilterBar data={data} filters={filters.filters} dispatchFilters={dispatchFilters} />
{isShowingMap ? (
<StatisticsFilterMap data={data} incidents={filteredIncidents} />
) : (
<>
<div className="my-4 flex flex-row flex-wrap gap-4">
<PieChart data={data} incidents={filteredIncidents}></PieChart>
<DummyGraph incidents={filteredIncidents} bounds={filteredBounds} />
<div className="mx-auto my-4 grid max-w-[500px] gap-4 lg:max-w-full lg:grid-cols-3">
<PieChart data={data} incidents={filteredIncidents}></PieChart>
<LineGraph incidents={filteredIncidents} bounds={filteredBounds} />
<DummyGraph incidents={filteredIncidents} bounds={filteredBounds} />
</div>
<IncidentTable data={data} incidents={filteredIncidents} />

0 comments on commit a242a29

Please sign in to comment.