Skip to content

Commit 18357a0

Browse files
committed
feat: Add Nivo chart visualizations for CSV tickets and enhance documentation
Signed-off-by: Andre Bossard <anbossar@microsoft.com>
1 parent 507601b commit 18357a0

10 files changed

Lines changed: 666 additions & 1 deletion

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Quart + Vite + React Demo Application
22

3+
**Task:** Visualize the CSV tickets with richer panels (status, priority, timeline, geography, SLA). **How would you like to view the tickets?**
4+
35
> A teaching-oriented full-stack sample that pairs a Python Quart backend with a React + FluentUI frontend, real-time Server-Sent Events (SSE), and Playwright tests.
46
57
## Why this repo?

docs/NIVO.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Nivo Charts for Tickets
2+
3+
This repo is a learning sandbox for CSV ticket data. Use Nivo to visualize tickets with minimal setup.
4+
5+
## Installation
6+
7+
```bash
8+
cd frontend
9+
npm install @nivo/core @nivo/pie @nivo/bar @nivo/line @nivo/axes @nivo/legends
10+
```
11+
12+
## Demo Component
13+
14+
`frontend/src/features/tickets/NivoTicketsDemo.jsx`
15+
16+
- Panels:
17+
- **Status (Pie)** – distribution across statuses
18+
- **Priority (Bar)** – counts per priority
19+
- **Timeline (Line)** – cumulative or daily counts over time
20+
- Sample data: `frontend/src/features/tickets/sampleTickets.js`
21+
22+
## Usage
23+
24+
```jsx
25+
import NivoTicketsDemo from '@/features/tickets/NivoTicketsDemo';
26+
27+
export function TicketsPage() {
28+
return (
29+
<div style={{ height: 420 }}>
30+
<NivoTicketsDemo />
31+
</div>
32+
);
33+
}
34+
```
35+
36+
## Wire in real data
37+
38+
1. **Fetch backend CSV** (or parse `csv/data.csv` server-side and expose an API).
39+
2. **Map to aggregates**:
40+
- `ticketsByStatus`: `[ { id, label, value } ]`
41+
- `ticketsByPriority`: `[ { priority, count } ]`
42+
- `ticketsTimelineSeries`: `[ { id, data: [ { x: date, y: count } ] } ]`
43+
3. **Pass props** to the component or replace `sampleTickets.js` with live data.
44+
45+
### Example aggregation (frontend)
46+
```ts
47+
import _groupBy from 'lodash/groupBy';
48+
49+
function toStatusPie(tickets) {
50+
const grouped = _groupBy(tickets, t => t.status || 'Unknown');
51+
return Object.entries(grouped).map(([status, items]) => ({
52+
id: status,
53+
label: status,
54+
value: items.length,
55+
}));
56+
}
57+
```
58+
59+
## Panels & layout
60+
61+
- Use the built-in panel switcher for multiple views.
62+
- Wrap in Fluent UI `Card` or `Surface` for consistent theming.
63+
- Keep heights fixed for responsive layouts (e.g., `height: 420`).
64+
65+
## Tips
66+
67+
- Dates should be ISO strings or consistent x-axis labels (`YYYY-MM-DD`).
68+
- Keep datasets small for demos; paginate or filter for large CSVs.
69+
- Customize colors: `colors={{ scheme: 'category10' }}` etc.
70+
- Enable tooltips by default (Nivo provides them out-of-the-box).
71+
72+
## Next steps
73+
74+
- Replace sample data with real CSV aggregates.
75+
- Add filters (date range, status, priority) to update charts.
76+
- Add a table view to complement charts (e.g., `@fluentui/react-components` `Table`).
77+
78+
## Question to reader
79+
80+
**How would you like to view the tickets?**
81+
- Status distribution?
82+
- Priority breakdown?
83+
- Time trends?
84+
- Geographic (by city/country)?
85+
- SLA compliance?
86+
87+
Let this guide which panels you build.

