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
Binary file added assets/mira-at-a-glance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 29 additions & 9 deletions compare.html
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,7 @@
/* Product screenshot under the hero */
.shot-wrap {
max-width: 1000px;
margin: -40px auto 0;
padding: 0 20px;
position: relative;
z-index: 2;
margin: 30px auto 0;
}

.shot {
Expand All @@ -155,6 +152,16 @@
box-shadow: 0 24px 60px rgba(17, 12, 46, .22);
}

/* "At a glance" conceptual figure */
.glance-fig {
width: 100%;
display: block;
border-radius: 12px;
border: 1px solid #e9e6f2;
background: #fff;
padding: 10px;
}

/* Section scaffolding */
section.band {
padding: 52px 0;
Expand Down Expand Up @@ -606,11 +613,20 @@ <h1>Multilayer network visualization — every view, in one browser tab</h1>
</div>
</section>

<!-- ── PRODUCT SHOT ─────────────────────────────────────────────── -->
<div class="shot-wrap">
<img class="shot" src="assets/mira-multilayer-network.png"
alt="MiRA showing a five-layer bipartite multilayer network in Network Mode, with interlayer links connecting shared species across layers">
</div>

<!-- ── AT A GLANCE ──────────────────────────────────────────────── -->
<section class="band tint">
<div class="band-inner">
<div class="kicker">At a glance</div>
<h2>Interact with your multilayer network and format it for publications</h2>
<p class="intro">
Load a multilayer network, explore it through seven linked modes, and save the result —
all in the browser, without installing anything or writing code.
</p>
<img class="glance-fig" src="assets/mira-at-a-glance.png"
alt="MiRA at a glance: (1) load multilayer data from the example datasets, the emln R package, a JSON file, or CSV files; (2) explore it in seven interactive modes — Network, Map, Layer, Grid, Meta-Network, Dashboard, and Data; (3) save the result as a PNG, JPG, or PDF image, or as a session file that reloads the full state including the data. MiRA is browser-based with no install, free and open source, requires no coding, offers coordinated mode transitions, and supports bipartite networks.">
</div>
</section>

<!-- ── SEVEN MODES ──────────────────────────────────────────────── -->
<section class="band">
Expand Down Expand Up @@ -832,6 +848,10 @@ <h3>Brain network →</h3>
<p>Explore a multilayer human brain connectome — beyond ecology.</p>
</a>
</div>
<div class="shot-wrap">
<img class="shot" src="assets/mira-multilayer-network.png"
alt="MiRA showing a five-layer bipartite multilayer network in Network Mode, with interlayer links connecting shared species across layers">
</div>
</div>
</section>

Expand Down
19 changes: 18 additions & 1 deletion css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2035,4 +2035,21 @@ body {
.tour-nav {
display: flex;
gap: 6px;
}
}
/* ============================================
"MiRA at a glance" figure modal (splash screen)
============================================ */
.glance-dialog-box {
position: relative;
width: min(1150px, 94vw); /* overrides the fixed 360px on .dialog-box */
max-width: 94vw;
padding: 34px 18px 18px;
}
.glance-modal-img {
display: block;
width: 100%;
height: auto;
max-height: 78vh;
object-fit: contain;
border-radius: 8px;
}
8 changes: 8 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,14 @@ <h3 id="layerDrillTitle" style="margin:0; font-size:14px; font-weight:600;">Laye
</div>

<!-- Citation dialog -->
<div id="glanceDialog" class="dialog-overlay" style="display:none;">
<div class="dialog-box glance-dialog-box">
<button id="glanceDialogClose" class="cite-close-btn" aria-label="Close" title="Close">&#10005;</button>
<img class="glance-modal-img" src="assets/mira-at-a-glance.png"
alt="MiRA at a glance: load multilayer data from example datasets, the emln R package, JSON or CSV; explore it in seven interactive modes (Network, Map, Layer, Grid, Meta-Network, Dashboard, Data); then save an image or a session file that reloads the full state including the data.">
</div>
</div>

<div id="citeDialog" class="dialog-overlay" style="display:none;">
<div class="dialog-box cite-dialog-box">
<button id="citeDialogClose" class="cite-close-btn" aria-label="Close" title="Close">✕</button>
Expand Down
35 changes: 24 additions & 11 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,25 +507,38 @@ bgMap.on('move', () => {
}
});

