Skip to content

Commit

Permalink
[feat] Piechart (#87)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
SlashStars1 and TetraTsunami authored Dec 1, 2024
1 parent e92bc4a commit 3feb16e
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 6 deletions.
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/components/graphs/DummyGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,4 @@ export default function DummyGraph({ incidents, bounds }: { incidents: [string,
<svg className="absolute inset-0" ref={d3Ref}></svg>
</div>
)
}
}
143 changes: 143 additions & 0 deletions src/components/graphs/PieChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { DB, Incident } from '@/types'
import * as d3 from 'd3'
import { useRef, useEffect } from 'react'

export default function PieChart({ incidents, data }: { incidents: [string, Incident][]; data: DB }) {
const containerRef = useRef<HTMLDivElement | null>(null)
const d3Ref = useRef<SVGSVGElement | null>(null)

//gets label and color for each incident
const parsedData = incidents.map((incident) => {
return {
label: data.Categories[data.Types[incident[1].typeID].categoryID].name,
color: data.Categories[data.Types[incident[1].typeID].categoryID].color,
}
})
let categories = Object.values(data.Categories).map((c) => c.name) //we want in this format: ["drugs", "robbery"]

type Category = { label: string; value: number }
let cleanData: Category[] = [] //should end up having this format for the pie chart: [{label: "drugs", value: 10} , {}]

let totalValue = 0
//creates cleanData
categories.forEach((category) => {
let categoryEntries = parsedData.filter((c) => c.label == category)
let value = 0

// for each category count how many incidents were in the category (Creating the value number)
categoryEntries.forEach(() => {
value++
totalValue++
})

cleanData.push({ label: category, value: value })
})
cleanData = cleanData.filter((d) => d.value > 0).sort((a, b) => d3.descending(a.value, b.value))

useEffect(() => {
function render() {
if (d3Ref.current) {
const svg = d3.select(d3Ref.current)
// Clear previous contents
svg.selectAll('*').remove()

// Set dimensions
const width = 400
const height = 200

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

const g = svg.append('g')
g.append('g').attr('class', 'slices')
g.append('g').attr('class', 'labels')
g.append('g').attr('class', 'lines')

g.attr('transform', 'translate(' + width / 4 + ',' + height / 2 + ')')

const radius = Math.min(width, height) / 2

const pie = d3
.pie<Category>()
.sort(null)
.value(function (d) {
return d.value
})

//helper function for color
//reduce() iterates over the Categories array and builds an object "acc" so that
//acc will be similar to a dictionary in the form: {"drugs": "black", "robbery": "white"}
const labelColorMap = Object.values(data.Categories).reduce(
(acc, category) => {
acc[category.name] = category.color
return acc
},
{} as Record<string, string>
)

const color = d3
.scaleOrdinal()
.domain(cleanData.map((d) => d.label))
.range(cleanData.map((d) => labelColorMap[d.label]))

const arc = d3
.arc()
.outerRadius(radius * 0.8)
.innerRadius(radius * 0.4)

/* ------- PIE SLICES -------*/

const pieData = pie(cleanData)

// Draw slices
const slice = g.select('.slices').selectAll('path.slice').data(pieData)

slice
.enter()
.append('path')
.attr('class', 'slice')
.style('fill', (d) => color(d.data.label) as string)
.attr('d', arc as any)

slice.exit().remove()

/* ------- LEGEND ------- */
const legend = svg
.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${width - 200}, 20)`) // Position legend to the right of the pie chart

const legendItems = legend
.selectAll('.legend-item')
.data(cleanData)
.enter()
.append('g')
.attr('transform', (_, i) => `translate(0, ${i * 20})`) // Position each legend item

// Add colored rectangles
legendItems
.append('rect')
.attr('width', 12)
.attr('height', 12)
.attr('fill', (d) => color(d.label) as string)

// Add text labels
legendItems
.append('text')
.attr('x', 18) // Position text next to rectangle
.attr('y', 10) // Vertically align text with rectangle
.text((d) => `${d.label} (${Math.floor((d.value / totalValue) * 100)}%)`)
.style('font-size', '12px')
.attr('fill', '#000')
}
}
addEventListener('resize', render)
render()
return () => removeEventListener('resize', render)
}, [incidents])

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>
</div>
)
}
5 changes: 4 additions & 1 deletion src/pages/StatsDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 PieChart from '@/components/graphs/PieChart'
import StatisticsFilterMap from '@/components/StatisticsFilterMap'

export type filterDispatchType = { type: 'ADD_FILTER' | 'REMOVE_FILTER' | 'UPDATE_FILTER'; payload: Partial<filterType> }
Expand Down Expand Up @@ -71,20 +72,22 @@ 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">
<DummyGraph incidents={filteredIncidents} bounds={filteredBounds} />
<PieChart data={data} incidents={filteredIncidents}></PieChart>
<DummyGraph incidents={filteredIncidents} bounds={filteredBounds} />
<DummyGraph incidents={filteredIncidents} bounds={filteredBounds} />
</div>
Expand Down

0 comments on commit 3feb16e

Please sign in to comment.