Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions apps/web/src/app/visualization/ScientificDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client'

import { ExpressionChart } from '@/components/scientific/ExpressionChart'
import { TP53_PATHWAY_EXPRESSION_FIXTURE } from '@/lib/scientific/expression.fixtures'
import { useExpressionChart } from '@/lib/scientific/useExpressionChart'

/**
* Client-side demonstration of the Phase 6.7 Scientific Charts.
*
* Loads a deterministic development fixture (see
* `lib/scientific/expression.fixtures.ts`) through the same data lifecycle as
* a production dataset. The loader flips to `fetchExpressionDataset` once the
* backend exposes an expression endpoint.
*/
export function ScientificDemo() {
const result = useExpressionChart({
loader: (signal) => {
if (signal.aborted) return Promise.reject(new DOMException('Aborted', 'AbortError'))
return Promise.resolve(TP53_PATHWAY_EXPRESSION_FIXTURE)
},
})

return <ExpressionChart result={result} title="Expression Chart" />
}
13 changes: 8 additions & 5 deletions apps/web/src/app/visualization/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { GeneTranscriptDemo } from './GeneTranscriptDemo'
import { GenomeBrowserDemo } from './GenomeBrowserDemo'
import { NetworkDemo } from './NetworkDemo'
import { ProteinDemo } from './ProteinDemo'
import { ScientificDemo } from './ScientificDemo'
import { VisualizationDemo } from './VisualizationDemo'

export const metadata: Metadata = {
title: 'Visualization — GenomeAI',
description:
'Visualization foundation, Genome Browser, Gene / Transcript viewer, Variant track, Protein Viewer, and Biological Network Viewer (Phase 6.1–6.6) for GenomeAI.',
'Visualization foundation, Genome Browser, Gene / Transcript viewer, Variant track, Protein Viewer, Biological Network Viewer, and Scientific Charts (Phase 6.1–6.7) for GenomeAI.',
}