// Splash screen links — "Ecological Complexity Lab" and "visualization guidelines"
canvas.addEventListener('click', (e) => {
if (model) return;
// Splash screen actions — "MiRA at a glance", "See the manual", "Load data"
const glanceDialog = document.getElementById('glanceDialog');
const glanceDialogClose = document.getElementById('glanceDialogClose');

const openGlanceDialog = () => { glanceDialog.style.display = 'flex'; };
const closeGlanceDialog = () => { glanceDialog.style.display = 'none'; };

glanceDialogClose.addEventListener('click', closeGlanceDialog);
glanceDialog.addEventListener('click', e => { if (e.target === glanceDialog) closeGlanceDialog(); });
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && glanceDialog.style.display === 'flex') closeGlanceDialog();
});

const splashActionAt = (e) => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const inBounds = b => b && mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h;
if (inBounds(renderer._ecoLabBounds)) window.open('https://ecomplab.com/', '_blank', 'noopener');
if (inBounds(renderer._guidelinesBounds)) window.open('docs/manual.html#guidelines', '_blank', 'noopener');
if (inBounds(renderer._glanceBounds)) return openGlanceDialog;
if (inBounds(renderer._manualBounds)) return () => window.open('docs/manual.html', '_blank', 'noopener');
if (inBounds(renderer._loadDataBounds)) return () => document.getElementById('openDemoDialogBtn')?.click();
return null;
};

canvas.addEventListener('click', (e) => {
if (model) return;
splashActionAt(e)?.();
});

canvas.addEventListener('mousemove', (e) => {
if (model) return;
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const inBounds = b => b && mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h;
canvas.style.cursor = (inBounds(renderer._ecoLabBounds) || inBounds(renderer._guidelinesBounds))
? 'pointer' : 'default';
canvas.style.cursor = splashActionAt(e) ? 'pointer' : 'default';
});

// ---- Interaction ----
Expand Down
3 changes: 3 additions & 0 deletions js/interaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ export class InteractionHandler {
return;
}

// Splash screen: nothing to hover, and app.js owns the cursor for its action links.
if (!this.renderer.model) return;

// Hover detection
const prevHoveredNode = this.renderer.hoveredNode;
const prevHoveredLink = this.renderer.hoveredLink;
Expand Down
94 changes: 54 additions & 40 deletions js/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,71 +495,85 @@ export class Renderer {
}

