This visualization displays a stacked bar chart showing the breakdown of Uber trip purposes by starting location. The chart highlights:
The visualization uses data from data.csv
that includes:
Built using D3.js v7, the visualization:
After reviewing your files, I'll make a few improvements to create a better stacked bar chart.
index.js
/**
* D3 Script for Stacked Bar Chart of Uber Trip Purposes
*/
// 1. Setup Dimensions and Margins
const margin = {
top: 30,
right: 180,
bottom: 60,
left: 120,
};
const totalWidth = 960;
const totalHeight = 550;
const width = totalWidth - margin.left - margin.right;
const height = totalHeight - margin.top - margin.bottom;
const N_TOP_LOCATIONS = 15; // How many top locations to visualize
// 2. Create SVG Container
const svg = d3
.select('#stacked_bar_chart')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// 3. Tooltip Setup
const tooltip = d3
.select('body')
.append('div')
.attr('class', 'tooltip');
// 4. Load and Process Data using d3.csv
d3.csv('data.csv')
.then((data) => {
// Start processing after data is loaded
console.log('Raw data loaded:', data.length, 'rows');
// --- Data Cleaning & Initial Processing ---
let processedData = data
.filter(
(d) =>
d.START &&
d.START.trim() &&
d.START.trim().toLowerCase() !== 'totals',
)
.map((d) => ({
START: d.START.trim(),
MILES: +d.MILES,
PURPOSE: d.PURPOSE && d.PURPOSE.trim() ? d.PURPOSE.trim() : 'Unknown',
}))
.filter((d) => d.MILES && !isNaN(d.MILES) && d.MILES > 0);
console.log('Processed Data Count:', processedData.length);
if (processedData.length === 0) {
displayError("No valid data found after cleaning");
return;
}
// --- Aggregation 1: Calculate total miles per START location ---
const milesPerStart = d3.rollup(
processedData,
(v) => d3.sum(v, (d) => d.MILES),
(d) => d.START,
);
if (milesPerStart.size === 0) {
displayError("Could not group data by start location");
return;
}
// --- Get Top N START Locations (based on total mileage) ---
const topStartLocations = Array.from(milesPerStart.entries())
.sort(([, milesA], [, milesB]) => milesB - milesA)
.slice(0, N_TOP_LOCATIONS)
.map(([startLocation]) => startLocation);
if (topStartLocations.length === 0) {
displayError("No top locations found");
return;
}
// --- Filter processed data to include only trips from these top locations ---
const dataFilteredByTop = processedData.filter((d) =>
topStartLocations.includes(d.START),
);
if (dataFilteredByTop.length === 0) {
displayError("No data matches top locations");
return;
}
// --- Get Unique Purposes (Stack Keys) from the filtered data ---
const purposeKeys = [...new Set(dataFilteredByTop.map((d) => d.PURPOSE))].sort();
if (purposeKeys.length === 0) {
displayError("No trip purposes found");
return;
}
// --- Aggregation 2: Prepare data for stacking ---
const dataGroupedByStart = d3.group(dataFilteredByTop, (d) => d.START);
const dataReadyForStack = topStartLocations.map((startLoc) => {
const entry = {
START: startLoc,
total: milesPerStart.get(startLoc) || 0,
};
const tripsFromLoc = dataGroupedByStart.get(startLoc) || [];
purposeKeys.forEach((purpose) => {
entry[purpose] = d3.sum(
tripsFromLoc.filter((d) => d.PURPOSE === purpose),
(d) => d.MILES,
);
});
return entry;
});
if (dataReadyForStack.length === 0) {
displayError("Failed to prepare data for stacking");
return;
}
// 5. Define Scales
// Y Scale (Categorical: Start Locations)
const yScale = d3
.scaleBand()
.domain(topStartLocations)
.range([0, height])
.padding(0.15);
// X Scale (Linear: Miles)
const maxTotalMiles = d3.max(dataReadyForStack, (d) => d.total);
const xScale = d3
.scaleLinear()
.domain([0, maxTotalMiles * 1.05])
.range([0, width]);
// Color Scale (Ordinal: Trip Purposes)
const colorScale = d3
.scaleOrdinal()
.domain(purposeKeys)
.range(d3.schemeTableau10);
// 6. Create D3 Stack Layout Generator
const stack = d3
.stack()
.keys(purposeKeys)
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone);
// Apply the stack generator to the data
const series = stack(dataReadyForStack);
if (!series || series.length === 0 || series[0].length === 0) {
displayError("Failed to generate stack layers");
return;
}
// 7. Draw the Stacked Bars (Rectangles)
svg
.append('g')
.selectAll('g')
.data(series)
.join('g')
.attr('fill', (d) => colorScale(d.key))
.attr('class', (d) => `layer-${d.key.replace(/[\s/]+/g, '-')}`)
.selectAll('rect')
.data((d) => d)
.join('rect')
.attr('class', 'bar-segment')
.attr('y', (d) => yScale(d.data.START) || 0)
.attr('x', (d) => !isNaN(xScale(d[0])) ? xScale(d[0]) : 0)
.attr('width', (d) => {
const startX = xScale(d[0]);
const endX = xScale(d[1]);
const widthVal = !isNaN(startX) && !isNaN(endX) ? endX - startX : 0;
return Math.max(0, widthVal);
})
.attr('height', yScale.bandwidth())
.on('mouseover', function (event, d) {
// Highlight effect
d3.select(this)
.style('opacity', 0.7)
.style('stroke', '#000')
.style('stroke-width', '1px');
// Get data for tooltip
const purpose = d3.select(this.parentNode).datum().key;
const startLoc = d.data.START;
const milesForPurpose = d.data[purpose];
const totalMiles = d.data.total;
const percentage = totalMiles > 0 ? ((milesForPurpose / totalMiles) * 100).toFixed(1) : 0;
// Show tooltip
tooltip
.transition()
.duration(100)
.style('opacity', 1);
tooltip
.html(`
<strong>Location:</strong> ${startLoc}<br>
<strong>Purpose:</strong> ${purpose}<br>
<strong>Miles:</strong> ${milesForPurpose.toFixed(1)}<br>
<strong>Total Miles:</strong> ${totalMiles.toFixed(1)}<br>
<strong>Percentage:</strong> ${percentage}%
`)
.style('left', event.pageX + 15 + 'px')
.style('top', event.pageY - 28 + 'px');
})
.on('mouseout', function () {
// Remove highlight
d3.select(this)
.style('opacity', 1)
.style('stroke', 'none');
// Hide tooltip
tooltip
.transition()
.duration(200)
.style('opacity', 0);
});
// 8. Add Axes
// X Axis (Miles)
svg
.append('g')
.attr('class', 'axis x-axis')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale).ticks(width / 80).tickFormat(d3.format(',')));
// Y Axis (Start Locations)
svg
.append('g')
.attr('class', 'axis y-axis')
.call(d3.axisLeft(yScale));
// Add X Axis Label
svg
.append('text')
.attr('class', 'axis-label')
.attr('x', width / 2)
.attr('y', height + margin.bottom * 0.7)
.attr('text-anchor', 'middle')
.text('Total Miles Driven');
// Add Y Axis Label
svg
.append('text')
.attr('class', 'axis-label')
.attr('transform', 'rotate(-90)')
.attr('x', -height / 2)
.attr('y', -margin.left * 0.8)
.attr('text-anchor', 'middle')
.text('Start Location');
// Add Title
svg
.append('text')
.attr('class', 'chart-title')
.attr('x', width / 2)
.attr('y', -10)
.attr('text-anchor', 'middle')
.style('font-weight', 'bold')
.style('font-size', '16px')
.text('Business Trip Purposes by Start Location');
// 9. Add Legend
const legend = svg
.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${width + 25}, 0)`);
const legendItemHeight = 20;
legend
.selectAll('.legend-item')
.data(purposeKeys.reverse())
.join('g')
.attr('class', 'legend-item')
.attr('transform', (d, i) => `translate(0, ${i * legendItemHeight})`)
.each(function (purpose) {
const item = d3.select(this);
item
.append('rect')
.attr('width', 15)
.attr('height', 15)
.attr('fill', colorScale(purpose));
item
.append('text')
.attr('x', 20)
.attr('y', 7.5)
.attr('dy', '0.35em')
.style('font-size', '11px')
.text(purpose);
});
// Legend Title
legend
.append('text')
.attr('x', 0)
.attr('y', -10)
.style('font-weight', 'bold')
.style('text-anchor', 'start')
.text('Trip Purpose');
// Add a "total miles" annotation at the end of each bar
svg.selectAll('.total-label')
.data(dataReadyForStack)
.join('text')
.attr('class', 'total-label')
.attr('x', d => xScale(d.total) + 5)
.attr('y', d => yScale(d.START) + yScale.bandwidth() / 2)
.attr('dy', '0.35em')
.attr('text-anchor', 'start')
.style('font-size', '10px')
.text(d => d3.format(',.1f')(d.total) + ' mi');
})
.catch((error) => {
console.error('Error loading or processing data:', error);
displayError("Failed to load or process data");
});
// Helper function to display errors
function displayError(message) {
console.error(message);
svg
.append('text')
.attr('x', width / 2)
.attr('y', height / 2)
.attr('text-anchor', 'middle')
.style('fill', 'red')
.text(message);
}
styles.css
body {
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
margin: 30px;
background-color: #f9f9f9;
color: #333;
line-height: 1.5;
}
h1 {
text-align: center;
font-size: 1.8em;
margin-bottom: 30px;
color: #3a3a3a;
}
#stacked_bar_chart {
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 5px;
padding: 10px;
margin: 0 auto;
max-width: 1000px;
}
/* Axes styling */
.axis path,
.axis line {
fill: none;
stroke: #ddd;
shape-rendering: crispEdges;
}
.axis text {
font-size: 11px;
fill: #555;
font-family: sans-serif;
}
.axis-label {
font-size: 12px;
font-weight: bold;
fill: #333;
text-anchor: middle;
}
/* Chart title */
.chart-title {
font-size: 16px;
font-weight: bold;
}
/* Bar segments */
.bar-segment {
transition: opacity 0.2s, stroke 0.2s;
}
.bar-segment:hover {
opacity: 0.8;
cursor: pointer;
stroke: #000;
stroke-width: 1px;
}
/* Legend styling */
.legend {
font-size: 11px;
}
.legend rect {
stroke-width: 0.5px;
stroke: #888;
}
.legend text {
dominant-baseline: middle;
font-family: sans-serif;
fill: #333;
}
/* Tooltip styling */
.tooltip {
position: absolute;
text-align: left;
padding: 10px;
font: 12px sans-serif;
background: rgba(45, 45, 45, 0.9);
color: white;
border: 0px;
border-radius: 4px;
pointer-events: none;
opacity: 0;
white-space: nowrap;
transition: opacity 0.2s;
z-index: 10;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
line-height: 1.5;
}
.tooltip strong {
color: #77ddff;
}
/* Total miles labels */
.total-label {
fill: #555;
font-weight: 500;
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Uber Trip Purposes by Start Location</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<h1>Analysis of Uber Trip Purposes by Starting Location</h1>
<div id="stacked_bar_chart"></div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="index.js"></script>
</body>
</html>
README.md
# Uber Trip Purposes Analysis
This visualization displays a stacked bar chart showing the breakdown of Uber trip purposes by starting location. The chart highlights:
- Top starting locations by total mileage
- Breakdown of miles by trip purpose at each location
- Interactive tooltips with detailed information
## Features
- Responsive design with modern styling
- Interactive tooltips showing detailed information
- Color-coded trip purposes with a clear legend
- Total miles label for each location
## Data Structure
The visualization uses data from `data.csv` that includes:
- START_DATE, END_DATE: Date and time of trip
- CATEGORY: Trip category (e.g., Business)
- START, STOP: Start and end locations
- MILES: Distance traveled
- PURPOSE: Purpose of the trip (e.g., Meeting, Customer Visit)
## Implementation
Built using D3.js v7, the visualization:
1. Processes the raw data to extract top starting locations
2. Creates stacked bars for different trip purposes
3. Implements interactive elements for better user experience
myMessage.js
The improvements I made include:
The chart now provides a clearer visualization of trip purposes by start location with improved usability and aesthetics.