Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat charts #326

Merged
merged 5 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
74 changes: 74 additions & 0 deletions app/Console/Commands/GenerateHeatMapData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace App\Console\Commands;

use App\Models\Collection;
use Illuminate\Console\Command;

class GenerateHeatMapData extends Command
{
protected $signature = 'coconut:generate-heat-map-data';

protected $description = 'This generates Heat Map data for collection overlaps.';

public function handle()
{
$heat_map_data = [];
$collections = Collection::all();

// Store molecule identifiers
foreach ($collections as $collection) {
$molecule_identifiers = $collection->molecules()->pluck('identifier')->toArray();
$heat_map_data['identifier_data'][$collection->id.'|'.$collection->title] = $molecule_identifiers;
}

// Calculate percentage overlaps
$heat_map_data['overlap_data'] = [];
$collection_keys = array_keys($heat_map_data['identifier_data']);

foreach ($collection_keys as $collection1_key) {
$heat_map_data['overlap_data'][$collection1_key] = [];
$set1 = array_unique($heat_map_data['identifier_data'][$collection1_key]);
$set1_count = count($set1);

foreach ($collection_keys as $collection2_key) {
$set2 = array_unique($heat_map_data['identifier_data'][$collection2_key]);
$set2_count = count($set2);

// Calculate intersection
$intersection = array_intersect($set1, $set2);
$intersection_count = count($intersection);

// Calculate percentage overlap
if ($set1_count > 0 && $set2_count > 0) {
// Using Jaccard similarity: intersection size / union size
$union_count = $set1_count + $set2_count - $intersection_count;
$overlap_percentage = ($intersection_count / $union_count) * 100;
} else {
$overlap_percentage = 0;
}

$heat_map_data['overlap_data'][$collection1_key][$collection2_key] = round($overlap_percentage, 2);

// Add additional overlap statistics
$heat_map_data['overlap_stats'][$collection1_key][$collection2_key] = [
'intersection_count' => $intersection_count,
'collection1_count' => $set1_count,
'collection2_count' => $set2_count,
'percentage' => round($overlap_percentage, 2),
];
}
}

$json = json_encode($heat_map_data, JSON_PRETTY_PRINT);

// Save the JSON to a file
$filePath = public_path('reports/heat_map_metadata.json');
if (! file_exists(dirname($filePath))) {
mkdir(dirname($filePath), 0777, true);
}
file_put_contents($filePath, $json);

$this->info('JSON metadata saved to public/reports/heat_map_metadata.json');
}
}
54 changes: 54 additions & 0 deletions app/Livewire/CollectionOverlap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace App\Livewire;

use Livewire\Component;

class CollectionOverlap extends Component
{
public $collections = [];

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

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

$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->collections = $decodedData['overlap_data'];
// Example data structure - replace with your actual data fetching logic
// $this->collections = [
// 'Collection A' => [
// 'Collection A' => 100,
// 'Collection B' => 75,
// 'Collection C' => 45,
// ],
// 'Collection B' => [
// 'Collection A' => 75,
// 'Collection B' => 100,
// 'Collection C' => 60,
// ],
// 'Collection C' => [
// 'Collection A' => 45,
// 'Collection B' => 60,
// 'Collection C' => 100,
// ],
// ];
}

public function render()
{
return view('livewire.collection-overlap', [
'collectionsData' => json_encode($this->collections),
]);
}
}
189 changes: 189 additions & 0 deletions resources/views/livewire/collection-overlap.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<div>
<h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl mb-5 ">
Collection Overlap Heatmap
</h2>
<div id="heatmap" class="w-full">
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
const data = JSON.parse(@js($collectionsData));
const statsData = JSON.parse(@js($statsData ?? '{}')); // Get the stats data if available

// Clear any existing chart
d3.select("#heatmap").selectAll("*").remove();

// Dynamic sizing
const containerWidth = document.getElementById('heatmap').offsetWidth;
const margin = {
top: 10,
right: 60, // Increased right margin for vertical legend
bottom: 120,
left: 120
};
const width = containerWidth - margin.left - margin.right;
const height = Math.min(800, width);

// Create SVG
const svg = d3.select("#heatmap")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);

