Published
Edited
Oct 17, 2019
1 fork
1 star
Insert cell
Insert cell
fontFamily = "'barlow condensed', sans-serif"
Insert cell
main = render(({useSetter}) => {
const [layout, setLayout] = useState(initialLayout)
const [title, setTitle] = useState(initialTitle)
useSetter(layout)

const onImport = useCallback((e) => doImport(e, {setLayout, setTitle}), [setLayout, setTitle, doImport])
const onExport = useCallback(() => doExport({layout, title}), [layout, title, doExport])
const onTitleChange = useCallback((e) => doTitleChange(e, setTitle) , [setTitle])
return jsx`
<div className="row-1" style=${{display: 'flex'}}>
<div key="0" className="vertical-divider divider-left"></div>
<div key="1" className="f3 sans-serif" style=${{marginTop: "1em", width: "100%"}}><span style=${{fontSize: "125%", verticalAlign: "middle"}}>🗺</span> <input class="title" placeholder="MapScript" value=${title} onInput=${onTitleChange}/><span className="button-tray"><button onClick=${onExport}>⤓ EXPORT</button><input className="import" type="file" onInput=${onImport} onChange=${onImport}/></span></h1>
<div key="2" className="vertical-divider divider-right"></div>
</div>
<${BreadLoaf}
makeElement=${makeElement}
makeSlice=${() => (
{type: 'console'} || {type: 'parallel-coordinates'} || {type: 'trellis'} || {type: 'inspector'}
)}
updateLayout=${(data, trigger, view) => {
console.log("updateLayout", {data, trigger, view})
updateHashState({layout: data})
setLayout(data)
}}
finishAll=${() => {
// console.log("on finishAll")
}}
onEntered=${() => {
// console.log("on entered")
}}
onExited=${() => {
// console.log("on exited")
}}
onMoved=${(n) => {
instanceTable.findAll().forEach((instance) => instance.refresh && instance.refresh())
}}
layout=${layout}
footer=${Footer}
/>`
})
Insert cell
function doTitleChange(e, setTitle) {
const title = e.target.value
updateHashState({title})

setTitle(title)
}
Insert cell
function doExport({layout, title}) {
const width = 800, height = 800

const hs = viewof hashState.value
const born = hs.born
const mtime = hs.mtime

let ts = mtime instanceof Date ? mtime : new Date()

const year = ts.getFullYear()
const month = (ts.getMonth()+1).toString().padStart(2, '0')
const day = (ts.getDay()+1).toString().padStart(2, '0')
const seconds = ts.getSeconds() + (60 * (ts.getMinutes() + (60 * ts.getHours())));
const timesuffix = `-${year}-${month}-${day}-${seconds}`

let name = "map"
if (title && title.length > 0) {
name = title.toLowerCase().replace(/ /g, "-")
}
name += `${timesuffix}.mapscript.png`

downloadMap({
name,
title,
positions: viewof positions.value,
meridians: viewof meridians.value,
yAnchor: viewof yAnchor.value,
xAnchor: viewof xAnchor.value,
width,
height,
born,
mtime,
description: JSON.stringify({layout, title, source: instanceTable.editorActions.source}),
})
}
Insert cell
function doImport(e, {setLayout, setTitle}) {
const file = e.target.files[0];

if (!file) {
return
}
var reader = new FileReader();
reader.onload = function () {
const {Description} = readPngText(new Uint8Array(this.result))
const {source, layout, title} = JSON.parse(Description)
if (layout) {
setLayout(layout)
}
if (title) {
setTitle(title)
}
instanceTable.editorActions.source = source
};
reader.readAsArrayBuffer(file);
e.target.value = null;
}
Insert cell
main.value
Insert cell
import {ParallelCoordinatesView, TrellisView, style as vizStyle} from 'f91979a51e9fbe0a'
Insert cell
import {ConsoleView} from 'f11f2bc0cd96a9c9'
Insert cell
import {Search, SelectionView} from '8a16453ac64d7b1a'
Insert cell
import {MapView, download as downloadMap} from 'a57fb39c89cfae00'
Insert cell
import {table} from "425af7571aaefece"
Insert cell
import {World, editor} from '6e557ad8d4923730'
Insert cell
import {detectDPI} from 'f441540396f6ff43'
Insert cell
import {write as writePngText, read as readPngText, chunksEncodeText, chunkPhysicalPixelDimensionsFromDPI, chunkTimestamp} from '8e3e05cee416fda4'
Insert cell
import {
viewof meridians,
viewof positions,
viewof xAnchor,
viewof yAnchor,
xExtent,
yExtent,
drag,
update,
nodes,
} with {
world
} from 'fa57406f769082d6'
Insert cell
import {jsx, render, component, BreadLoaf, useState, useMemo, style as baseStyle, useRef, useCallback, useLayoutEffect} with {dev} from 'bcef231b0388fd15'
Insert cell
import {svelte, render as svelteRender, readable, derived, derivedPromises, generatedPromises, readableInput, getComponent} from '@ajbouh/svelte'
Insert cell
initialTitle = viewof hashState.value.title || ''
Insert cell
initialLayout = viewof hashState.value.layout || [
{ rowId: 2, items: [{ id: 30, type: 'map'}]},
{ rowId: 1, items: [{ id: 42, type: 'editor'}]},
// { rowId: 1, items: [{ id: 44, type: 'browser'}]},
]
Insert cell
Insert cell
makeElement = ({fork, close, beginDrag, view: {type, id, ...view}, layout, updateView}) => {
const ref = useRef()
const refLayout = useRef()
refLayout.current = layout

useLayoutEffect(() => {
let entry = instanceTable.find(id)
if (!entry) {
entry = instanceTable.make(id, type)
if (!entry) {
throw new Error("can't make type: " + type)
}
}

const {instance} = entry
const {element, title} = instance
view.title === title || updateView({title})
if (instance.mount) {
instance.mount({view: {type, id, ...view}, updateView})
}
const cur = ref.current

if (cur.children[0] !== element) {
if (cur.children[0]) {
cur.replaceChild(element, cur.children[0])
} else {
cur.appendChild(element)
}
}

instance.refresh && setTimeout(() => instance.refresh(), 50)
});
function headerMouseDown(e) {
if (e.button !== 0) {
e.preventDefault()
e.stopPropagation()
return false
}
return beginDrag(e)
}

function copy() {
fork()
}

function kill() {
const instance = instanceTable.kill(id) || {}
console.log("kill", instance, instance.kill)
instance.kill && instance.kill()
close()
}
function maximize() {
if (!ref.current) {
return
}
ref.current.requestFullscreen().catch(err => {
console.error(err)
})
}

return jsx`<div className="slice-wrap ${type}" style=${{flex: 1}} >
<div className="slice" style=${{flex: 1}}>
<div ref=${ref} className="lh-copy code" style=${{flex: 1, overflow: 'scroll'}}/>
<div className="slice-header" onMouseDown=${headerMouseDown}>
<div style=${{flexGrow: 1}}><span className="handle">⠿</span> <span className="title">${view.title || ''}</span></div>
<button className="expand" onClick=${maximize}>⤡\u00a0\u00a0FULLSCREEN</button>
<button onClick=${kill}>\u00D7</button>
</div>
</div>
</div>`
}
Insert cell
workerScenarioStore = generatedPromises(() => world.generator(w => {
const s = w.scenarios()[0]
return s && s.worker
}))
Insert cell
instanceTable = {
const editorInstance = editor({
world,
value: viewof hashState.value.source,
autosave: (source) => updateHashState({source}),
width: '100%',
height: '100%',
style: {maxWidth: '100%', height: '100%', cursor: 'text'},
options: {lineWrapping: true},
})
const editorActions = {
get editor() {
return editorInstance.actions
},
get source() {
return this.editor.source
},
set source(v) {
return this.editor.source = v
},
reveal(mark) {
this.editor.reveal(mark)
},
select(mark) {
this.editor.select(mark)
},
focus() {
this.editor.focus()
}
}
function svelteInstance(...args) {
const generator = svelteRender(...args)
const {value} = generator.next()
const svelteComponent = getComponent(value)
return {
svelteComponent,
refresh() {
if (svelteComponent.refresh) {
svelteComponent.refresh()
}
},
element: value,
kill: () => setTimeout(() => generator.return(), 0),
}
}

const instanceTable = new InstanceTable({
'editor': () => {
return {
title: "Editor",
refresh() {
this.element.editor.refresh()
},
element: editorInstance,
}
},
'table': () => {
return {
title: "Table",
element: table([
{name: "Adam", food: "Pizza"},
])
}
},
// 'trellis': () => ({
// title: "Plot",
// ...svelteInstance(TrellisView, {
// height: 300,
// data: generatedPromises(() => world.generator(w => {
// try {
// const scenarios = w.scenarios()
// console.log("scenarios", scenarios)
// if (scenarios && scenarios[0]) {
// const result = scenarios[0].worker.samples()
// console.log("result", result)
// return result
// }
// } catch (e) {
// console.log(e)
// }
// return {values: [], schema: []}
// }), {values: [], schema: []}),
// }),
// }),
'parallel-coordinates': () => {
return {
title: "Parallel Coordinates",
...svelteInstance(ParallelCoordinatesView, {
height: 300,
fontFamily,
labelFontSize: 14,
tickFontSize: 12,
data: derivedPromises(
workerScenarioStore,
scenario => world.generator(() => {
return scenario ? scenario.samples() : {samplingGroups: []}
}),
{samplingGroups: []},
),
}),
}
},
'browser': () => ({
title: "Browser",
...svelteInstance(
svelte `
{#if src}
<iframe style="border: 0" width="100%" height="100%" src="{src}" />
{/if}

<script>

export let selectionStore
export let scenarioStore

let src

$: scenario = $scenarioStore
$: selectedComponentDef = $selectionStore.find(d => d.type === "component-def")
$: selectedUse = selectedComponentDef && Array.from(scenario.usesOf(selectedComponentDef.component))[0]
$: src = selectedUse && selectedUse.attrs.url
</script>
`,
{
scenarioStore: generatedPromises(() => {
return world.generator(w => {
const sc = w.scenarios()[0]
return sc && sc.ui
})
}),
selectionStore: generatedPromises(() => {
return world.generator(w => {
const r = w.selected()
return r
})
}, []),
},
)
}),
'map': () => ({
title: "Map",
...svelteInstance(MapView, {
drag,
defaultFontFamily: "'Barlow Semi Condensed', sans-serif",
defaultFontStyle: undefined,
defaultFontColor: "#555",
defaultFontWeight: 600,
axisFontFamily: "'Barlow', sans-serif",
axisLetterSpacing: 2,
axisFontSize: 13,
axisFontColor: "#777",
axisFontWeight: 800,
xExtent,
yExtent,
highlight: generatedPromises(() => world.generator(w => {
const selectedKeys = new Set(w.selected(d => d.key))
return d => selectedKeys.has(d.key)
}), d => false),
editorActions,
positions: readableInput(viewof positions),
meridians: readableInput(viewof meridians),
yAnchor: readableInput(viewof yAnchor),
xAnchor: readableInput(viewof xAnchor),
},
html`<div style="height: 100%;" class="min-half-height"></div>`),
}),
'console': () => {
let updateView
let id
return {
title: "Console",
mount({view: {id: _id}, updateView: _updateView}) {
updateView = _updateView
id = _id
this.svelteComponent.focus()
},
...svelteInstance(ConsoleView, {
styleMaxHeight: '30rem', // really just here to test scrolling
styleHeight: '100%',
runCommand(text) {
if (text === "open") {
return {text, flags: {}, result: "opening..."}
}
try {
const type = text
const entry = instanceTable.find(id)
if (entry && entry.type !== type) {
const {instance} = instanceTable.kill(id) || {}
instance && instance.kill && instance.kill()
instanceTable.make(id, type)
updateView({type})
}
return {text, flags: {}, result: "launching..."}
} catch (e) {
return {text, flags: {error: true}, result: e.message}
}
},
},
html`<div style="height: 100%"></div>`),
}
},
// 'search': () => ({
// title: "Search",
// ...svelteInstance(Search, {
// components: generatedPromises(() => world.generator(w => w.components())),
// drag,
// width: '100%',
// xExtent,
// yExtent,
// positions: readableInput(viewof positions),
// yAnchor: readableInput(viewof yAnchor),
// xAnchor: readableInput(viewof xAnchor),
// }),
// }),
'inspector': () => {
return {
title: "Inspector",
...svelteInstance(SelectionView, {
editorActions,
scenarioStore: generatedPromises(() => {
return world.generator(w => {
const sc = w.scenarios()[0]
return sc && sc.ui
})
}),
selectionStore: generatedPromises(() => {
return world.generator(w => {
const r = w.selected()
return r
})
}, []),
}),
}
},
})
// This is a weird thing to do...
instanceTable.editorActions = editorActions
window.instanceTable = instanceTable
return Generators.disposable(instanceTable, et => et.killAll().forEach(t => t.kill && t.kill()))
}
Insert cell
class InstanceTable {
constructor(types) {
this.types = types
this.byId = new Map()
this.byType = new Map()
}
find(id) {
return this.byId.get(id)
}
findAll(type) {
if (type === undefined) {
return Array.from(this.byId.values(), d => d.instance)
}

return Array.from(this.byType.get(type) || [], d => d.instance)
}
make(id, type) {
if (this.byId.has(id)) {
throw new Error("can't make an id we already have " + id)
}
const ctor = this.types[type]
if (!ctor) {
throw new Error("can't make unknown type: " + type)
}
const instance = ctor()
const entry = {id, instance, type}
this.byId.set(id, entry)
let set = this.byType.get(type)
if (!set) {
set = new Set()
this.byType.set(type, set)
}
set.add(entry)
return entry
}
kill(id) {
const entry = this.byId.get(id)
if (!entry) {
console.log("id not found: " + id)
return
}
this.byId.delete(id)
const existing = this.byType.get(entry.type)
if (existing) {
existing.delete(entry)
}
return entry.instance
}
killAll() {
const r = Array.from(this.byId.values(), d => d.instance)
this.byId.clear()
this.byType.clear()
return r
}
}
Insert cell
function readHashState() {
var hash = window.location.hash
if (!hash) {
return {}
}

try {
var parts = hash.substr(1).split("_")
if (parts.length <= 1) {
return {}
}
var options = parts[1]
if (options.length <= 1 || options[0] !== ".") {
return {}
}
var b64 = options.substr(1)
var json = atob(b64)
return JSON.parse(json)
} catch (e) {
console.error(e)
return {}
}
}
Insert cell
viewof hashState = new View(readHashState())
Insert cell
class View {
constructor(value) {
Object.defineProperties(this, {
_list: {value: [], writable: true},
_value: {value, writable: true}
});
}
get value() {
return this._value;
}
set value(value) {
this._value = value;
this.dispatchEvent({type: "input", value});
}
addEventListener(type, listener) {
if (type != "input" || this._list.includes(listener)) return;
this._list = [listener].concat(this._list);
}
removeEventListener(type, listener) {
if (type != "input") return;
this._list = this._list.filter(l => l !== listener);
}
dispatchEvent(event) {
const p = Promise.resolve(event);
this._list.forEach(l => p.then(l));
}
}
Insert cell
function writeHashState(state) {
window.location.hash = "_." + btoa(JSON.stringify(state))
}
Insert cell
function updateHashState(update) {
const existing = readHashState()
let times = {}
const now = new Date()
if (!'mtime' in update) {
times.mtime = now
}
if (!'born' in existing) {
times.born = now
}
writeHashState(Object.assign(existing, update, times))
}
Insert cell
world = {
const world = new World(['x', 'y'])
window.world = world // for debugging!
return world
}
Insert cell
update
Insert cell
dev = false
Insert cell
style = html`${baseStyle}
${vizStyle}
<link href="https://fonts.googleapis.com/css?family=Barlow|Barlow+Condensed|Barlow+Semi+Condensed" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Playfair+Display" rel="stylesheet">

<style>

#main .min-half-height {
min-height: 50vh;
}

#main .bread-total-rows-1 .min-half-height {
min-height: 85vh;
}

.CodeMirror {
max-height: 85vh;
}

.header h1 {
font-weight: 100;
}

.slice {
/*border: 1px solid #d8d8d8;*/
min-height: 100px;
box-shadow: 1px 2px 7px rgba(0, 0, 0, 0.17);
transition: opacity 200ms ease-in;
margin-right: auto;
margin-left: auto;
height: 100%;
background: white;
display: flex;
flex-direction: column;
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
}

.bread-col.dragging .slice {
opacity: 0.3;
}

.slice.dragging {
opacity: 0.3;
}

.slice .body {
padding: 10px;
overflow: hidden;
flex-grow: 1;
max-height: 70vh;
}

.slice-header:hover {
background: #eee;
}
.slice-header {
padding: 7px 10px;
/* color: white; */
background: #f1f1f1;
cursor: grab;
display: flex;
white-space: nowrap;
font-weight: 500;
color: #555;
}

.slice-header .handle {
padding-top: 0.2em;
display: inline-block;
}

.slice-header .title {
padding-left: 1em;
padding-bottom: 0.3em;
display: inline-block;
font-size: 11px;
}

.slice-header .title, .slice-header button, label.import {
color: #777;
vertical-align: middle;
font-family: BlinkMacSystemFont;
text-transform: uppercase;
}

input[type=file]::-webkit-file-upload-button {
border: none;
margin: 0;
padding: 0;
-webkit-appearance: none;
width: 0;
}

input[type=file]::after {
content: '→ IMPORT ...';
}

/* "x::-webkit-file-upload-button" forces the rules to only apply to browsers that support this pseudo-element */
x::-webkit-file-upload-button, input[type=file]::after {
display: inline-block;
position: relative;
}

.button-tray {
float: right;
font-size: 11px;
}

.button-tray button, input[type=file] {
margin-top: 4px;
vertical-align: unset;
}

.button-tray button, input[type=file]::after {
padding: 4px 8px;
background: white;
}

.button-tray button:hover, input[type=file]:hover::after {
background: #eee;
}

.slice-header button.expand {
font-variant: small-caps;
font-size: 11px;
font-weight: 600;
color: #999;
}


input.title {
margin-left: 0.25em;
vertical-align: unset;
font-family: inherit;
font-size: inherit;
outline: none;
border: 0;
padding: 0;
}

input[type="file"].import {
width: 8em;
}

.slice-header button {
background: transparent;
display: block;
font-size: inherit;
color: inherit;
font-weight: 300;
}

.button-tray button, input[type=file]::after, .slice-header button, input[type="file"].import, label.import {
border: 0;
cursor: pointer;
border-radius: 3px;
line-height: 1.4em;
outline: none;
}

.slice-header button:hover {
background: rgba(255,255,255,0.5);
}

.slice-header .button-toggle {
font-size: 11px;
margin-top: 5px;
display: inline-block;
position: relative;
cursor: pointer;
color: gray;
margin-right: 5px;
-webkit-user-select: none;
}

.bread.dragging * {
cursor: grabbing;
}

.slice-header .button-toggle.active {
color: purple;
}

.slice-header .button:hover {
color: black;
}

.slice-header .button.active {
color: purple;
}

.fake-slice {
border: 2px dashed #d8d8d8;
border-radius: 2px;
min-height: 30px;
padding-bottom: 20px;
text-align: center;
font-size: 100px;
color: #d8d8d8;
-webkit-user-select: none;
cursor: pointer;
flex-grow: 1;
transition: all 200ms ease-in;
margin: 0 19px;
}

.fake-slice:hover {
border: 2px dashed gray;
color: gray;
}

.bread-row > * {
max-height: calc(100vh - 3em);
}

@media (max-width: 700px) {
.bread-row > span {
display: block;
}

.vertical-divider {
display: none;
}

.bread-col {
padding: 5px 10px;
}
}

@media (min-width: 700px) {
.row-1 {
max-width: 70vw;
margin-left: auto;
margin-right: auto;
}
}
</style>
`
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more