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

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