Public
Edited
Jul 8, 2024
Importers
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// This config needs to be part of account or survey config
brandConfig = ({
colors: {
brand: brand, // or, provide and color hex code
accent: accent, // or, provide and color hex code
// The color of text which are usually displayed on top of the brand or accent colors.
"text-on-brand": "#ffffff",
},
fonts: {
"brand-font": font
}
})
Insert cell
Insert cell
script = ({
hashPrefix = ''
} = {}) => html`<script>
${updateMenu}
window.addEventListener('hashchange', () => updateMenu);
updateMenu();
</script>`
Insert cell
Insert cell
Insert cell
enableJavasscriptSnippet = html`<noscript class="noscript">
${enableJavascriptContent.outerHTML}
</noscript>`
Insert cell
Insert cell
surveyView = (questions, layout, config, answers, options) => {
const sections = d3.group(layout, d => d['menu'])
const survey = view`
${custom_css()}
${header(sections, config, options)}
<main id="main-content" class="bg-near-white">
<article data-name="article-full-bleed-background">
${['...', sectionsView(questions, layout, config, sections, answers, options)]}
</article>
</main>
${pageFooter()}
`
return survey;
}
Insert cell
Insert cell
exampleSurvey
Insert cell
Insert cell
custom_css = () => html`
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400&display=swap');
body {
font-family: ${brandConfig.fonts['brand-font']}
}
:root {
--lh-copy: 1.3;
}
.nav {}

.hide { display: none;}

.sticky-top {
position: sticky;
top: 0;
}
.sticky-bottom {
bottom: 0;
}
.lh-copy {
line-height: var(--lh-copy);
}

a:not(class) {
text-decoration: none;
color: var(--brand);
}

a:not(class):hover,
a:not(class):focus,
a:not(class):active {
text-decoration: underline;
}

${componentStyles.innerHTML}
</style>
`
Insert cell
Insert cell
Insert cell
header(d3.group(layout.data, d => d['menu']), surveyConfig, {
layout: 'relative',
hashPrefix: "foo|"
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
organizeSections = (sections) => d3.rollup([...sections.keys()].map(path => path.split("/")), (children) => children.map(child => child[1]).filter(_ => _), d => d[0])
Insert cell
addMenuBehaviour = {
window.addEventListener('hashchange', updateMenu);
invalidation.then(() => window.removeEventListener('hashchange', updateMenu));
updateMenu()

}
Insert cell
updateMenu = (dom = document) => {
if (!dom.querySelectorAll) dom = document;
// The top layer of the menu is always visible, but only one tab is highlighted
[...dom.querySelectorAll(".nav-1")].forEach(nav => {
const navHash = "#" + nav.href.split("#")[1].split("/")[0]
if (window.location.hash.startsWith(navHash)) {
nav.classList.add(...navActiveClasses);
} else {
nav.classList.remove(...navActiveClasses)
}
});
// The 2nd layer of menu only has the options relevant to the top layer
// And then the specific section within that layer is highlighted
[...dom.querySelectorAll(".nav-2")].forEach(nav => {
const navHash = nav.href.split("#")?.[1]
const topLayerNav = navHash.split("/")?.[0];
if (window.location.hash.length < 1) return;
const topLayerWindow = window.location.hash.split('#')[1].split("/")[0];
console.log(topLayerNav, topLayerWindow)

const nav2ActiveClasses = [...navActiveClasses, "text-on-brand"];
if (topLayerNav !== topLayerWindow) {
nav.classList.add("hide")
} else {
nav.classList.remove("hide")
if ("#" + navHash === window.location.hash) {
nav.classList.add(...nav2ActiveClasses)
} else {
nav.classList.remove(...nav2ActiveClasses)
}
}
});
// Due to Observablehq framing, the CSS :target selector for show/hide sections based on hash does not
// work properly. So we manually toggle it.
[...dom.querySelectorAll("[data-survey-section]")].forEach(section => {
if (`#${section.id}` === window.location.hash) {
section.style.display = "block";
} else {
section.style.display = "none";
}
});

if (isSurveyStandalone()) {
scrollToTop();
}
}
Insert cell
isSurveyStandalone = () => document.body.dataset.standaloneSurvey === "true";
Insert cell
scrollToTop = () => window.scrollTo(0,0);
Insert cell
[...d3.group(layout.data, d => d['menu']).keys()]
Insert cell
Insert cell
async function resolveObject(obj) {
return Object.fromEntries(await Promise.all(
Object.entries(obj).map(async ([k, v]) => [k, await v])
));
}
Insert cell
images = resolveObject({
"mainstream": FileAttachment("core_mainstream@1.jpg").url(),
"operation": FileAttachment("core_operation@1.jpg").url(),
"intro": FileAttachment("intro@3.jpg").url(),
"default": FileAttachment("core_intro@1.jpg").url(),
})
Insert cell
function imageFor(section) {
if (section.includes("mainstream")) {
return images.mainstream;
} else if (section.includes("operation")) {
return images.operation;
} else if (section.includes("intro")) {
return images.intro;
} else {
return images['default']
}w
}
Insert cell
Insert cell
viewof sectionViewExample = sectionsView(questions, layout.data, surveyConfig, d3.group(layout.data, d => d['menu']))
Insert cell
sectionViewExample
Insert cell
sectionsView = (questions, layout, config, sections, answers = new Map(), {
hashPrefix = '',
putFile,
getFile
} = {}) => {
const cells = new Map([...questions.entries()].map(([id, q], index) => [id, createQuestion({
...q,
value: answers.get(id)
}, index, {
putFile, getFile
})]));
bindLogic(cells, layout)
// We inject the views as just pure presentation
const sectionViews = [...sections.keys()].map(sectionKey => sectionView(config, cells, sections, sectionKey, {
hashPrefix
}))
// But we also want the questions inside the sections bound as a single flat list of questions.
// It should be flat as we don't want layout information leaking into data access model, e.g. we don't want
// moving a question to a different section to invalide persisted answers.
let questionViews = sectionViews.reduce(
(questions, section) => {
// Copy over section propties (which are views of questions) into growing mega object of views)
return Object.assign(questions, section)
}, {}
)
// Some questions are undefined if they cannot be looked up, we need to filter those out
questionViews = Object.fromEntries(Object.entries(questionViews).filter(([k , v]) => v));
const container = view`<div class="black-80">
${sectionViews}
${/* put our questions as hidden view*/ ['_...', questionViews]}
</div>`
return container;
}
Insert cell
viewof exampleSectionView = sectionView(surveyConfig,
new Map([...questions.entries()].map(([id, q]) => [id, createQuestion(q)])),
d3.group(layout.data, d => d['menu']),
"extended_survey/internal_operations")
Insert cell
exampleSectionView
Insert cell
sectionView = (config, cells, sections, sectionKey, {
hashPrefix = ''
} = {}) => {
const suffix = sectionKey.split("/").pop();
const subtitle = config.menuSegmentLabels?.[suffix] || suffix;
const orderedQuestions = sections.get(sectionKey).map(layoutRow => {
let cell = cells.get(layoutRow.id)
if (cell === undefined) {
cell = md`<mark>Error question not-found for ${layoutRow.id}</mark>`
}
cell.id = layoutRow.id
return cell;
});
const pageKeys = paginationKeys(sections, sectionKey);
const paginationEl = pagination({...pageKeys, hashPrefix});
// background-position-x is set to 4rem, which is approximate height of the header
return view`<section id="${hashPrefix}${sectionKey}"
data-survey-section="${hashPrefix}${sectionKey}"
class="pa2 pa4-ns pl5-l"
style="background: #f4f4f4 url(${imageFor(sectionKey)});
background-size: cover;
background-attachment: fixed;
background-position: center 4rem;
background-repeat: no-repeat;
display: ${location.hash === `#${hashPrefix}${sectionKey}`? 'block' : 'none'};
">
<div class="bg-white shadow-2 f4 measure-wide mr-auto">
<div class="ph4 pt3 pb0 f5 lh-copy">
<!-- <h1 class="mt0 mb4">${subtitle}</h1> -->
<div class="db">
${['...', orderedQuestions.reduce((acc, q) => Object.defineProperty(acc, q.id, {value: q, enumerable: true}), {})]}
</div>
</div>
<div class="sticky bottom-0">
<div class="ph4 pv3 bt b--black-10 bg-near-white">
${paginationEl}
</div>
</div>
</div>
</section>`
}
Insert cell
paginationKeys = (sections, key) => {
const tree = organizeSections(sections);
const keys = [...tree.keys()].reduce((acc, parent) => {
const subsections = tree.get(parent);
if (subsections.length > 0) {
return [
...acc,
...(subsections.map(sb => `${parent}/${sb}`))
]
}
return [...acc, parent];
}, []);

let previous;
let next;

const currentIndex = keys.findIndex(k => k === key);
if (currentIndex > 0) {
previous = keys[currentIndex - 1];
}

if (currentIndex < (keys.length - 1)) {
next = keys[currentIndex + 1];
}

return {
previous, next,
}
}
Insert cell
questions
Insert cell
Insert cell
{
// Initialising styles for demos to work on this notebook
initializeStyles()
}
Insert cell
initializeStyles = () => tachyonsExt(brandConfig)
Insert cell
Insert cell
stylesForNotebooks = html`<style>
a[href].nav {
color: var(--text-on-brand);
}

a[href].nav:hover {
text-decoration: none;
}

.black-90 {
color: rgba(0,0,0,.9) !important;
}`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more