Skip to content

Commit a0d07df

Browse files
committed
add axis border, fix export
1 parent 03bd7b3 commit a0d07df

File tree

10 files changed

+338
-59
lines changed

10 files changed

+338
-59
lines changed

packages/app/src/components/data/plot/definition/sections/general-section.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
SelectTrigger,
1616
SelectValue,
1717
} from "@/components/ui/select";
18+
import { Switch } from "@/components/ui/switch";
1819
import { toast } from "sonner";
1920
import { updatePlotDefinitionAction } from "@/actions/data/plots/updatePlotDefinition";
2021
import { LabelWithBadge } from "../../form/label-badge";
@@ -45,6 +46,31 @@ type DataSource =
4546
};
4647

4748
export function GeneralSection({ plot, queries, tables }: GeneralSectionProps) {
49+
const handleShowFrameChange = async (showFrame: boolean) => {
50+
try {
51+
const currentDefinition = plot.definition;
52+
if (!currentDefinition) return;
53+
54+
const updatedDefinition = {
55+
...currentDefinition,
56+
appearance: {
57+
...currentDefinition.appearance,
58+
showFrame,
59+
},
60+
} as PlotDefinition;
61+
62+
await updatePlotDefinitionAction(
63+
plot.id,
64+
updatedDefinition,
65+
plot.projectId,
66+
);
67+
toast.success("Frame setting updated");
68+
} catch (error) {
69+
console.error("Failed to update frame setting:", error);
70+
toast.error("Failed to update frame setting");
71+
}
72+
};
73+
4874
const handlePlotTypeChange = async (type: PlotDefinition["type"]) => {
4975
try {
5076
// Get the default definition for the new type
@@ -209,6 +235,23 @@ export function GeneralSection({ plot, queries, tables }: GeneralSectionProps) {
209235
/>
210236
</div>
211237
</div>
238+
239+
<div className="mt-6 space-y-4">
240+
<div className="flex items-center justify-between">
241+
<div className="space-y-0.5">
242+
<LabelWithBadge
243+
isValid={true}
244+
description="Show or hide the plot frame border"
245+
>
246+
Show frame
247+
</LabelWithBadge>
248+
</div>
249+
<Switch
250+
checked={plot.definition?.appearance?.showFrame ?? true}
251+
onCheckedChange={handleShowFrameChange}
252+
/>
253+
</div>
254+
</div>
212255
</SectionWrapper>
213256
);
214257
}

packages/app/src/components/data/plot/form/text-labels-form.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Plus, Trash2 } from "lucide-react";
88
import { type PlotDefinition } from "@common/db/schema/plot";
99
import { isDefined } from "@/utils/helpers";
1010
import { ColorPicker } from "@/components/ui/color-picker";
11+
import { usePlotTheme } from "@/hooks/use-plot-theme";
1112

1213
interface TextLabelsFormProps {
1314
onSubmit: (_data: PlotDefinition) => void;
@@ -16,6 +17,7 @@ interface TextLabelsFormProps {
1617
export function TextLabelsForm({ onSubmit }: TextLabelsFormProps) {
1718
const { watch, setValue, handleSubmit } = useFormContext<PlotDefinition>();
1819
const textLabels = watch("textLabels") ?? [];
20+
const theme = usePlotTheme();
1921

2022
const handleAddLabel = () => {
2123
const newLabels = [
@@ -25,7 +27,7 @@ export function TextLabelsForm({ onSubmit }: TextLabelsFormProps) {
2527
y: 0,
2628
text: "new label",
2729
color: "#000000",
28-
fontSize: 12,
30+
fontSize: theme.fonts.annotation,
2931
rotation: 0,
3032
},
3133
];
@@ -163,7 +165,7 @@ export function TextLabelsForm({ onSubmit }: TextLabelsFormProps) {
163165
type="number"
164166
min="8"
165167
max="72"
166-
value={textLabels[index]?.fontSize ?? 12}
168+
value={textLabels[index]?.fontSize ?? theme.fonts.annotation}
167169
onChange={(e) =>
168170
handleLabelChange(index, "fontSize", e.target.value)
169171
}

packages/app/src/components/data/plot/plot-export/utils.ts

Lines changed: 185 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { PLOT_FONT_CONFIG } from "@/hooks/use-plot-theme";
2+
13
export function getPlotSVG(container: HTMLElement): SVGElement {
24
// Get the main plot SVG that contains everything
35
const svg = container.querySelector(
@@ -8,21 +10,152 @@ export function getPlotSVG(container: HTMLElement): SVGElement {
810
throw new Error("Plot SVG not found");
911
}
1012

11-
// Clone the SVG to avoid modifying the original
12-
const clonedSvg = svg.cloneNode(true) as SVGElement;
13-
1413
// Get the original dimensions
1514
const box = svg.getBoundingClientRect();
1615

16+
// Clone the SVG to avoid modifying the original
17+
const clonedSvg = svg.cloneNode(true) as SVGElement;
18+
1719
// Set the dimensions explicitly
1820
clonedSvg.setAttribute("width", String(box.width));
1921
clonedSvg.setAttribute("height", String(box.height));
2022
clonedSvg.setAttribute("viewBox", `0 0 ${box.width} ${box.height}`);
2123

24+
// Apply explicit font sizes to ensure they're preserved
25+
applyExplicitFontSizes(clonedSvg);
26+
27+
// Also ensure all text has proper font family and other properties
28+
ensureTextProperties(clonedSvg);
29+
2230
return clonedSvg;
2331
}
2432

33+
function applyExplicitFontSizes(svg: SVGElement): void {
34+
// Find all text elements and apply explicit font sizes using inline styles
35+
const textElements = svg.querySelectorAll("text, tspan");
36+
37+
textElements.forEach((element) => {
38+
const textElement = element as SVGTextElement;
39+
40+
// Get current style attribute or create empty one
41+
let currentStyle = textElement.getAttribute("style") || "";
42+
43+
// Use data attributes for robust identification
44+
const plotElement = textElement.getAttribute("data-plot-element");
45+
46+
let targetFontSize = `${PLOT_FONT_CONFIG.axisTick}px`; // Default
47+
48+
// Identify element type using data attributes and class names
49+
if (plotElement === "axis-label") {
50+
targetFontSize = `${PLOT_FONT_CONFIG.axisLabel}px`;
51+
} else if (plotElement === "axis-tick") {
52+
targetFontSize = `${PLOT_FONT_CONFIG.axisTick}px`;
53+
} else if (plotElement === "legend-title") {
54+
targetFontSize = `${PLOT_FONT_CONFIG.legendTitle}px`;
55+
} else if (plotElement === "legend-item") {
56+
targetFontSize = `${PLOT_FONT_CONFIG.legendItem}px`;
57+
} else if (plotElement === "text-annotation") {
58+
targetFontSize = `${PLOT_FONT_CONFIG.annotation}px`;
59+
} else {
60+
// Fallback to class name identification
61+
const hasAxisLabelClass =
62+
textElement.classList.contains("plot-axis-label");
63+
const hasAxisTickClass = textElement.classList.contains("plot-axis-tick");
64+
65+
if (hasAxisLabelClass) {
66+
targetFontSize = `${PLOT_FONT_CONFIG.axisLabel}px`;
67+
} else if (hasAxisTickClass) {
68+
targetFontSize = `${PLOT_FONT_CONFIG.axisTick}px`;
69+
} else {
70+
// Default fallback - ensure minimum font size
71+
const hasFontSize = textElement.getAttribute("fontSize");
72+
if (hasFontSize) {
73+
const currentSize = parseInt(hasFontSize);
74+
const minSize = PLOT_FONT_CONFIG.axisTick;
75+
targetFontSize =
76+
currentSize < minSize ? `${minSize}px` : `${currentSize}px`;
77+
}
78+
}
79+
}
80+
81+
// Remove any existing font-size from style attribute
82+
currentStyle = currentStyle.replace(/font-size\s*:\s*[^;]+;?/g, "");
83+
84+
// Add the new font-size to the style attribute
85+
if (currentStyle && !currentStyle.endsWith(";")) {
86+
currentStyle += ";";
87+
}
88+
currentStyle += `font-size: ${targetFontSize};`;
89+
90+
// Set the updated style attribute
91+
textElement.setAttribute("style", currentStyle);
92+
93+
// Also set fontSize attribute as backup
94+
textElement.setAttribute("fontSize", targetFontSize.replace("px", ""));
95+
});
96+
97+
// Find axis labels using data attributes and class names
98+
const axisLabels = svg.querySelectorAll(
99+
// eslint-disable-next-line quotes
100+
'[data-plot-element="axis-label"], .plot-axis-label',
101+
);
102+
axisLabels.forEach((element) => {
103+
const textElement = element as SVGTextElement;
104+
if (textElement.tagName === "text" || textElement.tagName === "tspan") {
105+
let currentStyle = textElement.getAttribute("style") || "";
106+
currentStyle = currentStyle.replace(/font-size\s*:\s*[^;]+;?/g, "");
107+
if (currentStyle && !currentStyle.endsWith(";")) {
108+
currentStyle += ";";
109+
}
110+
currentStyle += `font-size: ${PLOT_FONT_CONFIG.axisLabel}px;`;
111+
textElement.setAttribute("style", currentStyle);
112+
textElement.setAttribute("fontSize", String(PLOT_FONT_CONFIG.axisLabel));
113+
}
114+
});
115+
}
116+
117+
function ensureTextProperties(svg: SVGElement): void {
118+
// Find all text elements and ensure they have proper font properties
119+
const textElements = svg.querySelectorAll("text, tspan");
120+
121+
textElements.forEach((element) => {
122+
const textElement = element as SVGTextElement;
123+
let currentStyle = textElement.getAttribute("style") || "";
124+
125+
// Ensure font-family is set (use system default if not specified)
126+
if (!currentStyle.includes("font-family")) {
127+
if (currentStyle && !currentStyle.endsWith(";")) {
128+
currentStyle += ";";
129+
}
130+
currentStyle +=
131+
"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;";
132+
}
133+
134+
// Ensure font-weight is set for better rendering
135+
if (!currentStyle.includes("font-weight")) {
136+
if (currentStyle && !currentStyle.endsWith(";")) {
137+
currentStyle += ";";
138+
}
139+
currentStyle += "font-weight: 400;";
140+
}
141+
142+
// Ensure text-rendering is optimized
143+
if (!currentStyle.includes("text-rendering")) {
144+
if (currentStyle && !currentStyle.endsWith(";")) {
145+
currentStyle += ";";
146+
}
147+
currentStyle += "text-rendering: optimizeLegibility;";
148+
}
149+
150+
// Set the updated style attribute
151+
textElement.setAttribute("style", currentStyle);
152+
});
153+
}
154+
25155
export function svgToDataURL(svg: SVGElement): string {
156+
// Add font definitions to ensure fonts are preserved
157+
addFontDefinitions(svg);
158+
26159
const serializer = new XMLSerializer();
27160
const svgString = serializer.serializeToString(svg);
28161
const svgBlob = new Blob([svgString], {
@@ -31,12 +164,56 @@ export function svgToDataURL(svg: SVGElement): string {
31164
return URL.createObjectURL(svgBlob);
32165
}
33166

167+
function addFontDefinitions(svg: SVGElement): void {
168+
// Get the document's font information
169+
const fontFaces = Array.from(document.fonts)
170+
.map((font) => {
171+
const family = font.family;
172+
const style = font.style;
173+
const weight = font.weight;
174+
const stretch = font.stretch;
175+
const unicodeRange = font.unicodeRange;
176+
177+
return `@font-face {
178+
font-family: "${family}";
179+
font-style: ${style};
180+
font-weight: ${weight};
181+
font-stretch: ${stretch};
182+
unicode-range: ${unicodeRange};
183+
src: local("${family}");
184+
}`;
185+
})
186+
.join("\n");
187+
188+
// Add a style element with font definitions
189+
if (fontFaces) {
190+
const styleElement = document.createElementNS(
191+
"http://www.w3.org/2000/svg",
192+
"style",
193+
);
194+
styleElement.textContent = fontFaces;
195+
196+
// Insert at the beginning of the SVG
197+
const defs =
198+
svg.querySelector("defs") ||
199+
svg.insertBefore(
200+
document.createElementNS("http://www.w3.org/2000/svg", "defs"),
201+
svg.firstChild,
202+
);
203+
defs.appendChild(styleElement);
204+
}
205+
}
206+
34207
export async function svgToPngDataURL(
35208
svg: SVGElement,
36209
scale = 2,
37210
backgroundColor = "white",
38211
): Promise<string> {
39212
return new Promise((resolve, reject) => {
213+
// Ensure font sizes are explicitly set before converting to PNG
214+
applyExplicitFontSizes(svg);
215+
ensureTextProperties(svg);
216+
addFontDefinitions(svg);
40217
const svgURL = svgToDataURL(svg);
41218
const img = new Image();
42219
const width = parseFloat(svg.getAttribute("width") || "0");
@@ -53,6 +230,10 @@ export async function svgToPngDataURL(
53230
throw new Error("Failed to get canvas context");
54231
}
55232

233+
// Set high quality rendering
234+
ctx.imageSmoothingEnabled = true;
235+
ctx.imageSmoothingQuality = "high";
236+
56237
// Fill background if specified
57238
if (backgroundColor) {
58239
ctx.fillStyle = backgroundColor;
@@ -64,7 +245,7 @@ export async function svgToPngDataURL(
64245
ctx.drawImage(img, 0, 0);
65246

66247
// Convert to data URL
67-
const dataURL = canvas.toDataURL("image/png");
248+
const dataURL = canvas.toDataURL("image/png", 1.0); // Maximum quality
68249
URL.revokeObjectURL(svgURL);
69250
resolve(dataURL);
70251
} catch (error) {

0 commit comments

Comments
 (0)