export default function VisualizationPage() {
Expand All @@ -19,16 +20,18 @@ export default function VisualizationPage() {
<h1 className="text-2xl font-bold text-gray-900">Visualization</h1>
<p className="text-sm text-gray-600">
Phase 6.1 foundation, the Phase 6.2 Genome Browser, the Phase 6.3 Gene / Transcript
viewer, the Phase 6.4 Variant track, the Phase 6.5 Protein Viewer, and the Phase 6.6
Biological Network Viewer — region parsing, viewport navigation, track rendering,
gene/transcript structure, point variants, protein sequence + annotation windows, and
deterministic relationship networks over the GenomeAI API and development fixtures.
viewer, the Phase 6.4 Variant track, the Phase 6.5 Protein Viewer, the Phase 6.6
Biological Network Viewer, and the Phase 6.7 Scientific Charts — region parsing, viewport
navigation, track rendering, gene/transcript structure, point variants, protein sequence +
annotation windows, deterministic relationship networks, and expression charts over the
GenomeAI API and development fixtures.
</p>
</div>
<GenomeBrowserDemo />
<GeneTranscriptDemo />
<NetworkDemo />
<ProteinDemo />
<ScientificDemo />
<VisualizationDemo />
</main>
)
Expand Down
138 changes: 138 additions & 0 deletions apps/web/src/components/scientific/ChartAxes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
'use client'

import type { PlotArea } from '@/lib/scientific/geometry'
import { categoryLabelTicks } from '@/lib/scientific/scale'
import type { CategoryScale, ContinuousScale } from '@/lib/scientific/scale'

export interface ChartAxesProps {
/** Data plot area (see `lib/scientific/geometry.ts`). */
plot: PlotArea
/** Sample categories on the x-axis. */
xScale: CategoryScale
/** Value scale on the y-axis. */
yScale: ContinuousScale
/** Ascending tick values to render gridlines + labels for. */
yTicks: number[]
/** Optional x-axis caption (e.g. "Sample"). */
xLabel?: string
/** Optional y-axis caption (e.g. "Expression value"). */
yLabel?: string
/** Renders a tick value as a label (defaults to scientific formatting). */
formatValue?: (value: number) => string
}

const AXIS_COLOR = '#cbd5e1'
const GRID_COLOR = '#e2e8f0'
const TICK_COLOR = '#475569'
const CAPTION_COLOR = '#94a3b8'

/**
* Reusable SVG axes for scientific charts: gridlines, y tick labels, the
* sample labels along the x-axis, and optional axis captions. Pure layout is
* computed from the supplied scales so the component stays presentation-only.
*/
export function ChartAxes({
plot,
xScale,
yScale,
yTicks,
xLabel,
yLabel,
formatValue = (value) => String(value),
}: ChartAxesProps) {
const baselineY = plot.y0 + plot.height
const sampleTicks = categoryLabelTicks(xScale, plot.width)

return (
<g data-testid="chart-axes">
<g data-testid="chart-grid">
{yTicks.map((tick) => {
const y = yScale.toPixel(tick)
return (
<line
key={tick}
x1={plot.x0}
y1={y}
x2={plot.x0 + plot.width}
y2={y}
stroke={GRID_COLOR}
strokeWidth={1}
/>
)
})}
</g>
<g data-testid="chart-y-ticks">
{yTicks.map((tick) => (
<text
key={tick}
x={plot.x0 - 8}
y={yScale.toPixel(tick)}
textAnchor="end"
dominantBaseline="middle"
fontSize={11}
fill={TICK_COLOR}
>
{formatValue(tick)}
</text>
))}
</g>
{yLabel !== undefined ? (
<text
x={8}
y={plot.y0 + plot.height / 2}
transform={`rotate(-90 8 ${plot.y0 + plot.height / 2})`}
textAnchor="middle"
fontSize={12}
fill={CAPTION_COLOR}
data-testid="chart-y-label"
>
{yLabel}
</text>
) : null}
<g data-testid="chart-x-labels">
{sampleTicks.map((tick) =>
tick.visible ? (
<text
key={tick.sample}
x={tick.x}
y={baselineY + 18}
textAnchor="middle"
fontSize={11}
fill={TICK_COLOR}
>
{tick.sample}
</text>
) : null,
)}
</g>
{xLabel !== undefined ? (
<text
x={plot.x0 + plot.width / 2}
y={baselineY + 40}
textAnchor="middle"
fontSize={12}
fill={CAPTION_COLOR}
data-testid="chart-x-label"
>
{xLabel}
</text>
) : null}
<line
x1={plot.x0}
y1={baselineY}
x2={plot.x0 + plot.width}
y2={baselineY}
stroke={AXIS_COLOR}
strokeWidth={1}
/>
<line
x1={plot.x0}
y1={plot.y0}
x2={plot.x0}
y2={baselineY}
stroke={AXIS_COLOR}
strokeWidth={1}
/>
</g>
)
}
37 changes: 37 additions & 0 deletions apps/web/src/components/scientific/ChartLegend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client'

export interface ChartLegendItem {
id: string
label: string
color: string
}

export interface ChartLegendProps {
items: ChartLegendItem[]
}

/**
* Reusable series legend: a labelled list of color swatches. Rendered as a
* semantic list so screen readers announce each series.
*/
export function ChartLegend({ items }: ChartLegendProps) {
if (items.length === 0) return null
return (
<ul
className="flex w-full flex-wrap items-center gap-x-4 gap-y-1"
aria-label="Series legend"
data-testid="chart-legend"
>
{items.map((item) => (
<li key={item.id} className="flex items-center gap-1.5 text-xs text-gray-600">
<span
aria-hidden="true"
className="inline-block h-2.5 w-2.5 rounded-full"
style={{ backgroundColor: item.color }}
/>
{item.label}
</li>
))}
</ul>
)
}
44 changes: 44 additions & 0 deletions apps/web/src/components/scientific/ChartTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client'

import type { PointTooltip } from '@/lib/scientific/tooltip'

/** Pixels reserved for the tooltip panel (`w-56` = 14rem = 224px). */
export const TOOLTIP_WIDTH = 224

export interface ChartTooltipProps {
tooltip: PointTooltip
/** Pixel position of the hovered point (SVG coordinates). */
x: number
y: number
/** Chart canvas width, used to keep the tooltip on-screen. */
width: number
}

/**
* Reusable hover tooltip for scientific charts. Rendered as an absolutely
* positioned HTML element over the SVG; the same `PointTooltip` rows feed the
* accessible detail panel, so both surfaces stay in sync.
*/
export function ChartTooltip({ tooltip, x, y, width }: ChartTooltipProps) {
const left = Math.min(Math.max(x + 16, 4), Math.max(width - TOOLTIP_WIDTH, 4))
const top = Math.max(y - 8, 4)
return (
<div
data-testid="chart-tooltip"
className="pointer-events-none absolute z-10 rounded-md border border-gray-200 bg-white p-2 shadow-md"
style={{ left, top, width: TOOLTIP_WIDTH }}
role="tooltip"
>
<p className="truncate text-xs font-semibold text-gray-900">{tooltip.title}</p>
<p className="truncate text-xs text-gray-500">{tooltip.subtitle}</p>
<dl className="mt-1 grid w-full grid-cols-[auto_1fr] gap-x-3 gap-y-0.5">
{tooltip.rows.map((row, index) => (
<div key={`${row.label}-${index}`} className="contents">
<dt className="text-xs text-gray-500">{row.label}</dt>
<dd className="truncate text-xs text-gray-900">{row.value}</dd>
</div>
))}
</dl>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
)
}
Loading
Loading