_drawPlaceholder(ctx, w, h) {
const logoGap = 16;
const line1 = 'Welcome to MiRA (v1.1), the Multilayer Interactive Rendering App';
const devBy = 'Developed by the ';
const labName = 'Ecological Complexity Lab';
const line3 = 'Load a multilayer network via the Data panel to visualize';

// Line y-offsets relative to cy (textBaseline = 'middle')
const L1_OFF = -24, L4_OFF = 70;
const FONT1 = 18, FONT4 = 14;
const textBlockTop = L1_OFF - FONT1 / 2; // top of first line
const textBlockBottom = L4_OFF + FONT4 / 2; // bottom of last line
const logoH = textBlockBottom - textBlockTop; // matches text block height exactly
const LOGO_GAP = 16;
const TITLE_OFF = -24; // y-offsets relative to canvas centre
const HINT_OFF = 8;
const ACTION_OFF = 44;
const FONT_TITLE = 18, FONT_HINT = 15, FONT_ACTION = 14;
const HIT_PAD = 11; // half-height of a clickable text row
const SEPARATOR = ' · ';

const title = 'Welcome to MiRA (v1.1), the Multilayer Interactive Rendering App';
const hint = 'Load a multilayer network via the Data panel to visualize';
const actions = [
{ label: 'MiRA at a glance', key: '_glanceBounds' },
{ label: 'See the manual', key: '_manualBounds' },
{ label: 'Load example data', key: '_loadDataBounds' },
];

const textBlockTop = TITLE_OFF - FONT_TITLE / 2;
const textBlockBottom = ACTION_OFF + FONT_ACTION / 2;
const logoH = textBlockBottom - textBlockTop;

const img = this._logoImage;
const logoW = (img?.complete && img.naturalWidth > 0)
? Math.round(logoH * img.naturalWidth / img.naturalHeight)
: logoH;

ctx.font = 'bold 18px Inter, system-ui, sans-serif';
const line1W = ctx.measureText(line1).width;
ctx.font = '16px Inter, system-ui, sans-serif';
const line2W = ctx.measureText(devBy + labName).width;
ctx.font = '15px Inter, system-ui, sans-serif';
const line3W = ctx.measureText(line3).width;
ctx.font = `bold ${FONT_TITLE}px Inter, system-ui, sans-serif`;
const titleW = ctx.measureText(title).width;
ctx.font = `${FONT_HINT}px Inter, system-ui, sans-serif`;
const hintW = ctx.measureText(hint).width;
ctx.font = `${FONT_ACTION}px Inter, system-ui, sans-serif`;
const actionsW = ctx.measureText(actions.map(a => a.label).join(SEPARATOR)).width;

const textBlockW = Math.max(line1W, line2W, line3W);
const totalW = logoW + logoGap + textBlockW;
const startX = (w - totalW) / 2;
const textBlockW = Math.max(titleW, hintW, actionsW);
const startX = (w - (logoW + LOGO_GAP + textBlockW)) / 2;
const cy = h / 2;

if (img?.complete && img.naturalWidth > 0) {
ctx.drawImage(img, startX, cy + textBlockTop, logoW, logoH);
}

const textX = startX + logoW + logoGap;
const textX = startX + logoW + LOGO_GAP;
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';

ctx.fillStyle = '#1f2937';
ctx.font = 'bold 18px Inter, system-ui, sans-serif';
ctx.fillText(line1, textX, cy - 24);

ctx.font = '16px Inter, system-ui, sans-serif';
const devByW = ctx.measureText(devBy).width;
ctx.fillText(devBy, textX, cy + 4);
ctx.fillStyle = '#6366f1';
ctx.fillText(labName, textX + devByW, cy + 4);
const labW = ctx.measureText(labName).width;
this._ecoLabBounds = { x: textX + devByW, y: cy + 4 - 12, w: labW, h: 24 };
ctx.font = `bold ${FONT_TITLE}px Inter, system-ui, sans-serif`;
ctx.fillText(title, textX, cy + TITLE_OFF);

ctx.fillStyle = '#6b7280';
ctx.font = '15px Inter, system-ui, sans-serif';
ctx.fillText(line3, textX, cy + 44);
ctx.font = `${FONT_HINT}px Inter, system-ui, sans-serif`;
ctx.fillText(hint, textX, cy + HINT_OFF);

const line4 = 'See our visualization guidelines for best practices →';
ctx.font = '14px Inter, system-ui, sans-serif';
const line4W = ctx.measureText(line4).width;
ctx.fillStyle = '#6366f1';
ctx.fillText(line4, textX, cy + 70);
this._guidelinesBounds = { x: textX, y: cy + 70 - 11, w: line4W, h: 22 };
this._drawSplashActions(ctx, actions, textX, cy + ACTION_OFF, FONT_ACTION, SEPARATOR, HIT_PAD);

ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'left';
}

/** Draw the splash action row, recording a clickable hit box for each item. */
_drawSplashActions(ctx, actions, x, y, fontSize, separator, hitPad) {
ctx.font = `${fontSize}px Inter, system-ui, sans-serif`;
const sepW = ctx.measureText(separator).width;
let cursorX = x;

actions.forEach((action, i) => {
const labelW = ctx.measureText(action.label).width;
ctx.fillStyle = '#6366f1';
ctx.fillText(action.label, cursorX, y);
this[action.key] = { x: cursorX, y: y - hitPad, w: labelW, h: hitPad * 2 };
cursorX += labelW;

if (i < actions.length - 1) {
ctx.fillStyle = '#d1d5db';
ctx.fillText(separator, cursorX, y);
cursorX += sepW;
}
});
}

_drawLayerPolygon(ctx, layerIndex, layer) {
const corners = this.getLayerCorners(layerIndex);

Expand Down
Loading