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
55 changes: 55 additions & 0 deletions Homework2/mnakagawa/Design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
## Visual for HW2

![alt text](fig/image-1.png)

## Dataset

Spotify :
<https://www.kaggle.com/datasets/wardabilal/spotify-global-music-dataset-20092025>

Files used in this project:

- track_data_final.csv (historical popular tracks; 2009-2023 hits)
- spotify_data_clean.csv (recent tracks snapshot; 2025 hits)

### Template

I used given React templates for this project.

## User Goals

This dashboard is designed for independent music artists who want to understand what characteristics make recent songs popular on Spotify.

Users can learn:
1. How the sound profile of popular songs has changed over time

2. How artist popularity relates to track popularity among popular songs

3. How the audio features of recent hits differ from older popular tracks


## View

1. Main (View 1): 100% stacked area of release-year trends (composition)
- Group by release year (album_release_date)
- Compute yearly averages for each proxy "feature" and normalize per feature (0-1), then convert to per-year shares (sum=100%)
- Y-axis shows % share of the normalized feature mix (not an absolute score)

2. Bottom-left (View 2): Star Plot (Radar) comparison between datasets (past vs 2025 snapshot)
- Gray = track_data_final.csv baseline (fixed at 0.5 for readability)
- Green vertex labels show 2025 raw means; each axis also shows +/- delta% vs baseline
- Note: this view uses a normalized delta visualization (baseline=0.5, delta scaled, amplified) to make small differences visible

3. Bottom-right (View 3): Scatter plot (artist_popularity vs track_popularity)
- Points are colored by artist_followers

## Design Choices

1. Amplify the difference in view 2

The numerical difference was too small as the shown graph below, so I decided to amplify the differences.
![alt text](fig/image.png)

2. Design palette
use [d3.interpolateYlGn(t)](https://d3js.org/d3-scale-chromatic/sequential#interpolateYlGn) to represent spotify theme

8,583 changes: 8,583 additions & 0 deletions Homework2/mnakagawa/data/spotify_data_clean.csv

Large diffs are not rendered by default.

8,779 changes: 8,779 additions & 0 deletions Homework2/mnakagawa/data/track_data_final.csv

Large diffs are not rendered by default.

Binary file added Homework2/mnakagawa/fig/image-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Homework2/mnakagawa/fig/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions Homework2/mnakagawa/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>
34 changes: 34 additions & 0 deletions Homework2/mnakagawa/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"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",
"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"
}
}
85 changes: 85 additions & 0 deletions Homework2/mnakagawa/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Example from './components/Example'
import Notes from './components/Notes'
import Panel from './components/Panel'
import StarPlot from './components/StarPlot'
import Scatter from './components/Scatter'
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 CssBaseline from '@mui/material/CssBaseline';
import { grey } from '@mui/material/colors';
import Streamgraph from './components/Streamgraph'


const theme = createTheme({
palette: {
mode: 'dark',
primary:{
main: '#1DB954', // Spotify green
},
secondary:{
main: '#1ED760',
},
background: {
default: '#0B0B0F',
paper: '#121218',
},
text: {
primary: '#EDEDED',
secondary: '#A7A7A7',
},
},
})

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

function Layout() {
return (
<Box id='main-container'>
<Stack spacing={1} sx={{ height: '100%' }}>
{/* Top row: Example component taking about 60% width */}
<Grid container spacing={3} sx={{ height: '55%' }}>
<Grid size={12}>
<Panel title="Hit Trend By Release Year">
<Streamgraph />
</Panel>
</Grid>
{/* flexible spacer to take remaining space */}
{/* <Grid size="grow" /> */}
</Grid>
{/* Bottom row: Notes component taking full width */}
<Grid container spacing={1} sx={{ height: '45%' }}>
<Grid size={5}>
<Panel title="Trend Comparison : Past vs 2025">
<StarPlot />
</Panel>
</Grid>
<Grid size={7}>
<Panel title="Relationship Track Popularity vs Artist Popularity">
<Scatter />
</Panel>
</Grid>
{/* <Notes msg={"This is a message sent from App.tsx as component prop"} />
{
// <CountProvider>
// <NotesWithReducer msg={"This is a message sent from App.tsx as component prop"} />
// </CountProvider>
} */}
</Grid>
</Stack>
</Box>
)
}

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

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

