Public
Edited
Jul 8, 2024
Importers
5 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
viewof surveyUi = {
(initialLoadQuestions, initialLoadLayout, load_config)

console.log("Executing surveyUi")

const updateEditorState = (state) => {
ui.dataset.surveyEditorState = state;
console.log(ui.dataset.surveyEditorState);
};
const resetEditorState = () => updateEditorState('editor');

const ui = view`<div
class="[ survey-ui ][ brand-font bg-near-white ba b--light-gray ]"
data-survey-editor-state="editor">
<div class="solid-shadow-y-1">${pageHeader(['Survey Designer'])}</div>
<main class="[ mr-auto mw9 ][ space-y-3 pa3 ]">
<div class="toolbar flex items-center">
<!-- <div class=""><a class="brand hover-underline" href="#">← Back</a></div> -->
<div class="ml-auto button-group">
${Inputs.button(buttonLabel({label: "Import", iconLeft: "download"}), {reduce: () => updateEditorState('import')})}
${Inputs.button(buttonLabel({label: "Export", iconLeft: "upload"}), {reduce: () => updateEditorState('export')})}
</div>
</div>

<div class="[ survey-editor__import ][ space-y-3 ]">
<div class="card space-y-3">
<div class="flex">
<h2 class="mr-auto">Import</h2>
${Inputs.button("Close", { reduce: resetEditorState})}
</div>
<div class="space-y-3">
<div>${importUi(resetEditorState)}</div>
</div>
</div>
</div>
<div class="[ survey-editor__export ][ space-y-3 ]">
<div class="card space-y-3">
<div class="flex">
<h2 class="mr-auto">Export</h2>
${Inputs.button("Close", { reduce: resetEditorState})}
</div>
<!-- Exports UI -->
<div>${exportUi()}</div>
</div>
</div>
<div class="[ survey-editor__editor ][ space-y-3 ]">
${['...', surveyEditor()]}
</div>
</main>
${pageFooter()}
</div>`

return ui;
}
Insert cell
Insert cell
Insert cell
questionsNoLayout
Insert cell
Insert cell
syncSurveyUiInputToSurveyUi = {
console.log("syncSurveyUiInputToSurveyUi");
if (!_.isEqual(viewof surveyUi.value, viewof surveyUiInput.value)) {
console.log("syncSurveyUiInputToSurveyUi: change detected");
viewof surveyUi.value = viewof surveyUiInput.value;
// Manually updating the UI state
// viewof surveyUi.applyValueUpdates();
viewof surveyUi.dispatchEvent(new Event('input', {bubbles: true}))
}
}
Insert cell
Insert cell
Insert cell
syncSurveyOutput = {
console.log("surveyOutput")
// convert ui representation (pages -> cells) to {questions, layout, config} for storage.

if (surveyUiOutput.pages.length === 0) return invalidation;
// Extract questions
const questions = new Map();
surveyUiOutput.pages.forEach(page => {
page.cells.forEach(cell => {
questions.set(cell.id, uiCellToQuestion({
...cell.inner.result,
type: cell.inner.type,
}))
})
});

// Extract layout
const layout = [];
surveyUiOutput.pages.forEach(page => {
page.cells.forEach(cell => {
const connections = cell?.connections?.connections || []
const set = connections.map(c => c.set).join(",");
layout.push({
id: cell.id,
menu: page.title,
set,
role: set === "" ? "" : connections.map(c => c.role).join(","),
})
})
});

// Extract config
const config = {
...viewof surveyConfig.value, // carry over initial state
pageTitle: surveyUiOutput.metadata.title
};
viewof surveyOutput.value = {
questions,
layout,
config
};
viewof surveyOutput.dispatchEvent(new Event('input', {bubbles: true}))
}
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
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof settings.value.versions.at(-1)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
onQuestionUpload = {
viewof questions.value = csvToQuestions(await questionUpload.csv());
viewof questions.dispatchEvent(new Event('input', {bubbles: true}));
}
Insert cell
function csvToQuestions(csv) {
return csv.reduce(
(acc, row) => {

// Now append the rows question attributes and values to the current question being processed
const attribute = row['key'];
const value = row['value'];
const id = row['id'] || acc.previous?.id;

let current = acc.previous;
if (id != acc.previous?.id) {
current = {
id: id
}
acc.questions.push(current)
}

const arrays = ['options', 'rows', 'columns'];
if (arrays.some(arr => attribute.startsWith(arr))) {
// But if the element is packed as an array we don't unwind
let packed = false;
try {
if (Array.isArray(eval(value))) {
packed = true;
current[attribute] = value;
}
} catch (err) {}

if (attribute === 'rows' && !Number.isNaN(+value)) {
// When rows is in a textarea is it not in an array
current[attribute] = value;
} else if (!packed) {
// Arrays come in a list of elements
const array = current[attribute] || [];
if (arrays.includes(attribute)) {
array.push({
value: value,
label: value
});
} else {
array.push(value);
}
current[attribute] = array;
}
} else {
current[attribute] = sanitizeValue(value);
}

return {
questions: acc.questions,
previous: current
}
}
, {
questions: [],
previous: null
}
).questions.reduce( // Index by id
(map, q) => {
const {id, ...value} = q
map.set(id, value)
return map;
},
new Map() // Map remembers insertion order which is useful
)
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
updateQuestionsCsvDataUriView = {
viewof questionsCsvDataUriView.value = questionsCsvDataUri /* sync questionsCsvDataUri changes to the view */
viewof questionsCsvDataUriView.dispatchEvent(new Event('input', {bubbles: true}))
}
Insert cell
downloadQuestionsCsv = htl.html`<a href=${viewof questionsCsvDataUriView.value} download="questions_${Date.now()}.csv">
Download questions.csv
</a>
${exportQuestionsProblems.length > 0 ? md`<mark> Warning, some questions are not exporting properly, you may lose data in export` : null}
`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
onLayoutUpload = {
viewof layoutData.value = {data: csvToLayout(await layoutUpload.csv())}
viewof layoutData.dispatchEvent(new Event('input', {bubbles: true}))
}
Insert cell
Insert cell
Insert cell
layoutCsvDataUri = URL.createObjectURL(new Blob([ d3.csvFormat(exportLayoutCSV) ], { type: 'text/csv' }));
Insert cell
viewof layoutCsvDataUriView = Inputs.input(undefined)
Insert cell
updateLayoutCsvDataUriView = {
viewof layoutCsvDataUriView.value = layoutCsvDataUri
viewof layoutCsvDataUriView.dispatchEvent(new Event('input', {bubbles: true}))
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
exportUi = () => {
const now = Date.now();

return view`<div class="space-y-3">
<div>
<a href=${viewof questionsCsvDataUriView.value} download="questions_${Date.now()}.csv">Download Questions</a>
</div>

<div>
<a href=${viewof layoutCsvDataUriView.value} download="layout_${Date.now()}.csv">Download Layout</a>
</div>
</div>`
}
Insert cell
Insert cell
viewof sampleImportUi = importUi()
Insert cell
sampleImportUi
Insert cell
importUi = (afterSave) => {
const submitFiles = async () => {
if (ui.value.questionsCsv) {
console.log('Updating questions CSV')
viewof questions.value = csvToQuestions(await ui.value.questionsCsv.csv());
}

if (ui.value.layoutCsv) {
console.log('Updating layout CSV')
viewof layoutData.value = {data: csvToLayout(await ui.value.layoutCsv.csv())}
}

if (ui.value.questionsCsv || ui.value.layoutCsv) {
viewof questions.dispatchEvent(new Event('input', {bubbles: true}));
viewof layoutData.dispatchEvent(new Event('input', {bubbles: true}))
}

if (typeof afterSave === 'function') {
afterSave();
}
}
const submit = Inputs.button(buttonLabel({label: "Save"}), {reduce: submitFiles});
const ui = view`<div class="space-y-3">
<h3 class="f5">Questions CSV file</h3>
${['questionsCsv', fileInput({prompt: "Drop questions as a CSV file here"})]}
<h3 class="f5">Layout CSV file</h3>
${['layoutCsv', fileInput({prompt: "Drop layout as a CSV file here"})]}
<div>
${submit}
</div>`
return ui;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
questionsNoLayout.values().next()
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
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
styles = html`<style>
/* Survey Editor */

.survey-editor__import,
.survey-editor__export,
.survey-editor__editor {
display: none;
}

[data-survey-editor-state="import"] .survey-editor__import,
[data-survey-editor-state="export"] .survey-editor__export,
[data-survey-editor-state="editor"] .survey-editor__editor {
display: block;
}

/* Styles when displayed as a stand alone notebook */
[data-standalone-designer-notebook] .observablehq > h2 {
padding-top: var(--spacing-medium);
border-top: 1px solid;
border-color: #eee; /* .b--light-gray */
}
</style>`
Insert cell
Insert cell
<style>
.survey-ui {
overflow-y: auto;
max-height: 600px;
overscroll-behavior-y: contain;
}
</style>
Insert cell
Insert cell
Insert cell
responses
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
Insert cell
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