Skip to content
Open
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
1 change: 1 addition & 0 deletions workbench/_web/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions workbench/_web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,19 @@
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}

.print-hide {
@media print {
display: none;
}
}
.printable {
background-color: red;
@media print {
/* 1. Force the canvas to be no wider than the printable area */
max-width: 100% !important;

/* 2. Maintain the aspect ratio by letting the height auto-adjust */
height: auto !important;
}
}
2 changes: 1 addition & 1 deletion workbench/_web/src/app/workbench/[workspaceId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function WorkbenchLayout({
}>) {
return (
<div className="flex flex-col h-screen bg-gradient-to-tr from-background dark:to-primary/15 to-primary/30">
<header className="p-3 pl-5 flex items-center justify-between">
<header className="p-3 pl-5 flex items-center justify-between print-hide">
<Link href="/workbench">
<Button variant="ghost" className="bg-transparent hover:!white/10 border-1">
<ArrowLeft className="w-4 h-4" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,14 @@ export function Editor() {

return (
<div className="h-full w-full flex flex-col">
<div className="flex items-center justify-end border-b h-14 px-3 py-3">
<div className="flex items-center justify-between border-b h-14 px-3 py-3 print-hide">
<button
onClick={window.print}
className="rounded-md text-sm font-medium border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground px-4 py-2"
title="System print dialog/save to PDF"
>
Print/Save PDF
</button>
<div className="text-xs text-muted-foreground inline-flex items-center gap-2 px-3">
{isQueuedToSave || mutation.isPending ? (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ function ChartEmbedComponent({

return (
<Card
className={`p-3 rounded cursor-pointer ${isSelected ? "border-primary ring-1 ring-primary" : ""}`}
className={"p-3 rounded cursor-pointer"}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
Expand All @@ -219,7 +219,7 @@ function ChartEmbedComponent({
>
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-muted-foreground">{name}</span>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 print-hide">
<Select
value={size}
onValueChange={(value) => setSize(value as "small" | "medium" | "large")}
Expand Down Expand Up @@ -271,7 +271,7 @@ function ChartEmbedComponent({
</div>
) : type === "heatmap" && chart.data ? (
<div
className={cn("w-full", {
className={cn("w-full print:m-auto", {
"h-[40vh]": size === "small",
"h-[60vh]": size === "medium",
"h-[80vh]": size === "large",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { Editor } from "./components/Editor";
export default function OverviewPage() {
return (
<div className="flex size-full min-h-0">
<div className="w-[20vw]">
<div className="w-[20vw] print-hide">
<ChartCardsSidebar />
</div>
<div className="pb-3 pr-3 w-[80vw] min-h-0">
<div className="pb-3 pr-3 w-[80vw] min-h-0 print:w-full">
<div className="size-full border rounded dark:bg-secondary/60 bg-secondary/80">
<Editor />
</div>
Expand Down
42 changes: 42 additions & 0 deletions workbench/_web/src/components/charts/PrintableCanvasImg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useRef, useEffect } from "react";
import React from "react";

interface PrintableCanvasImgProps {
canvasRef: React.RefObject<HTMLCanvasElement>;
}

export const PrintableCanvasImg = ({ canvasRef: heatmapCanvasRef }: PrintableCanvasImgProps) => {
const printImgRef = useRef<HTMLImageElement>(null);

useEffect(() => {
const handleBeforePrint = () => {
// We need refs because the src has to be updated synchronously
const canvas = heatmapCanvasRef?.current;
const printImg = printImgRef?.current;

if (canvas && printImg) {
const url = canvas.toDataURL("image/png", 1.0);
console.log("Canvas img url: " + url);
printImg.src = url;
}
};

window.addEventListener("beforeprint", handleBeforePrint);

return () => {
window.removeEventListener("beforeprint", handleBeforePrint);
};
// We ONLY want this to run once, on mount: empty array
// https://reactjs.org/docs/hooks-effect.html
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<img
src={undefined}
alt="Printable chart"
className="hidden w-full h-auto mx-auto print:block"
ref={printImgRef}
/>
);
};
168 changes: 85 additions & 83 deletions workbench/_web/src/components/charts/heatmap/Heatmap.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useMemo } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { ResponsiveHeatMapCanvas } from "@nivo/heatmap";
import { heatmapMargin, heatmapTheme } from "../theming";
import { resolveThemeCssVars } from "@/lib/utils";
Expand All @@ -9,6 +9,7 @@ import { HeatmapRow } from "@/types/charts";
import { Tooltip } from "./Tooltip";
import { Metrics } from "@/types/lens";
import React from "react";
import { PrintableCanvasImg } from "../PrintableCanvasImg";

interface HeatmapProps {
rows: HeatmapRow[];
Expand Down Expand Up @@ -75,88 +76,89 @@ export function Heatmap({
}, [rows, statisticType]);

return (
<div
className="size-full relative cursor-crosshair"
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
>
<canvas
ref={heatmapCanvasRef}
className="absolute inset-0 size-full pointer-events-auto z-20"
/>
{useTooltip && <Tooltip />}
<ResponsiveHeatMapCanvas
data={rows}
margin={margin}
valueFormat=">-.2f"
axisTop={null}
axisBottom={{
legend: "Layer",
legendOffset: 40,
tickSize: 0,
tickPadding: 10,
format: (value) => String(value),
}}
axisLeft={{
tickSize: 0,
tickPadding: 10,
format: (value) => String(value).replace(/-\d+$/, ""),
}}
axisRight={
statisticType !== Metrics.PROBABILITY
? {
tickSize: 0,
tickPadding: 10,
format: (value) => {
// Access rightAxisLabel using the lookup map
return (
rightAxisLabelMap[value] || String(value).replace(/-\d+$/, "")
);
},
}
: null
}
label={(cell) => {
if (cell.data.label) {
return cell.data.label;
<>
{heatmapCanvasRef && <PrintableCanvasImg canvasRef={heatmapCanvasRef} />}
<div
className="size-full relative cursor-crosshair print-hide"
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseLeave={onMouseLeave}
>
{useTooltip && <Tooltip />}
<ResponsiveHeatMapCanvas
ref={heatmapCanvasRef}
data={rows}
margin={margin}
valueFormat=">-.2f"
axisTop={null}
axisBottom={{
legend: "Layer",
legendOffset: 40,
tickSize: 0,
tickPadding: 10,
format: (value) => String(value),
}}
axisLeft={{
tickSize: 0,
tickPadding: 10,
format: (value) => String(value).replace(/-\d+$/, ""),
}}
axisRight={
statisticType !== Metrics.PROBABILITY
? {
tickSize: 0,
tickPadding: 10,
format: (value) => {
// Access rightAxisLabel using the lookup map
return (
rightAxisLabelMap[value] ||
String(value).replace(/-\d+$/, "")
);
},
}
: null
}
return "";
}}
labelTextColor={(cell) => {
// Use white text for dark cells, black for light cells
const value = cell.data.y;
return value !== null && value > 0.5 ? "#ffffff" : "#000000";
}}
colors={{
type: "sequential",
scheme: "blues",
minValue: minValue,
maxValue: maxValue,
}}
hoverTarget="cell"
inactiveOpacity={1}
theme={resolvedTheme}
animate={false}
isInteractive={false}
legends={[
{
title: statisticType === Metrics.RANK ? "Rank (log)" : statisticType,
anchor: "right",
translateX: statisticType !== Metrics.PROBABILITY ? 60 : 30,
translateY: 0,
length: 400,
thickness: 8,
direction: "column",
tickPosition: "after",
tickSize: 3,
tickSpacing: 4,
tickOverlap: false,
tickFormat: ">-.2f",
titleAlign: "start",
},
]}
/>
</div>
label={(cell) => {
if (cell.data.label) {
return cell.data.label;
}
return "";
}}
labelTextColor={(cell) => {
// Use white text for dark cells, black for light cells
const value = cell.data.y;
return value !== null && value > 0.5 ? "#ffffff" : "#000000";
}}
colors={{
type: "sequential",
scheme: "blues",
minValue: minValue,
maxValue: maxValue,
}}
hoverTarget="cell"
inactiveOpacity={1}
theme={resolvedTheme}
animate={false}
isInteractive={false}
legends={[
{
title: statisticType === Metrics.RANK ? "Rank (log)" : statisticType,
anchor: "right",
translateX: statisticType !== Metrics.PROBABILITY ? 60 : 30,
translateY: 0,
length: 400,
thickness: 8,
direction: "column",
tickPosition: "after",
tickSize: 3,
tickSpacing: 4,
tickOverlap: false,
tickFormat: ">-.2f",
titleAlign: "start",
},
]}
/>
</div>
</>
);
}
3 changes: 2 additions & 1 deletion workbench/_web/src/components/charts/heatmap/HeatmapCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Separator } from "@/components/ui/separator";
import CodeExport from "@/app/workbench/[workspaceId]/components/CodeExport";
import { CopyImage } from "../CopyImage";
import { Metrics } from "@/types/lens";
import { PrintableCanvasImg } from "../PrintableCanvasImg";

interface HeatmapCardProps {
chart: HeatmapChart;
Expand Down Expand Up @@ -179,7 +180,7 @@ const HeatmapCardContent = ({ captureRef, chart, statisticType }: HeatmapCardCon
</Button>
</div>
</div>
<div className="flex size-full" ref={captureRef}>
<div className="flex size-full relative" ref={captureRef}>
<Heatmap
rows={data}
heatmapCanvasRef={heatmapCanvasRef}
Expand Down
5 changes: 4 additions & 1 deletion workbench/_web/src/components/ui/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow print:shadow-none",
className,
)}
{...props}
/>
),
Expand Down
Loading