import { Bar, ComponentSize, Margin } from '../types';
// A "extends" B means A inherits the properties and methods from B.
interface CategoricalBar extends Bar{
category: string;
}



export default function Example() {
const [bars, setBars] = useState<CategoricalBar[]>([]);
const barRef = useRef<HTMLDivElement>(null);
const [size, setSize] = useState<ComponentSize>({ width: 0, height: 0 });
const margin: Margin = { top: 40, right: 20, bottom: 80, left: 60 };
const onResize = useDebounceCallback((size: ComponentSize) => setSize(size), 200)

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

useEffect(() => {
// For reading json file
/*if (isEmpty(dataFromJson)) return;
setBars(dataFromJson.data);*/

// For reading csv file
const dataFromCSV = async () => {
try {
const csvData = await d3.csv('../../data/demo.csv', d => {
// This callback allows you to rename the keys, format values, and drop columns you don't need
return {category: d.category, value: +d.value};
});
setBars(csvData);
} catch (error) {
console.error('Error loading CSV:', error);
}
}
dataFromCSV();
}, [])

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

function initChart() {
// select the svg tag so that we can insert(render) elements, i.e., draw the chart, within it.
let chartContainer = d3.select('#bar-svg')


// Here we compute the [min, max] from the data values of the attributes that will be used to represent x- and y-axis.
let yExtents = d3.extent(bars.map((d: CategoricalBar) => d.value as number)) as [number, number]
// This is to get the unique categories from the data using Set, then store in an array.
let xCategories: string[] = [ ...new Set(bars.map((d: CategoricalBar) => d.category as string))]

// We need a way to map our data to where it should be rendered within the svg (in screen pixels), based on the data value,
// so the extents and the unique values above help us define the limits.
// Scales are just like mapping functions y = f(x), where x refers to domain, y refers to range.
// In our case, x should be the data, y should be the screen pixels.
// We have the margin here just to leave some space
// In viewport (our screen), the leftmost side always refer to 0 in the horizontal coordinates in pixels (x).
let xScale = d3.scaleBand()
.rangeRound([margin.left, size.width - margin.right])
.domain(xCategories)
.padding(0.1) // spacing between the categories

// In viewport (our screen), the topmost side always refer to 0 in the vertical coordinates in pixels (y).
let yScale = d3.scaleLinear()
.range([size.height - margin.bottom, margin.top]) //bottom side to the top side on the screen
.domain([0, yExtents[1]]) // This is based on your data, but if there is a natural value range for your data attribute, you should follow
// e.g., it is natural to define [0, 100] for the exame score, or [0, <maxVal>] for counts.

// There are other scales such as scaleOrdinal,
// whichever is appropriate depends on the data types and the kind of visualizations you're creating.

// This following part visualizes the axes along with axis labels.
// Check out https://observablehq.com/@d3/margin-convention?collection=@d3/d3-axis for more details
const xAxis = chartContainer.append('g')
.attr('transform', `translate(0, ${size.height - margin.bottom})`)
.call(d3.axisBottom(xScale))

const yAxis = chartContainer.append('g')
.attr('transform', `translate(${margin.left}, 0)`)
.call(d3.axisLeft(yScale))

const yLabel = chartContainer.append('g')
.attr('transform', `translate(${margin.left / 2}, ${size.height / 2}) rotate(-90)`)
.append('text')
.text('Value')
.style('font-size', '.8rem')

const xLabel = chartContainer.append('g')
.attr('transform', `translate(${(size.width - margin.left) / 2}, ${size.height - margin.top})`)
.append('text')
.text('Categories')
.style('font-size', '.8rem')

// "g" is grouping element that does nothing but helps avoid DOM looking like a mess
// We iterate through each <CategoricalBar> element in the array, create a rectangle for each and indicate the coordinates, the rectangle, and the color.
const chartBars = chartContainer.append('g')
.selectAll('rect')
.data<CategoricalBar>(bars) // TypeScript expression. This always expects an array of objects.
.join('rect')
// specify the left-top coordinate of the rectangle
.attr('x', (d: CategoricalBar) => xScale(d.category) as number)
.attr('y', (d: CategoricalBar) => yScale(d.value) as number)
// specify the size of the rectangle
.attr('width', xScale.bandwidth())
.attr('height', (d: CategoricalBar) => Math.abs(yScale(0) - yScale(d.value))) // this substraction is reversed so the result is non-negative
.attr('fill', 'teal')

// For transform, check out https://www.tutorialspoint.com/d3js/d3js_svg_transformation.htm, but essentially we are adjusting the positions of the selected elements.
const title = chartContainer.append('g')
.append('text') // adding the text
.attr('transform', `translate(${size.width / 2}, ${size.height - margin.top + 15})`)
.attr('dy', '0.5rem') // relative distance from the indicated coordinates.
.style('text-anchor', 'middle')
.style('font-weight', 'bold')
.text('Distribution of Demo Data') // text content
}

return (
<>
<div ref={barRef} className='chart-container'>
<svg id='bar-svg' width='100%' height='100%'></svg>
</div>
</>
)
}
24 changes: 24 additions & 0 deletions Homework2/mnakagawa/src/components/Notes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import { Paper, Divider, Button } from '@mui/material';
import { useState } from 'react';

