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
2 changes: 1 addition & 1 deletion observable/src/components/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const theme = {
};

export const plotStyle = {
background: theme.bg,
background: "transparent",
color: theme.text,
fontFamily: theme.fontFamily,
fontSize: "12px",
Expand Down
178 changes: 178 additions & 0 deletions observable/src/components/wda-chart-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
const ICON_DOWNLOAD = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
const ICON_SHARE = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>`;
const ICON_FULLSCREEN = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`;

const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 20"><circle cx="10" cy="10" r="8.5" fill="none" stroke="#aaa" stroke-width="1.5"/><ellipse cx="10" cy="10" rx="4" ry="8.5" fill="none" stroke="#aaa" stroke-width="1"/><line x1="1.5" y1="10" x2="18.5" y2="10" stroke="#aaa" stroke-width="1"/><text x="23" y="14" font-family="sans-serif" font-size="11" font-weight="700" fill="#aaa" letter-spacing="1">WDA</text></svg>`;

function loadImage(url) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.src = url;
});
}

async function downloadChart(block) {
const svg = [...block.querySelectorAll("svg")].find(
(s) => parseInt(s.getAttribute("width") || "0") > 100,
);
if (!svg) return;

const svgW = svg.viewBox?.baseVal?.width || svg.clientWidth || 640;
const svgH = svg.viewBox?.baseVal?.height || svg.clientHeight || 400;

const titleText =
block.querySelector(".wda-chart-title")?.textContent?.trim() || "";
const subtitleText =
block.querySelector(".wda-chart-subtitle")?.textContent?.trim() || "";
const sourceEl = block.querySelector(".wda-chart-source");
const sourceText = sourceEl?.textContent?.trim() || "";

// Use actual rendered width of source element for the separator line
const screenSvgW = svg.getBoundingClientRect().width || svgW;
const domToSvg = svgW / screenSvgW;
const sourceLineW = sourceEl
? sourceEl.getBoundingClientRect().width * domToSvg
: 0;

const scale = 2;
const pad = 20;
const titleH = titleText ? 26 : 0;
const subtitleH = subtitleText ? 20 : 0;
const gapH = titleH || subtitleH ? 10 : 0;
const sourceH = sourceText ? 28 : 0;
const totalW = svgW + pad * 2;
const totalH = pad + titleH + subtitleH + gapH + svgH + sourceH + pad;

const serializer = new XMLSerializer();
const svgUrl = URL.createObjectURL(
new Blob([serializer.serializeToString(svg)], { type: "image/svg+xml" }),
);
const logoUrl = URL.createObjectURL(
new Blob([LOGO_SVG], { type: "image/svg+xml" }),
);

const [chartImg, logoImg] = await Promise.all([
loadImage(svgUrl),
loadImage(logoUrl),
]);
URL.revokeObjectURL(svgUrl);
URL.revokeObjectURL(logoUrl);

const canvas = document.createElement("canvas");
canvas.width = totalW * scale;
canvas.height = totalH * scale;
const ctx = canvas.getContext("2d");
ctx.scale(scale, scale);
ctx.fillStyle = "#1a1a2e";
ctx.fillRect(0, 0, totalW, totalH);

let y = pad;
if (titleText) {
ctx.font = "bold 16px sans-serif";
ctx.fillStyle = "#e0e0e0";
ctx.fillText(titleText, pad, y + 16);
y += titleH;
}
if (subtitleText) {
ctx.font = "13px sans-serif";
ctx.fillStyle = "#aaaaaa";
ctx.fillText(subtitleText, pad, y + 14);
y += subtitleH;
}
y += gapH;

ctx.drawImage(chartImg, pad, y, svgW, svgH);

// WDA logo — top-right, aligned with title row
const logoW = 58,
logoH = 20;
ctx.globalAlpha = 0.5;
ctx.drawImage(logoImg, totalW - pad - logoW, pad - 4, logoW, logoH);
ctx.globalAlpha = 1;

y += svgH;
if (sourceText) {
ctx.strokeStyle = "#3d3d6b";
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(pad, y + 6);
ctx.lineTo(pad + sourceLineW, y + 6);
ctx.stroke();
ctx.font = "11px sans-serif";
ctx.fillStyle = "#3d3d6b";
ctx.fillText(sourceText, pad, y + 18);
}

const filename = titleText.replace(/\s+/g, "-").toLowerCase() || "chart";
canvas.toBlob((pngBlob) => {
const a = document.createElement("a");
a.href = URL.createObjectURL(pngBlob);
a.download = `wda-${filename}.png`;
a.click();
});
}

function shareChart(block) {
const title =
block.querySelector(".wda-chart-title")?.textContent?.trim() || "Chart";
const url =
window.location.origin + window.location.pathname + window.location.search;
if (navigator.share) {
navigator.share({ title, url }).catch(() => {});
} else {
navigator.clipboard.writeText(url).then(() => {
const btn = block.querySelector('[data-action="share"]');
if (btn) {
const prev = btn.innerHTML;
btn.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
setTimeout(() => (btn.innerHTML = prev), 1500);
}
});
}
}

function toggleFullscreen(block, btn) {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
block.requestFullscreen();
}
document.addEventListener(
"fullscreenchange",
() => {
btn.title = document.fullscreenElement
? "Quitter le plein écran"
: "Plein écran";
},
{ once: false },
);
}

