Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Homework2/kinxu/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# More about the Framework


This is a template in React and TypeScript. React has a steeper learning curve than Vue.js because it requires understanding JSX syntax and concepts like hooks and component lifecycle.

If you want to use React but not with TypeScript, just remove any type specifications from the `Example.tsx`, `Notes.tsx`, and `NotesWithReducer.tsx`. You can always refer to `VanillaJS-Template/example.js` for this migration.


## Files You Have to Care about

`package.json` is where we manage the libraries we installed. Besides this, most of the files you can ignore, but **the files under `./src/` are your concern**.

* `./src/main.tsx` is the root script file for React that instatinates our single page application.
* `./src/App.tsx` is the root file for all **development** needs and is also where we manage the layout and load in components.
* `./src/types.ts` is usually where we declare our customized types if you're planning to use it.
* `./src/stores/` is where we manage the stores if you're planning to use it. The store is responsible for global state management.
* `./src/components/` is where we create the components. You may have multiple components depends on your design.
* `Example.tsx` shows how to read `.csv` and `.json`, how component size is being watched, how a bar chart is created, and how the component updates if there are any changes.
* `Notes.tsx` shows the difference of **state** and **prop**, how to use MUI, and how a local state updates based on interaction.
* `NotesWithReducer.tsx` is equivalent to `Notes.tsx`, excepts it uses store called reducer.

## Libraries Installed in this Framework
* D3.js v7 for visualization
* [axios](https://axios-http.com/docs/intro) for API.
* [Material UI](https://mui.com/material-ui/getting-started/) for UI components.
* [lodash](https://lodash.com/) for utility functions in JavaScript.
10 changes: 10 additions & 0 deletions Homework2/kinxu/data/demo.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
category,value
a,21
b,42
c,43
d,5
e,26
f,7
l,10
s,18
x,85
41 changes: 41 additions & 0 deletions Homework2/kinxu/data/demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"data":[
{
"value": 21,
"category": "a"
},
{
"value": 42,
"category": "b"
},
{
"value": 43,
"category": "c"
},
{
"value": 5,
"category": "d"
},
{
"value": 26,
"category": "e"
},
{
"value": 7,
"category": "f"
},
{
"value": 10,
"category": "l"
},
{
"value": 18,
"category": "s"
},
{
"value": 85,
"category": "x"
}
]

}
991 changes: 991 additions & 0 deletions Homework2/kinxu/data/top_1000_most_swapped_books.csv

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions Homework2/kinxu/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
35 changes: 35 additions & 0 deletions Homework2/kinxu/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "react-template",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/material": "^7.3.6",
"@types/d3": "^7.4.3",
"@types/lodash": "^4.17.21",
"axios": "^1.13.2",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"lodash": "^4.17.21",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"usehooks-ts": "^3.1.1"
},
"devDependencies": {
"@types/material-ui": "^0.21.18",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"globals": "^17.0.0",
"typescript": "^5.9.3",
"vite": "^7.3.0"
}
}
61 changes: 61 additions & 0 deletions Homework2/kinxu/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Example from './components/Example'
import Notes from './components/Notes'
import BarChart from './components/BarChart';
import PieChart from './components/PieChart';
import { NotesWithReducer, CountProvider } from './components/NotesWithReducer';
import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { grey } from '@mui/material/colors';
import SankeyDiagram from './components/SankeyDiagram';

// Adjust the color theme for material ui
const theme = createTheme({
palette: {
primary:{
main: grey[700],
},
secondary:{
main: grey[700],
}
},
})

// For how Grid works, refer to https://mui.com/material-ui/react-grid/

function Layout() {
return (
<Box id="main-container" sx={{ height: '100%' }}>
<Stack spacing={1} sx={{ height: '100%' }}>
{/* Top row: Bar + Pie */}
<Grid container spacing={1} sx={{ height: '40%' }}>
<Grid size={12}>
<BarChart />
</Grid>

</Grid>
<Grid size="grow"></Grid>
{/* Bottom row: Sankey full width */}
<Grid container spacing={1} sx={{ height: '60%' }}>
<Grid size={5}>
<PieChart />
</Grid>
<Grid size={7}>
<SankeyDiagram />
</Grid>
</Grid>
</Stack>
</Box>
);
}

function App() {
return (
<ThemeProvider theme={theme}>
<Layout />
</ThemeProvider>
)
}

export default App
203 changes: 203 additions & 0 deletions Homework2/kinxu/src/components/BarChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import React from "react";
import { useEffect, useState, useRef } from "react";
import * as d3 from "d3";
import { isEmpty } from "lodash";
import { useResizeObserver, useDebounceCallback } from 'usehooks-ts';

import { BookData, ComponentSize, Margin } from '../types';