frontend/package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
"dependencies": {
1414
"@fluentui/react-components": "^9.47.0",
1515
"@fluentui/react-icons": "^2.0.239",
16+
"@nivo/axes": "^0.99.0",
17+
"@nivo/bar": "^0.99.0",
18+
"@nivo/core": "^0.99.0",
19+
"@nivo/legends": "^0.99.0",
20+
"@nivo/line": "^0.99.0",
21+
"@nivo/pie": "^0.99.0",
22+
"@nivo/sankey": "^0.99.0",
23+
"@nivo/stream": "^0.99.0",
1624
"react": "^18.2.0",
1725
"react-dom": "^18.2.0",
1826
"react-router-dom": "^7.9.6"
@@ -24,4 +32,4 @@
2432
"@vitejs/plugin-react": "^4.2.1",
2533
"vite": "^5.1.0"
2634
}
27-
}
35+
}

frontend/src/App.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ import {
1616
tokens,
1717
} from '@fluentui/react-components'
1818
import {
19+
DataHistogram24Regular,
20+
Info24Regular,
1921
Table24Regular,
2022
} from '@fluentui/react-icons'
2123
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
2224
import CSVTicketTable from './features/csvtickets/CSVTicketTable'
25+
import FieldsDocs from './features/fields/FieldsDocs'
26+
import KitchenSink from './features/kitchensink/KitchenSink'
2327