// after the colon is the typing, before is how props are passed
export default function Notes({ msg }: { msg: string }) {
const [click, setClick] = useState(0);

return (
<>
<Paper elevation={1} style={{padding: '1rem'}}>
<h3>{msg}</h3>
<Divider />
<p>
Edit <code>src/components/Notes.tsx</code> to try different UI components from Material UI.
</p>
<p>
This template uses Material UI, a UI library based on Google's Material Design that can help you design the layout and populate template components in a consistent style.<br />
</p>
<Button variant='contained' onClick={() => setClick(click + 1)}>Have clicked this {click} times</Button>
</Paper>
</>
)
}
42 changes: 42 additions & 0 deletions Homework2/mnakagawa/src/components/NotesWithReducer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react'
import { Paper, Divider, Button } from '@mui/material';
import { createContext, useContext, useReducer, ReactNode } from 'react';
import { reducer, ACTIONS, State, Action } from '../stores/Reducer';

const CountContext = createContext<{ state: State, dispatch: React.Dispatch<Action> } | undefined>(undefined);

// Create provider component
export function CountProvider({ children }: {children: ReactNode}){
const [state, dispatch] = useReducer(reducer, { count: 0 });

return (
<CountContext.Provider value={{ state, dispatch }}>
{children}
</CountContext.Provider>
);
};

// after the colon is the typing, before is how props are passed
export function NotesWithReducer({ msg }: { msg: string }) {
const click = useContext(CountContext);

if (!click) throw new Error('Counter must be used within a CountProvider');

const { state, dispatch } = click;

return (
<>
<Paper elevation={1} style={{padding: '1rem'}}>
<h3>{msg}</h3>
<Divider />
<p>
Edit <code>src/components/Notes.tsx</code> to try different UI components from Material UI.
</p>
<p>
This template uses Material UI, a UI library based on Google's Material Design that can help you design the layout and populate template components in a consistent style.<br />
</p>
<Button variant='contained' onClick={() => dispatch({type: ACTIONS.INCREMENT})}>Have clicked this {state.count} times</Button>
</Paper>
</>
)
}
Loading