// Get collection names
const collections = Object.keys(data).map(key => key.split('|')[1]);
console.log(collections)

// Create scales
const x = d3.scaleBand()
.range([0, width])
.domain(collections)
.padding(0.05);

const y = d3.scaleBand()
.range([0, height])
.domain(collections)
.padding(0.05);

const color = d3.scaleSequential()
.interpolator(d3.interpolateBlues)
.domain([0, 100]);

// Add X axis
svg.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.style("font-size", "12px");

// Add Y axis
svg.append("g")
.call(d3.axisLeft(y))
.selectAll("text")
.style("font-size", "12px");

// Create tooltip with enhanced styling
const tooltip = d3.select("#heatmap")
.append("div")
.style("position", "absolute")
.style("visibility", "hidden")
.style("background-color", "rgba(0, 0, 0, 0.85)")
.style("color", "white")
.style("padding", "12px")
.style("border-radius", "6px")
.style("font-size", "14px")
.style("box-shadow", "0 4px 6px rgba(0,0,0,0.3)")
.style("max-width", "300px");

// Add cells
Object.keys(data).forEach(rowKey => {
Object.keys(data).forEach(colKey => {
const rowName = rowKey.split('|')[1];
const colName = colKey.split('|')[1];

svg.append("rect")
.attr("x", x(colName))
.attr("y", y(rowName))
.attr("width", x.bandwidth())
.attr("height", y.bandwidth())
.style("fill", color(data[rowKey][colKey]))
.style("stroke", "white")
.style("stroke-width", 1)
.on("mouseover", function(event) {
d3.select(this)
.style("stroke", "#2563eb")
.style("stroke-width", 2);

tooltip
.style("visibility", "visible")
.html(`
<div class="font-bold mb-1">${rowName} → ${colName}</div>
<div>Overlap: ${data[rowKey][colKey].toFixed(1)}%</div>
`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 10) + "px");
})
.on("mouseout", function() {
d3.select(this)
.style("stroke", "white")
.style("stroke-width", 1);
tooltip.style("visibility", "hidden");
});
});
});
// Add title
// svg.append("text")
// .attr("x", width / 2)
// .attr("y", -margin.top / 2)
// .attr("text-anchor", "middle")
// .style("font-size", "20px")
// .style("font-weight", "bold")
// .text("Collection Overlap Heatmap");

// Vertical legend
const legendWidth = 20;
const legendHeight = height * 0.6;

// Create legend group
const legend = svg.append("g")
.attr("transform", `translate(${width + 40},${(height - legendHeight) / 2})`);

// Create gradient
const defs = legend.append("defs");
const gradient = defs.append("linearGradient")
.attr("id", "heatmap-gradient")
.attr("x1", "0%")
.attr("x2", "0%")
.attr("y1", "100%")
.attr("y2", "0%");

// Add gradient stops
const stops = d3.range(0, 1.1, 0.1);
stops.forEach(stop => {
gradient.append("stop")
.attr("offset", stop * 100 + "%")
.attr("stop-color", color(stop * 100));
});

// Add legend rectangle
legend.append("rect")
.attr("width", legendWidth)
.attr("height", legendHeight)
.style("fill", "url(#heatmap-gradient)");

// Create scale for legend axis
const legendScale = d3.scaleLinear()
.domain([0, 100])
.range([legendHeight, 0]);

// Add legend axis
const legendAxis = d3.axisRight(legendScale)
.ticks(5)
.tickFormat(d => d + "%");

legend.append("g")
.attr("transform", `translate(${legendWidth},0)`)
.call(legendAxis);

// Add legend title
legend.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -legendHeight / 2)
.attr("y", -30)
.attr("text-anchor", "middle")
.style("font-size", "12px")
.text("Overlap Percentage");
});

// Add resize handler
window.addEventListener('resize', () => {
initHeatmap();
});
</script>
3 changes: 3 additions & 0 deletions resources/views/livewire/stats.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ class="pointer-events-none absolute inset-0 rounded-xl ring-1 ring-inset ring-gr
'chartData' => $chartData,
])
@endforeach
</div class="mx-auto max-w-6xl pb-32 px-8 w-full">
<div>
@livewire('collection-overlap')
</div>
</div>
</div>