2428
const useStyles = makeStyles({
2529
app: {
@@ -57,6 +61,8 @@ export default function App() {
5761
const navigate = useNavigate()
5862
const tabs = [
5963
{ value: 'csvtickets', label: 'Tickets', icon: <Table24Regular />, path: '/csvtickets', testId: 'tab-csvtickets' },
64+
{ value: 'kitchensink', label: 'Kitchen Sink', icon: <DataHistogram24Regular />, path: '/kitchensink', testId: 'tab-kitchensink' },
65+
{ value: 'fields', label: 'Fields', icon: <Info24Regular />, path: '/fields', testId: 'tab-fields' },
6066
]
6167
const activeTab = tabs.find((tab) => location.pathname.startsWith(tab.path))?.value ?? 'csvtickets'
6268

@@ -92,6 +98,8 @@ export default function App() {
9298
<Routes>
9399
<Route path="/" element={<Navigate to="/csvtickets" replace />} />
94100
<Route path="/csvtickets" element={<CSVTicketTable />} />
101+
<Route path="/kitchensink" element={<KitchenSink />} />
102+
<Route path="/fields" element={<FieldsDocs />} />
95103
<Route path="*" element={<Navigate to="/csvtickets" replace />} />
96104
</Routes>
97105
</main>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Caption1, Card, CardHeader, makeStyles, Spinner, Subtitle1, Text, tokens } from '@fluentui/react-components'
2+
import { useEffect, useState } from 'react'
3+
import { getCSVTicketFields } from '../../services/api'
4+
5+
const useStyles = makeStyles({
6+
container: {
7+
padding: tokens.spacingVerticalL,
8+
display: 'flex',
9+
flexDirection: 'column',
10+
gap: tokens.spacingVerticalL,
11+
},
12+
tableWrapper: {
13+
overflowX: 'auto',
14+
backgroundColor: tokens.colorNeutralBackground1,
15+
borderRadius: tokens.borderRadiusMedium,
16+
boxShadow: tokens.shadow4,
17+
},
18+
table: {
19+
width: '100%',
20+
borderCollapse: 'collapse',
21+
fontSize: tokens.fontSizeBase200,
22+
},
23+
th: {
24+
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
25+
textAlign: 'left',
26+
backgroundColor: tokens.colorNeutralBackground3,
27+
borderBottom: `2px solid ${tokens.colorNeutralStroke1}`,
28+
whiteSpace: 'nowrap',
29+
},
30+
td: {
31+
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`,
32+
borderBottom: `1px solid ${tokens.colorNeutralStroke2}`,
33+
maxWidth: '320px',
34+
overflow: 'hidden',
35+
textOverflow: 'ellipsis',
36+
whiteSpace: 'nowrap',
37+
},
38+
tr: {
39+
':nth-child(even)': {
40+
backgroundColor: tokens.colorNeutralBackground2,
41+
},
42+
},
43+
})
44+
45+
export default function FieldsDocs() {
46+
const styles = useStyles()
47+
const [fields, setFields] = useState([])
48+
const [loading, setLoading] = useState(true)
49+
const [error, setError] = useState(null)
50+
51+
useEffect(() => {
52+
async function load() {
53+
try {
54+
const data = await getCSVTicketFields()
55+
setFields(data.fields || [])
56+
} catch (err) {
57+
setError(err?.message || 'Failed to load fields')
58+
} finally {
59+
setLoading(false)
60+
}
61+
}
62+
load()
63+
}, [])
64+
65+
return (
66+
<div className={styles.container}>
67+
<div>
68+
<Subtitle1>CSV Ticket Fields</Subtitle1>
69+
<Caption1>Fetched from API: `/csv-tickets/fields`</Caption1>
70+
</div>
71+
72+
{loading && (
73+
<Spinner label="Loading fields..." />
74+
)}
75+
76+
{error && (
77+
<Text>{error}</Text>
78+
)}
79+
80+
{!loading && !error && (
81+
<Card>
82+
<CardHeader header={<Text weight="semibold">Available Fields ({fields.length})</Text>} />
83+
<div className={styles.tableWrapper}>
84+
<table className={styles.table}>
85+
<thead>
86+
<tr>
87+
<th className={styles.th}>Name</th>
88+
<th className={styles.th}>Label</th>
89+
<th className={styles.th}>Type</th>
90+
</tr>
91+
</thead>
92+
<tbody>
93+
{fields.map((field) => (
94+
<tr key={field.name} className={styles.tr}>
95+
<td className={styles.td}>{field.name}</td>
96+
<td className={styles.td}>{field.label || '—'}</td>
97+
<td className={styles.td}>{field.type || 'string'}</td>
98+
</tr>
99+
))}
100+
</tbody>
101+
</table>
102+
</div>
103+
</Card>
104+
)}
105+
</div>
106+
)
107+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Subtitle1, Text } from '@fluentui/react-components'
2+
import NivoTicketsDemo from '../tickets/NivoTicketsDemo'
3+
import SankeyTicketsDemo from '../tickets/SankeyTicketsDemo'
4+
import StreamTicketsDemo from '../tickets/StreamTicketsDemo'
5+
6+
export default function KitchenSink() {
7+
return (
8+
<div style={{ display: 'flex', flexDirection: 'column', gap: 32, padding: '24px 0' }}>
9+
<div>
10+
<Subtitle1>Kitchen Sink Demo</Subtitle1>
11+
<Text>
12+
Ticket visualizations with Nivo panels, plus Sankey (status → priority flows) and Stream (stacked trends).
13+
</Text>
14+
</div>
15+
16+
<section>
17+
<Subtitle1>Status / Priority / Timeline</Subtitle1>
18+
<div style={{ height: 420 }}>
19+
<NivoTicketsDemo />
20+
</div>
21+
</section>
22+
23+
<section>
24+
<Subtitle1>Sankey — Status to Priority</Subtitle1>
25+
<div style={{ height: 420 }}>
26+
<SankeyTicketsDemo />
27+
</div>
28+
</section>
29+
30+
<section>
31+
<Subtitle1>Stream — Status Over Time</Subtitle1>
32+
<div style={{ height: 420 }}>
33+
<StreamTicketsDemo />
34+
</div>
35+
</section>
36+
</div>
37+
)
38+
}

0 commit comments

Comments
 (0)