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
20 changes: 15 additions & 5 deletions observable/src/components/vigilo-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,25 @@ const MAP_STYLE = "https://tiles.openfreemap.org/styles/positron";
export function createVigiloMap(
container,
observations,
{ center = [2.35, 46.5], zoom = 5 } = {},
{ center = [2.35, 46.5], zoom = 5, bounds = null, padding = 40 } = {},
) {
const map = new maplibregl.Map({
const mapOptions = {
container,
style: MAP_STYLE,
center,
zoom,
attributionControl: { compact: true },
});
preserveDrawingBuffer: true,
};
if (bounds) {
mapOptions.bounds = bounds;
mapOptions.fitBoundsOptions = { padding };
} else {
mapOptions.center = center;
mapOptions.zoom = zoom;
}
const map = new maplibregl.Map(mapOptions);

// Expose map instance so wda-chart-actions can trigger repaint before PNG export
container._wdaMap = map;

map.on("load", () => {
const geojson = {
Expand Down
130 changes: 81 additions & 49 deletions observable/src/components/wda-chart-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,51 +12,28 @@ function loadImage(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;

async function composeAndDownload(contentW, contentH, drawContent, block) {
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 sourceLineW = sourceEl ? sourceEl.getBoundingClientRect().width : 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 totalW = contentW + pad * 2;
const totalH = pad + titleH + subtitleH + gapH + contentH + 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);
const logoImg = await loadImage(logoUrl);
URL.revokeObjectURL(logoUrl);

const canvas = document.createElement("canvas");
Expand All @@ -82,16 +59,15 @@ async function downloadChart(block) {
}
y += gapH;

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

// 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;
y += contentH;
if (sourceText) {
ctx.strokeStyle = "#3d3d6b";
ctx.lineWidth = 0.5;
Expand All @@ -113,6 +89,54 @@ async function downloadChart(block) {
});
}

async function downloadChart(block) {
// SVG chart
const svg = [...block.querySelectorAll("svg")].find(
(s) => parseInt(s.getAttribute("width") || "0") > 100,
);
if (svg) {
const svgW = svg.viewBox?.baseVal?.width || svg.clientWidth || 640;
const svgH = svg.viewBox?.baseVal?.height || svg.clientHeight || 400;
const serializer = new XMLSerializer();
const svgUrl = URL.createObjectURL(
new Blob([serializer.serializeToString(svg)], { type: "image/svg+xml" }),
);
const chartImg = await loadImage(svgUrl);
URL.revokeObjectURL(svgUrl);
await composeAndDownload(
svgW,
svgH,
(ctx, x, y, w, h) => ctx.drawImage(chartImg, x, y, w, h),
block,
);
return;
}

// MapLibre GL canvas — trigger repaint, wait for frame, then read via toDataURL
const mapEl = block.querySelector(".vigilo-map");
const mapCanvas = block.querySelector(".maplibregl-canvas");
if (mapCanvas) {
const mapInstance = mapEl?._wdaMap;
if (mapInstance) {
await new Promise((resolve) => {
mapInstance.once("render", resolve);
mapInstance.triggerRepaint();
// fallback if render event never fires (e.g. headless)
setTimeout(resolve, 300);
});
}
const rect = mapCanvas.getBoundingClientRect();
const dataUrl = mapCanvas.toDataURL();
const mapImg = await loadImage(dataUrl);
await composeAndDownload(
rect.width,
rect.height,
(ctx, x, y, cw, ch) => ctx.drawImage(mapImg, x, y, cw, ch),
block,
);
}
}

function shareChart(block) {
const title =
block.querySelector(".wda-chart-title")?.textContent?.trim() || "Chart";
Expand Down Expand Up @@ -145,34 +169,42 @@ function toggleFullscreen(block, btn) {
btn.title = document.fullscreenElement
? "Quitter le plein écran"
: "Plein écran";
// MapLibre uses ResizeObserver but needs a nudge after fullscreen transition
const mapInstance = block.querySelector(".vigilo-map")?._wdaMap;
if (mapInstance) setTimeout(() => mapInstance.resize(), 50);
},
{ once: false },
);
}

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

const bar = document.createElement("div");
bar.className = "wda-chart-actions";
bar.innerHTML = `
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);
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);
}

export function initBlockActions(block) {
processBlock(block);
}

export function initChartActions() {
document.querySelectorAll(".wda-chart-block").forEach(processBlock);
}
57 changes: 21 additions & 36 deletions observable/src/dataset/nafrev2.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,32 @@ title: Dataset — NAF Rév. 2
---

<div class="page-header">
<div class="breadcrumb"><a href="./">Accueil</a> › Datasets › NAF Rév. 2</div>
<div class="breadcrumb"><a href="../">Accueil</a> › Datasets › NAF Rév. 2</div>
<h1>Dataset NAF Rév. 2</h1>
<p class="page-subtitle">Nomenclature française des activités économiques, référentiel hiérarchique publié par l'INSEE.</p>
</div>

```js
const datasets = await FileAttachment("../data/wda-datasets.json").json();
const meta = datasets.find((d) => d.provider === "nafrev2");

display(
html`<div class="card">
<h3>Informations</h3>
<div class="meta-row">
<div class="meta-item">
<div class="meta-label">Description</div>
<div class="meta-value">${meta.description}</div>
</div>
<div class="meta-item">
<div class="meta-label">Source officielle</div>
<div class="meta-value">
<a href="${meta.source}" target="_blank">${meta.website}</a>
</div>
</div>
<div class="meta-item">
<div class="meta-label">Observations</div>
<div class="meta-value">
${meta.nb_observations.toLocaleString("fr-FR")}
</div>
</div>
<div class="meta-item">
<div class="meta-label">Variables</div>
<div class="meta-value">${meta.nb_variables}</div>
</div>
<div class="meta-item">
<div class="meta-label">Périmètres (${meta.wda_scope})</div>
<div class="meta-value">${meta.nb_scopes.toLocaleString("fr-FR")}</div>
</div>
<div class="card">
<h3>Informations</h3>
<div class="meta-row">
<div class="meta-item">
<div class="meta-label">Description</div>
<div class="meta-value">Nomenclature française des activités économiques, référentiel hiérarchique à 5 niveaux publié par l'INSEE.</div>
</div>
<div class="meta-item">
<div class="meta-label">Source officielle</div>
<div class="meta-value"><a href="https://www.insee.fr/fr/information/2406147" target="_blank">insee.fr — NAF Rév. 2</a></div>
</div>
<div class="meta-item">
<div class="meta-label">Licence</div>
<div class="meta-value">Open Data — Licence Ouverte / Open Licence</div>
</div>
</div>`,
);
```
<div class="meta-item">
<div class="meta-label">Couverture</div>
<div class="meta-value">France — 732 sous-classes (codes APE)</div>
</div>
</div>
</div>

<div class="see-also">
<h3>Explorer les données</h3>
Expand Down
57 changes: 21 additions & 36 deletions observable/src/dataset/sirene.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,32 @@ title: Dataset — SIRENE
---

<div class="page-header">
<div class="breadcrumb"><a href="./">Accueil</a> › Datasets › SIRENE</div>
<div class="breadcrumb"><a href="../">Accueil</a> › Datasets › SIRENE</div>
<h1>Dataset SIRENE</h1>
<p class="page-subtitle">Registre national des entreprises et établissements français, géré par l'INSEE.</p>
</div>

```js
const datasets = await FileAttachment("../data/wda-datasets.json").json();
const meta = datasets.find((d) => d.provider === "sirene");

display(
html`<div class="card">
<h3>Informations</h3>
<div class="meta-row">
<div class="meta-item">
<div class="meta-label">Description</div>
<div class="meta-value">${meta.description}</div>
</div>
<div class="meta-item">
<div class="meta-label">Source officielle</div>
<div class="meta-value">
<a href="${meta.source}" target="_blank">${meta.website}</a>
</div>
</div>
<div class="meta-item">
<div class="meta-label">Observations</div>
<div class="meta-value">
${meta.nb_observations.toLocaleString("fr-FR")}
</div>
</div>
<div class="meta-item">
<div class="meta-label">Variables</div>
<div class="meta-value">${meta.nb_variables}</div>
</div>
<div class="meta-item">
<div class="meta-label">Périmètres (${meta.wda_scope})</div>
<div class="meta-value">${meta.nb_scopes.toLocaleString("fr-FR")}</div>
</div>
<div class="card">
<h3>Informations</h3>
<div class="meta-row">
<div class="meta-item">
<div class="meta-label">Description</div>
<div class="meta-value">Registre national des entreprises et établissements français, géré par l'INSEE.</div>
</div>
<div class="meta-item">
<div class="meta-label">Source officielle</div>
<div class="meta-value"><a href="https://www.insee.fr/fr/information/3591226" target="_blank">insee.fr — SIRENE</a></div>
</div>
<div class="meta-item">
<div class="meta-label">Licence</div>
<div class="meta-value">Open Data — Licence Ouverte / Open Licence</div>
</div>
</div>`,
);
```
<div class="meta-item">
<div class="meta-label">Couverture</div>
<div class="meta-value">France entière (~12 millions d'établissements)</div>
</div>
</div>
</div>

<div class="see-also">
<h3>Explorer les données</h3>
Expand Down
Loading
Loading