Skip to content

Commit

Permalink
feat: new bubble frequency chart and data
Browse files Browse the repository at this point in the history
  • Loading branch information
sriramkanakam87 committed Dec 12, 2024
1 parent 0f7fc4c commit b57ee98
Show file tree
Hide file tree
Showing 5 changed files with 245 additions and 20 deletions.
42 changes: 42 additions & 0 deletions app/Livewire/BubbleFrequencyPlot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace App\Livewire;

use Livewire\Component;

class BubbleFrequencyPlot extends Component
{
public $chartData;

public $firstColumnName;

public $secondColumnName;

public $chartId;

public $name_corrections = [

];

public function mount($chartName, $chartData)
{
// Extract column names
[$this->firstColumnName, $this->secondColumnName] = explode('|', $chartName);

// Ensure data is properly formatted
$this->chartData = array_map(function ($item) {
return [
'column_values' => $item['column_values'],
'first_column_count' => $item['first_column_count'] ?? 0,
'second_column_count' => $item['second_column_count'] ?? 0,
];
}, $chartData);

$this->chartId = 'bubble-chart-'.uniqid();
}

public function render()
{
return view('livewire.bubble-frequency-plot');
}
}
55 changes: 37 additions & 18 deletions app/Livewire/Stats.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,53 @@ class Stats extends Component
{
public $properties_json_data = [];

public $bubble_frequency_json_data = [];

public function mount()
{
$jsonPath = public_path('reports/density_charts.json');

try {
if (! file_exists($jsonPath)) {
throw new \Exception('Density chart data file not found');
// update the switch cases when you add new paths to this
$plotDataFiles = [
'reports/density_charts.json',
'reports/bubble_frequency_charts.json',
];

foreach ($plotDataFiles as $filePath) {
$plotJson = public_path($filePath);

try {
if (! file_exists($plotJson)) {
throw new \Exception('Density chart data file not found');
}

$jsonContent = file_get_contents($plotJson);
$decodedData = json_decode($jsonContent, true);

if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Error decoding JSON data: '.json_last_error_msg());
}

// Store in the corresponding public properties - updade this when adding a new path
switch ($filePath) {
case 'reports/density_charts.json':
$this->properties_json_data = $decodedData['properties'];
break;
case 'reports/bubble_frequency_charts.json':
$this->bubble_frequency_json_data = $decodedData;
break;
default:
break;
}
} catch (\Exception $e) {
\Log::error('Failed to load '.$filePath.' chart data: '.$e->getMessage());
}

$jsonContent = file_get_contents($jsonPath);
$decodedData = json_decode($jsonContent, true);

if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Error decoding JSON data: '.json_last_error_msg());
}

// Store in the public property
$this->properties_json_data = $decodedData['properties'];

} catch (\Exception $e) {
\Log::error('Failed to load density chart data: '.$e->getMessage());
}
}

public function render()
{
return view('livewire.stats', [
'properties_json_data' => $this->properties_json_data,
'bubble_frequency_json_data' => $this->bubble_frequency_json_data,
]);
}
}
1 change: 1 addition & 0 deletions public/reports/bubble_frequency_charts.json

Large diffs are not rendered by default.

157 changes: 157 additions & 0 deletions resources/views/livewire/bubble-frequency-plot.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
{{-- resources/views/livewire/word-bubble-chart.blade.php --}}
<div>
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl mb-1">
{!! $name_corrections[$firstColumnName] ?? ucfirst(str_replace('_', ' ', $firstColumnName)) !!} vs {!! $name_corrections[$secondColumnName] ?? ucfirst(str_replace('_', ' ', $secondColumnName)) !!}
</h2>
<div id="{{$chartId}}"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const data = @json($chartData);
const width = 800;
const height = 600;
// Create SVG
const svg = d3.select('#{{$chartId}}')
.append('svg')
.attr('width', width)
.attr('height', height);
// Calculate radius based on total value
const radiusScale = d3.scaleSqrt()
.domain([0, d3.max(data, d => d.first_column_count + d.second_column_count)])
.range([30, 80]);
// Create force simulation
const simulation = d3.forceSimulation(data)
.force('charge', d3.forceManyBody().strength(5))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(d => radiusScale(d.first_column_count + d.second_column_count) + 2));
// Create container for each bubble
const bubbles = svg.selectAll('.bubble')
.data(data)
.enter()
.append('g')
.attr('class', 'bubble');
// Create pie generator for split circles
const pie = d3.pie()
.value(d => d.value)
.sort(null);
// Create arc generator
const arc = d3.arc()
.innerRadius(0);
// Add split circles
bubbles.each(function(d) {
const radius = radiusScale(d.first_column_count + d.second_column_count);
const g = d3.select(this);
// Prepare data for pie
const pieData = pie([{
value: d.first_column_count,
color: '#99B3FF'
},
{
value: d.second_column_count,
color: '#FFB3B3'
}
]);
// Create arcs
arc.outerRadius(radius);
g.selectAll('path')
.data(pieData)
.enter()
.append('path')
.attr('d', arc)
.style('fill', d => d.data.color);
// Add word label
g.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '-0.2em')
.style('font-size', `${radius * 0.3}px`)
.style('fill', '#333')
.text(d.word);
// Add values label
g.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '-0.2em') // Move text up a bit from center
.style('font-size', `${radius * 0.25}px`)
.style('fill', '#666')
.style('font-weight', 'bold')
.text(d => d.column_values)
.call(wrapText, radius * 1.2);
});
// Update bubble positions on simulation tick
simulation.on('tick', () => {
bubbles.attr('transform', d => `translate(${d.x},${d.y})`);
});
function wrapText(text, width) {
text.each(function() {
const text = d3.select(this);
const words = text.text().split(/\s+/);
const lineHeight = 1.1;
text.text(null);
// Calculate total lines for vertical centering
let lines = [];
let currentLine = [];
let tempText = text.append('tspan').attr('x', 0);
// First pass: determine number of lines
for (let word of words) {
currentLine.push(word);
tempText.text(currentLine.join(' '));
if (tempText.node().getComputedTextLength() > width) {
currentLine.pop();
lines.push(currentLine.join(' '));
currentLine = [word];
}
}
lines.push(currentLine.join(' '));
tempText.remove();
// Calculate starting position to center vertically
const totalLines = lines.length;
const startDy = (-((totalLines - 1) * lineHeight) / 2) + 'em';
// Second pass: actually create the lines
let tspan = text.append('tspan')
.attr('x', 0)
.attr('dy', startDy)
.text(lines[0]);
for (let i = 1; i < lines.length; i++) {
text.append('tspan')
.attr('x', 0)
.attr('dy', `${lineHeight}em`)
.text(lines[i]);
}
});
}
});
</script>

<style>
.bubble:hover {
opacity: 0.8;
cursor: pointer;
}
.bubble text {
pointer-events: none;
}
</style>
</div>
10 changes: 8 additions & 2 deletions resources/views/livewire/stats.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,13 @@ class="pointer-events-none absolute inset-0 rounded-xl ring-1 ring-inset ring-gr
@endforeach
</div>
</div>


<div class="mx-auto max-w-6xl pb-32 px-8 w-full">
@foreach ($bubble_frequency_json_data as $chartName => $chartData)
@livewire('bubble-frequency-plot', [
'chartName' => $chartName,
'chartData' => $chartData,
])
@endforeach
</div>
</div>
</div>

0 comments on commit b57ee98

Please sign in to comment.