Export Image
Export Code
Connected

Activity6_Task2

king-p-code

0 views in last 90 days
Last edited Apr 07, 2025
Created on Apr 06, 2025
Forked from Hello VizHub

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
MIT Licensed

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:

  1. Cleaned up the code and improved error handling
  2. Enhanced the visual design with better styling, spacing, and colors
  3. Added a chart title and improved the legend appearance
  4. Added total miles labels at the end of each bar for easier comparison
  5. Improved the tooltip styling and interaction
  6. Added a comprehensive README.md file explaining the visualization
  7. Removed the unused myMessage.js file
  8. Made the HTML more semantic with proper language attribute and viewport meta tag

The chart now provides a clearer visualization of trip purposes by start location with improved usability and aesthetics.