export default function BarChart() {
const [data, setData] = useState<BookData[]>([]);
const barRef = useRef<HTMLDivElement>(null);
const margin: Margin = {top: 40, right: 160, bottom: 80, left: 60 };
const [size, setSize] = useState<ComponentSize>({ width: 0, height: 0 });
const onResize = useDebounceCallback((size: ComponentSize) => setSize(size), 200);

useResizeObserver({ ref: barRef as React.RefObject<HTMLDivElement>, onResize });


// Read data
useEffect(() => {
const readData = async() => {
const csvData = await d3.csv("./data/top_1000_most_swapped_books.csv")
setData(csvData)
}
readData()
}, []);

// Generate bar char
useEffect(() => {
if (isEmpty(data)) return;
if (size.width === 0 || size.height === 0) return;
d3.select('#bar-svg').selectAll('*').remove();
generateBarChart();
}, [data, size])


function generateBarChart() {
const barChartContainer = d3.select("#bar-svg");
const ratings = [3, 3.5, 4, 4.5, d3.max(data.map((dataPoint) => Number(dataPoint.rating_average)))];
const years = [d3.min(data.map((dataPoint) => Number(dataPoint.publicationYear))), 1800, 1850, 1900, 1950, 2000,
d3.max(data.map((dataPoint) => Number(dataPoint.publicationYear))) + 1];

// Data needed for bar chart
let dataUse = [];
let yearRanges = [];
let maxCount = 0;

// Get the number of books published in each time period
for (let i = 0; i < years.length - 1; i++) {
const minYear = years[i];
const maxYear = years[i + 1];
let yearRange = String(minYear) + " - " + String(maxYear - 1);
let count = 0;

if (i == 0) {
yearRange = "Pre 1800";
}
const yearFilteredData = data.filter((dataPoint) => dataPoint.publicationYear >= minYear &&
dataPoint.publicationYear < maxYear);

// Count books grouped by average rating range
for (let j = 0; j < ratings.length - 1; j++) {
const minRating = ratings[j];
const maxRating = ratings[j + 1];
let ratingRange = String(minRating) + " - " + String(maxRating);

if (j != ratings.length - 2) {
ratingRange = String(minRating) + " - " + String(maxRating - 0.01);
}
const filteredData = yearFilteredData.filter((dataPoint) => dataPoint.rating_average >= minRating &&
dataPoint.rating_average < maxRating);

count += filteredData.length;
dataUse.push({"year": yearRange, "number_books": filteredData.length, "rating": ratingRange});
}

// Find maximum number of books published in a time period
if (count > maxCount) {
maxCount = count;
}

yearRanges.push(yearRange);
}

// Get set of time periods for x-axis
const ratingRanges = [... new Set(dataUse.map((dataPoint) => dataPoint.rating))]


const xScale = d3.scaleBand()
.domain(yearRanges)
.range([margin.left, size.width - margin.right])
.padding(0.1);

// Generate x-axis
const xAxis = barChartContainer.append("g")
.attr("transform", `translate(0, ${size.height - margin.bottom})`)
.call(d3.axisBottom(xScale));

// Generate x-axis label
const xLabel = barChartContainer.append('g')
.attr('transform', `translate(${(size.width - margin.left) / 2}, ${size.height - margin.top})`)
.append('text')
.text('Time Periods')
.style('font-size', '0.8rem');

const yScale = d3.scaleLinear()
.domain([0, maxCount])
.range([size.height - margin.bottom, margin.top]);

// Generate y-axis
const yAxis = barChartContainer.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(yScale));

// Generate y-axis label
const yLabel = barChartContainer.append('g')
.attr('transform', `translate(${margin.left / 2}, ${size.height / 2}) rotate(-90)`)
.append('text')
.text('Number of Books')
.style('font-size', '0.8rem');

// Set color encoding for average rating ranges
const colors = d3.scaleOrdinal()
.domain(ratingRanges)
.range(d3.schemeGreens[4]);

// Generate stacked bars
for (const yearRange of yearRanges) {
const yearFilteredData = dataUse.filter((dataPoint) => dataPoint.year == yearRange);

// Running total of books for this time period
let runningTotalCount = 0

// Generate a section of this time period's bar
barChartContainer.append("g")
.selectAll("rect")
.data(yearFilteredData)
.enter()
.append("rect")
.attr("x", (dataPoint) => xScale(dataPoint.year))
.attr("y", (dataPoint) => {
runningTotalCount += dataPoint.number_books
return yScale(runningTotalCount)
})
.attr("width", xScale.bandwidth())
.attr("height", (dataPoint) => {
const previousCount = runningTotalCount - dataPoint.number_books
return yScale(previousCount) - yScale(runningTotalCount)
})
.style("fill", (dataPoint) => colors(dataPoint.rating))
}

const legend = barChartContainer.append("g")
.attr("id", "bar-chart-legend-container")
.attr("transform", `translate(${size.width - margin.right}, ${margin.top + 20})`)

// Generate legend title
legend.append("g")
.append("text")
.attr("transform", `translate(0, 15)`)
.style("text-anchor", "right")
.style("font-weight", "bold")
.style("font-size", ".8rem")
.text("Average Rating Range")

// Generate legend
const legendItem = legend.selectAll(".legend-item")
.data(ratingRanges)
.enter()
.append("g")
.attr("class", "legend-item")
.attr("transform", (dataPoint, i) => `translate(0, ${i * 25 + 20})`);

legendItem.append("rect")
.attr("width", 18)
.attr("height", 18)
.attr("fill", dataPoint => colors(dataPoint));

legendItem.append("text")
.attr("x", 26)
.attr("y", 15)
.style("font-size", "0.8rem")
.text(dataPoint => dataPoint);

// Generate title
const title = barChartContainer.append('g')
.append("text")
.attr("transform", `translate(${size.width / 2}, ${margin.top - 15})`)
.attr("d", '0.5rem')
.style("text-anchor", "middle")
.style("font-weight", "bold")
.text("Distribution of Most-Swapped Books by Publication Time Period and Average Rating")

}

return (
<>
<div ref = {barRef} className = "chart-container">
<svg id = "bar-svg" width = "100%" height = "100%"></svg>
</div>
</>
)
}
Loading