feat: added sample coverage chart
This commit is contained in:
@@ -180,8 +180,7 @@ export async function getAnnotatedSamples(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.where(eq(dataset_entry.datasetId, datasetId))
|
.where(eq(dataset_entry.datasetId, datasetId))
|
||||||
.groupBy(dataset_entry.id)
|
.groupBy(dataset_entry.id);
|
||||||
.having((group) => gt(count(annotation.id), 0));
|
|
||||||
|
|
||||||
// Type assertion for the count
|
// Type assertion for the count
|
||||||
return samples
|
return samples
|
||||||
|
|||||||
@@ -68,6 +68,26 @@ export default function AnnotationDistribution({ experimentId }: AnnotationDistr
|
|||||||
...item,
|
...item,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
|
// Calculate per-sample distribution (histogram of annotation counts)
|
||||||
|
const sampleDistribution: { annotationCount: number; sampleCount: number; percentage: string }[] = [];
|
||||||
|
const countMap = new Map<number, number>();
|
||||||
|
|
||||||
|
samples.forEach((sample) => {
|
||||||
|
const count = sample.annotationCount;
|
||||||
|
countMap.set(count, (countMap.get(count) || 0) + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to sorted array with percentages
|
||||||
|
const totalSamples = samples.length;
|
||||||
|
Array.from(countMap.entries())
|
||||||
|
.map(([count, sampleCount]) => ({
|
||||||
|
annotationCount: count,
|
||||||
|
sampleCount,
|
||||||
|
percentage: totalSamples > 0 ? ((sampleCount / totalSamples) * 100).toFixed(1) : '0',
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.annotationCount - b.annotationCount)
|
||||||
|
.forEach((item) => sampleDistribution.push(item));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -157,6 +177,54 @@ export default function AnnotationDistribution({ experimentId }: AnnotationDistr
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Sample Annotation Distribution Chart */}
|
||||||
|
{!samplesLoading && sampleDistribution.length > 0 && (
|
||||||
|
<div className="mt-8">
|
||||||
|
<h2 className="text-xl font-bold mb-4 text-gray-900">Sample Annotation Coverage</h2>
|
||||||
|
<div className="h-[400px]">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
data={sampleDistribution}
|
||||||
|
margin={{ top: 20, right: 30, left: 20, bottom: 20 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="annotationCount"
|
||||||
|
label={{ value: 'Annotations per Sample', position: 'bottom', offset: 10 }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
label={{ value: 'Percentage of Samples (%)', angle: -90, position: 'insideLeft' }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
content={({ active, payload }) => {
|
||||||
|
if (active && payload && payload.length) {
|
||||||
|
const data = payload[0].payload;
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-3 border border-gray-300 rounded shadow-lg">
|
||||||
|
<p className="font-semibold text-gray-900">
|
||||||
|
{data.annotationCount} annotation{data.annotationCount !== 1 ? 's' : ''}
|
||||||
|
</p>
|
||||||
|
<p className="text-green-600">{data.percentage}% of samples</p>
|
||||||
|
<p className="text-gray-600 text-sm">({data.sampleCount} sample{data.sampleCount !== 1 ? 's' : ''})</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend wrapperStyle={{ paddingTop: '20px' }} />
|
||||||
|
<Bar
|
||||||
|
dataKey="percentage"
|
||||||
|
fill="#10b981"
|
||||||
|
name="Percentage (%)"
|
||||||
|
barCategoryGap="5%"
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user