Skip to content

Commit

Permalink
Merge branch 'development' of https://github.com/Steinbeck-Lab/coconut
Browse files Browse the repository at this point in the history
…into development
  • Loading branch information
CS76 committed Dec 17, 2024
2 parents 4daa7a1 + c1d14a4 commit c75c65b
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 0 deletions.
79 changes: 79 additions & 0 deletions app/Console/Commands/GenerateHeatMapData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?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();
$molecule_identifiers = array_map(function ($item) {
return preg_replace('/^CNP/i', '', $item);
}, $molecule_identifiers);
$heat_map_data['ids'][$collection->id . '|' . $collection->title] = $molecule_identifiers;
}

// Calculate percentage overlaps -> ol_d = overlap data
$heat_map_data['ol_d'] = [];
$collection_keys = array_keys($heat_map_data['ids']);

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

foreach ($collection_keys as $collection2_key) {
$set2 = array_unique($heat_map_data['ids'][$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['ol_d'][$collection1_key][$collection2_key] = round($overlap_percentage, 2);

// Add additional overlap statistics -> ol_s = overlap_stats
$heat_map_data['ol_s'][$collection1_key][$collection2_key] = [
// ol = overlap count
'ol' => $intersection_count,
'c1_count' => $set1_count,
'c2_count' => $set2_count,
'p' => round($overlap_percentage, 2),
];
}
}
unset($heat_map_data['ids']);

$json = json_encode($heat_map_data, JSON_UNESCAPED_SLASHES);

// 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');
}
}
37 changes: 37 additions & 0 deletions app/Livewire/CollectionOverlap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?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['ol_d'];

}

public function render()
{
return view('livewire.collection-overlap', [
'collectionsData' => json_encode($this->collections),
]);
}
}
1 change: 1 addition & 0 deletions public/reports/heat_map_metadata.json

Large diffs are not rendered by default.

188 changes: 188 additions & 0 deletions resources/views/livewire/collection-overlap.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<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));
// 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} vs. ${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>

0 comments on commit c75c65b

Please sign in to comment.