export function initChartActions() {
document.querySelectorAll(".wda-chart-block").forEach((block) => {
if (block.querySelector(".wda-chart-actions")) return;

const bar = document.createElement("div");
bar.className = "wda-chart-actions";
bar.innerHTML = `
<button class="wda-action-btn" data-action="download" title="Télécharger l'image">${ICON_DOWNLOAD}</button>
<button class="wda-action-btn" data-action="share" title="Partager">${ICON_SHARE}</button>
<button class="wda-action-btn" data-action="fullscreen" title="Plein écran">${ICON_FULLSCREEN}</button>
`;

bar.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
if (btn.dataset.action === "download") downloadChart(block);
else if (btn.dataset.action === "share") shareChart(block);
else if (btn.dataset.action === "fullscreen")
toggleFullscreen(block, btn);
});

const source = block.querySelector(".wda-chart-source");
if (source) block.insertBefore(bar, source);
else block.appendChild(bar);
});
}
176 changes: 169 additions & 7 deletions observable/src/style.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,132 @@
@import url("observablehq:default.css");
@import url("observablehq:theme-wide.css");
@import url("https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap");

/* ── Page content width ───────────────────────────────────────────────── */
main.observablehq {
max-width: 50vw !important;
margin-left: auto !important;
margin-right: auto !important;
}

/* ── Figures transparentes — héritent du fond du conteneur parent ─────── */
figure {
background: transparent !important;
margin: 0;
padding: 0;
}

/* ── WDA chart header/footer pattern ─────────────────────────────────── */
.wda-chart-title {
font-size: 18px;
font-weight: 700;
color: var(--theme-foreground);
margin: 0 0 0.25rem 0;
}

.wda-chart-subtitle {
font-size: 13px;
color: var(--theme-foreground-muted);
margin: 0 0 1rem 0;
line-height: 1.5;
}

.wda-chart-source {
font-size: 11px;
color: var(--theme-foreground-faint);
margin-top: 0.75rem;
border-top: 1px solid var(--theme-foreground-faintest);
padding-top: 0.5rem;
}

/* ── WDA chart block container ────────────────────────────────────────── */
.wda-chart-block {
position: relative;
background: #1a1a2e;
border: 1px solid #555;
border-radius: 8px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
}

.wda-chart-block::after {
content: "";
position: absolute;
top: 0.85rem;
right: 1rem;
width: 58px;
height: 20px;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 58 20'><circle cx='10' cy='10' r='8.5' fill='none' stroke='%23aaa' stroke-width='1.5'/><ellipse cx='10' cy='10' rx='4' ry='8.5' fill='none' stroke='%23aaa' stroke-width='1'/><line x1='1.5' y1='10' x2='18.5' y2='10' stroke='%23aaa' stroke-width='1'/><text x='23' y='14' font-family='sans-serif' font-size='11' font-weight='700' fill='%23aaa' letter-spacing='1'>WDA</text></svg>");
background-repeat: no-repeat;
background-size: contain;
opacity: 0.5;
pointer-events: none;
}

.wda-chart-block .wda-chart-title {
color: #e0e0e0;
}

.wda-chart-block .wda-chart-subtitle {
color: #aaa;
}

.wda-chart-block .wda-chart-source {
color: #3d3d6b;
border-top-color: #3d3d6b;
}

.wda-chart-actions {
display: flex;
gap: 0.35rem;
justify-content: flex-end;
margin-top: 0.6rem;
margin-bottom: 0.4rem;
}

.wda-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 5px;
color: #666;
cursor: pointer;
transition:
background 0.15s,
color 0.15s;
padding: 0;
}

.wda-action-btn:hover {
background: rgba(255, 255, 255, 0.14);
color: #aaa;
}

.wda-chart-block:fullscreen {
display: flex;
flex-direction: column;
padding: 1.5rem;
overflow-y: auto;
border-radius: 0;
}

.wda-chart-block:fullscreen figure {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}

.wda-chart-block:fullscreen figure > svg {
flex: 1;
width: 100% !important;
height: 100% !important;
min-height: 0;
}

/* ── Map popup dark style ─────────────────────────────────────────────── */
.vigilo-popup .maplibregl-popup-content {
Expand Down Expand Up @@ -388,15 +515,25 @@ h1 {

/* ── Page header with breadcrumb ─────────────────────────────────────── */
.page-header {
padding-bottom: 1.25rem;
margin-bottom: 1.75rem;
padding-top: 1rem;
padding-bottom: 1.5rem;
margin-bottom: 2rem;
border-bottom: 1px solid var(--theme-foreground-faintest);
}

.page-header h1 {
font-family: Georgia, "Times New Roman", serif;
font-size: 2.75rem;
font-weight: 700;
line-height: 1.15;
letter-spacing: -0.01em;
margin: 0.5rem 0 0;
}

.breadcrumb {
font-size: 0.78rem;
color: var(--theme-foreground-muted);
margin-bottom: 0.6rem;
margin-bottom: 0.5rem;
}

.breadcrumb a {
Expand All @@ -408,11 +545,36 @@ h1 {
}

.page-subtitle {
font-size: 1rem;
font-size: 1.15rem;
color: var(--theme-foreground-muted);
line-height: 1.6;
margin-top: 0.3rem;
max-width: 720px;
line-height: 1.55;
margin-top: 0.75rem;
max-width: 680px;
}

.page-meta {
font-size: 0.82rem;
color: var(--theme-foreground-faint);
margin-top: 0.75rem;
}

.page-meta a {
color: var(--theme-foreground-focus);
text-decoration: none;
}
.page-meta a:hover {
text-decoration: underline;
}

/* ── Inline note callout ─────────────────────────────────────────────── */
.wda-note {
font-size: 0.82rem;
color: var(--theme-foreground-faint);
border-left: 3px solid var(--theme-foreground-faintest);
padding: 0.35rem 0.75rem;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
line-height: 1.5;
}

/* ── Too-many warning ────────────────────────────────────────────────── */
Expand Down
Loading
Loading