Public
Edited
Aug 19, 2023
Insert cell
Insert cell
viewof blockDate = Inputs.date({label: "Date", value: Date.now() - new Date().getTimezoneOffset()*60*1000, submit: true})
Insert cell
blocksArray = []
Insert cell
drop = {

// there are 604800 seconds in a week (lane)
// there are 86400 seconds in a day

let offset = blockDate.getTimezoneOffset()

let now = Date.now() - offset*60*1000 // (UTC -8 hours)
let nowISO = new Date(now).toISOString()

let todayUnix = new Date(nowISO.substring(0, 10)).setUTCMinutes(offset).valueOf() // local today with not time, date only

// do we need to add a "today" index ref? Or can we use seconds to calculate?

let nowIdx = new Date(todayUnix).getDay()
let blockUnix = blockDate.setUTCMinutes(offset) // unix time in milliseconds

let unixGap = (todayUnix - blockUnix)/1000 // sec
let daysGap = unixGap / 86400

let lane = Math.floor((nowIdx - daysGap)/7)

let d = new Date(blockUnix)

let day = d.getDay()
let dayOfWk = day

let weekdays = ["Sun","Mon","Tues","Wed","Thurs","Fri","Sat"]
let weekday = weekdays[day]

let row = 6 - day

// this week always if the "daysGap" is bigger than the "day" position + 1

// if today it Thursday (it is index 4 of the week... 0 to 6)
// if we pick to a block on Mon (is index 1 of the week... 0 to 6)

let curDayBlocks = blocksArray.filter(b => b.lane == lane && b.row == row).length // look up blocks by existing row & lane

let stack = curDayBlocks + 1

let coords = blockCoords(row, lane, stack)

blocksArray.push({x: coords.x , y: coords.y, row, lane, stack})

return {
details: {now, nowISO, todayUnix, blockUnix, unixGap, daysGap, d, nowIdx, weekday},
idx: {row, lane, stack},
coords: {x: coords.x , y: coords.y}
}

}
Insert cell
Insert cell
blockCoords = (row, lane, stack) => {

// from input sliders for debug only
// let row = rowPosition
// let stack = stackPosition
// let lane = lanePosition

// Offsets for optimal position testing

// We can update the relative viewport x, y starting block coordinates for optimal placement
let xRef = 121 // 121 // 10
let yRef = 112 // 112 // 100

// each "day" represents a column position in a lane (week) (0 to 6, 1 to 7)
let xAdjRow = - row * 21 // decreasing steps of 21
let yAdjRow = row * 12 // increasing steps of 12

// each new block is "stacked" per day, so we can call that "stack position" (0 to 26, 1 to 27)
let yAdjStack = - stack * 24

// each week is a lane position (0 to 51, 1 to 52)

let xAdjLane = lane * 21
let yAdjLane = lane * 12

let xPosition = xRef + xAdjRow + xAdjLane
let yPosition = yRef + yAdjRow + yAdjStack + yAdjLane

return {x: xPosition, y: yPosition}
}
Insert cell
blockParty = {

// from the colorMatrix we load the CSS styles for various colored blocks
let cssBlockStyles = colorMatrix.map(color => color.class)

// sort the block array to draw blocks in the right order
blocksArray.sort((a, b) => a.lane - b.lane);
blocksArray.sort((a, b) => a.row - b.row);
blocksArray.sort((a, b) => a.stack - b.stack);

let blocksSvg = blocksArray.map(coords => {

let x = coords.x
let y = coords.y

return `<use class="color-33" href="#block" x="${x}" y="${y}"/>`
})

console.log(blocksSvg)

return html`

<svg viewBox="0 0 1222 333" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="block" class="cube-unit">
<rect width="21" height="24" fill="var(--left)" stroke="var(--stroke)" transform="skewY(30)"/>
<rect width="21" height="24" fill="var(--right)" stroke="var(--stroke)" transform="skewY(-30) translate(21 24.3)"/>
<rect width="21" height="21" fill="var(--top)" stroke="var(--stroke)" transform="scale(1.41,.81) rotate(45) translate(0 -21)"/>
</g>
</defs>

<g transform='translate(600 100)'>
<use class="color-30" href="#block" x="121" y="112"/>
${blocksSvg}
</g>
<g class="color-0" transform='translate(10 10)'>
<use href="#block" x="121" y="112"/>
<use href="#block" x="100" y="124"/>
<use href="#block" x="142" y="124"/>
<use href="#block" x="121" y="136"/>
<use href="#block" x="79" y="136"/>
<use href="#block" x="163" y="136"/>
<use href="#block" x="142" y="148"/>
<use href="#block" x="100" y="148"/>
<use href="#block" x="121" y="160"/>
<use href="#block" x="121" y="88"/>
<use href="#block" x="100" y="100"/>
<use class="color-6" href="#block" x="142" y="100"/>
<use href="#block" x="121" y="112"/>
<use class="color-12" href="#block" x="79" y="112"/>
<use href="#block" x="163" y="112"/>
<use href="#block" x="142" y="124"/>
</g>
<g class="color-18" transform='translate(222 100)'>
<use href="#block" x="121" y="48"/>
<use href="#block" x="121" y="24"/>
<use href="#block" x="121" y="0"/>
<use href="#block" x="100" y="60"/>
<use href="#block" x="100" y="36"/>
<use href="#block" x="100" y="12"/>
<use href="#block" x="142" y="60"/>
<use href="#block" x="142" y="36"/>
<use href="#block" x="142" y="12"/>
<use href="#block" x="163" y="72"/>
<use href="#block" x="163" y="48"/>
<use href="#block" x="163" y="24"/>
<use href="#block" x="79" y="72"/>
<use href="#block" x="79" y="48"/>
</g>
</svg>

<style>

.cube-unit {
fill-opacity: .9;
stroke-miterlimit:0;
}

${cssBlockStyles}

</style>`
}
Insert cell
generatedColors = {

let spinArray = []
for (let i = 10; i <= 360; i += 10){
spinArray.push(i);
}

let colors = spinArray.map(val => {

let init = "caf" //"#caf" // "#a8c" // "#ff0"

let color = tinycolor(init).spin(val).toHexString()

// let color = tinycolor.random().toHexString()
let darker5 = tinycolor(color).darken(5).toString()
let darker10 = tinycolor(color).darken(10).toString()
let darker15 = tinycolor(color).darken(15).toString()

return [color, darker5, darker10, darker15]
})

return colors
}
Insert cell
colorOptions = {

return swatches([].concat(...generatedColors))
}
Insert cell
Insert cell
colorMatrix = {

let blockColors = generatedColors.map((hex, idx) => {

let classString = `.color-${idx}{--right: ${hex[0]}; --top: ${hex[1]}; --left: ${hex[2]}; --stroke: ${hex[3]};}`

return {idx, hex, class: classString}

})

console.log(blockColors)

return blockColors

}
Insert cell
<svg viewBox="0 0 1111 333" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<g id="block" class="cube-unit">
<rect width="21" height="24" fill="var(--left)" stroke="var(--stroke)" transform="skewY(30)"/>
<rect width="21" height="24" fill="var(--right)" stroke="var(--stroke)" transform="skewY(-30) translate(21 24.3)"/>
<rect width="21" height="21" fill="var(--top)" stroke="var(--stroke)" transform="scale(1.41,.81) rotate(45) translate(0 -21)"/>
</g>
</defs>

<g class="color-11" transform='translate(10 10)'>
<use href="#block" x="121" y="112"/>
<use href="#block" x="100" y="124"/>
<use href="#block" x="142" y="124"/>
<use href="#block" x="121" y="136"/>
<use href="#block" x="79" y="136"/>
<use href="#block" x="163" y="136"/>
<use href="#block" x="142" y="148"/>
<use href="#block" x="100" y="148"/>
<use href="#block" x="121" y="160"/>
<use href="#block" x="121" y="88"/>
<use href="#block" x="100" y="100"/>
<use class="color-3" href="#block" x="142" y="100"/>
<use href="#block" x="121" y="112"/>
<use class="color-6" href="#block" x="79" y="112"/>
<use href="#block" x="163" y="112"/>
<use href="#block" x="142" y="124"/>
<use class="color-9" href="#block" x="100" y="124"/>
</g>

<g class="color-22" transform='translate(222 100)'>
<use href="#block" x="121" y="48"/>
<use href="#block" x="121" y="24"/>
<use href="#block" x="121" y="0"/>
<use href="#block" x="100" y="60"/>
<use href="#block" x="100" y="36"/>
<use href="#block" x="100" y="12"/>
<use href="#block" x="142" y="60"/>
<use href="#block" x="142" y="36"/>
<use href="#block" x="142" y="12"/>
<use href="#block" x="163" y="72"/>
<use href="#block" x="163" y="48"/>
<use href="#block" x="163" y="24"/>
<use href="#block" x="79" y="72"/>
<use href="#block" x="79" y="48"/>
</g>
</svg>

<style>

.cube-unit {
fill-opacity: .9;
stroke-miterlimit:0;
}

</style>
Insert cell
sampleData = {

// playground for data structures

// using the "date picker", we could push the minimal data needed to position blocks

let timeCoords = {
day: 0,
lane: 0,
stack: 0
}

// for each unique "date" (day), we should count the total "blocks" and give them a stack positon based on the order they were created in

// maybe these are not needed if we did a "fill" order in some type of default position?
let denseCoords = {
row: 0,
column: 0,
vert: 0
}

return {timeCoords, denseCoords}
}
Insert cell
Insert cell
viewof addNewBlock = Inputs.button(html`<div style="background:${generatedColors[0][0]};color:white;font-size:16px;border-radius: 12px;padding: 11px;"> Add Block
</div>`, {value: null, reduce: () => addBlock(blockDetails)})
Insert cell
visualizeTheseBlock = []
Insert cell
magicBlock = {

let newBlockData = visualizeTheseBlock

return
}
Insert cell
addBlock = (entry) => {

visualizeTheseBlock.push(entry)

console.log("this ran", entry)

}
Insert cell
config = {

// List of people who we'll for these value blocks
let people = [
{name: "Lindsay", location: "Los Angeles, CA"},
{name: "Bob", location: "Los Angeles, CA"}
]

// Block types can be added or edited here
let blockTypes = [
{name: "Sleep", group: "Life"},
{name: "Creation", group: "Work"}
]

// Add Frames here
let frames = [
{name: "Health", type: "Cause"},
{name: "Be:Cause", type: "Project", links: ['Vegan', 'Health']}
]

return {people, blockTypes, frames}

}
Insert cell
Insert cell
blocksHtml = {

let entries = reassemble //journalData.blockRecords
let cubit = journalData.blockIdx
let blcEmjClasses = emojiBlocksIndex.map(c => c.clrClass)
// Get the container elements
let ul = document.getElementById('blocksPreview')
let g = document.getElementById('daily-cubit')
//let style = document.getElementById('preview-styles')

let entriesHTML = entries.map(row => {

let frame = `<span class="frame-txt">${row.labels.frame}</span>`
let forWho = row.labels.forWho ? `<span class="for-txt sm">${row.labels.forWho}</span>` : ''
var group = row.labels.group ? `<span class="group-txt sm">${row.labels.group}</span>` : ''
let type = `<span class="blc-type sm">Doing</span>`
let details = row.details ? `<span class="details sm">${row.details}</span>` : ''

return `<p class="line"> ${row.main}
${frame}
${forWho}
${group}
${details}
</p>`
})


// Write over or Clear the existing lists
ul.innerHTML = entriesHTML.join('')
g.innerHTML = ''

// Create and append <use> elements based on the data array
cubit.forEach((blc, idx) => {
let use = document.createElement('use');
let classStrg = "emj-clr-" + blc.emojiIdx
use.setAttribute('class', classStrg);
use.setAttribute('href', '#block');
use.setAttribute('x', blc.cubitX);
use.setAttribute('y', blc.cubitY);
g.appendChild(use);
});

const svgElement = document.getElementById('cubit'); // Get the SVG element by ID

// Modify the innerHTML to trigger a redraw
svgElement.innerHTML = svgElement.innerHTML;

// Option #2 - Probably best for Email HTML

let htmlList = entries.map(item => {
return `<li class="mono-list">${item.main}</li>`
})

return {svgHtml: svgElement.innerHTML} //{htmlList}
}
Insert cell
reassemble = {

let parsed = journalData.parsed

let rendered = parsed.map(row => {
let blocks = row.blocks.raw.join('')
let words = row.text
let emojis = row.vibes

let main = blocks + " " + words + " " + emojis


let frame = row.frame
let forWho = row.who ? row.who.join(',') : null
let group = row.group
// let type = row.type
let labels = {frame, forWho, group}

let details = row.details

return {main, labels, details}
})

return rendered
}
Insert cell
Insert cell
Insert cell
journalData = {

// find indexOf():
// 1st "(" and optional 2nd "("
// 1st ")" and optional 2nd ")"
// 1st "|" and 2nd "|"
// 1st "{"
// 1st "}"

if(pastedBlockJournal){

var pastedDay = pastedBlockJournal
} else {
// Initial Sample Data
var pastedDay = `(B,L) May 27, 2023

🟦🟦 Quality Sleep 🛌 |Hlth| [ce]

🟪🟪 Eating Vegan 🌱 |Veg| [ce]

🟪🟪 Car Free🚶🏻‍♀️🚶🏻‍♂️|Eco| [ce]

🟪🟪 Minimalism 🪷|Simp| [ce]

🟩 Beach Medi (L) 🧘🏻‍♀️🏝 |Peac|:: Touched my inner self and found peace

🟩🟩 Morning Walk🚶🏻‍♀️🌹|Mov|:: Beautiful AM walk where we stopped and smelled the roses

🟨 Breakfast (L) 🍊🌮 |Hlth|

🟧🟧 Notes / Draw (B) 📘✍🏼 |+E|{EP1}

🟨 Online Groceries (L) 🛒🌭 |+L|

🟪 Beach Social (L) 🌊🌞|Scl|(Jl,Jy)

🟧 Roots of Old Game (B) 📝 |+E|{EP1}

🟧 Chat with GPT (B) 🔬🧫|+E|{EP1}

🟨 Dinner (L) 🍝 |Hlth|

🟩🟩 Streaming 📺 |Play|`

}

let blockRecords = pastedDay.split('\n').filter(line => line != "");

let lineOne = blockRecords.shift()
let lineOneLeftParIdx1 = lineOne.indexOf("(");

if(lineOneLeftParIdx1 == 0){
let lineOneRightParIdx1 = lineOne.indexOf(")");
var defaultCreators = lineOne.slice(lineOneLeftParIdx1 + 1, lineOneRightParIdx1).trim().split(",");
var date = lineOne.split(")")[1].trim()
} else {
var date = lineOne
var defaultCreators = null
}

var regex = /<a?:.+?:\d{18}>|\p{Extended_Pictographic}/gu; // /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;

var blocksIndex = []

// blocks, text, creators, vibes, frame, who, group, type, details

let parsed = blockRecords.map((row, idx) => {
let blockEntry = row

let firstLeftParIdx = blockEntry.indexOf("("); // returns -1 if none found
let closingLeftParIdx = blockEntry.indexOf("(", firstLeftParIdx + 1)
let openFrameIdx1 = blockEntry.indexOf("|");
let closeFrameIdx1 = blockEntry.indexOf("|", openFrameIdx1 + 1)
let finalFrameIdx = blockEntry.lastIndexOf("|")

let debug = {} // add debug info as needed

// blocks
let parts = row.split(" ")
let raw = parts.shift().match(regex)
blocksIndex.push(raw)
let qty = raw.length
let blocks = {raw, qty}
// text
let textStartPos = qty*2
let afterBlocks = blockEntry.slice(textStartPos)
let fistEmojiIdx = afterBlocks.search(regex) - 1

debug.firstEmjoiIdx = fistEmojiIdx
let startFrameIdx = afterBlocks.indexOf("|")
let startGroupIdx = afterBlocks.indexOf("{")
let nextBreakIdx
if(startGroupIdx && startGroupIdx < startFrameIdx){
// group "{" comes before frame "|"
nextBreakIdx = startGroupIdx
} else {
// typical break is at frame "|"
nextBreakIdx = startFrameIdx
}
debug.nextBreakIdx = nextBreakIdx
let textEndPos
let beginVibes
if(firstLeftParIdx == -1 || firstLeftParIdx > openFrameIdx1){
// use "emoji" break or "|" break
if(fistEmojiIdx > 0){
textEndPos = fistEmojiIdx + 1
beginVibes = textEndPos - 1
} else {
textEndPos = startFrameIdx
}
} else {
// use "(" break
textEndPos = afterBlocks.indexOf("(");
beginVibes = afterBlocks.indexOf(")");
}
let text = afterBlocks.slice(0, textEndPos).trim();

// vibes
let vibes = beginVibes ? afterBlocks.slice(beginVibes + 1, startFrameIdx).trim() : '' // .match(regex);
// creators
let leftParIdx1 = blockEntry.indexOf("(");
let rightParIdx1 = blockEntry.indexOf(")");
let creators
if(firstLeftParIdx == -1 || firstLeftParIdx > openFrameIdx1){
// use defaultCreator
creators = defaultCreators
} else {
creators = blockEntry.slice(leftParIdx1 + 1, rightParIdx1).trim().split(",");
}
// frame
let frame = blockEntry.slice(openFrameIdx1 + 1, closeFrameIdx1).trim();
// who
let afterFrame = blockEntry.slice(finalFrameIdx)
let leftParIdx2 = afterFrame.lastIndexOf("(");
let who
if(leftParIdx2 == -1){
// use defaultCreator
if(defaultCreators){
who = null
} else {
who = creators
}
} else {
let rightParIdx2 = afterFrame.lastIndexOf(")");
who = afterFrame.slice(leftParIdx2 + 1, rightParIdx2).trim().split(",");
}
// group
let leftCurlIdx = blockEntry.indexOf("{");
if(leftCurlIdx != -1){
let rightCurlIdx = blockEntry.indexOf("}");
var group = blockEntry.slice(leftCurlIdx + 1, rightCurlIdx).trim() //.split(",");
} else {
var group = null
}

// details
let details = blockEntry.split("::")
if(details.length == 2){
details = details[1].trim()
} else {
details = null
}
return {blocks, text, debug, creators, vibes, frame, who, group, details}

})

let processed = parsed.map((row, idx) => {

let creatorSplit = row.creators.map(creator => {

let qtyCreators = row.creators.length
let qtyBlocks = row.blocks.qty
let counts = {qtyCreators, qtyBlocks} // only return during debugging

let blocks = Math.ceil(qtyBlocks/qtyCreators) // round up on any partial block division
let text = row.text
let vibes = row.vibes
let frame = row.frame
let who = row.who
let group = row.group
let details = row.details

return {blocks, text, creator, vibes, frame, who, group, details}
})

return creatorSplit
}).flat()

// Block Index for Colored 3D Cubes

let flatBlockIdx = blocksIndex.flat()
let blockIdx = flatBlockIdx.map((blc,idx) => {
let hexCode = blc.codePointAt(0).toString(16)
let emojiIdx = emojiBlocksIndex.findIndex(obj => obj.hexCode === hexCode)
let cubitX = cubitPositions[idx].x
let cubitY = cubitPositions[idx].y
return {blc, hexCode, emojiIdx, cubitX, cubitY}
})

// Emoji Day Story

let emojiStory = (parsed.map(e => e.vibes)).join('')

// Stats

let uniqueCreators = Array.from(new Set(processed.map(obj => obj.creator)));

// Calculate stats summary
let statsSummary = processed.reduce((summary, obj) => {
const { creator, group, color, frame, type, blocks } = obj;
summary.totalBlocks += blocks;
summary.creators[creator] = (summary.creators[creator] || 0) + blocks;
summary.groups[group] = (summary.groups[group] || 0) + blocks;
// summary.colors[color] = (summary.colors[color] || 0) + blocks;
summary.frames[frame] = (summary.frames[frame] || 0) + blocks;
// summary.types[type] = (summary.types[type] || 0) + blocks;
return summary;
}, {
totalBlocks: 0,
creators: {},
groups: {},
// colors: {},
frames: {},
// types: {}
});

return {date, defaultCreators, blockRecords, blockIdx, parsed, processed, emojiStory, uniqueCreators, statsSummary}

}
Insert cell
//serverTestResp = (await fetch("https://9496-2600-1012-a001-e742-c993-9e6f-3e24-ba26.ngrok-free.app")).text()
Insert cell
Insert cell
Insert cell
emojiBlocksIndex = {

let emojiList = ["🟥", "🟧", "🟨", "🟩", "🟦", "🟪", "⬛️", "⬜️", "🟫"]
let colors = ["red", "darkorange", "gold", "forestgreen", "blue", "darkviolet", "black", "silver", "saddlebrown"]
let colorCodes = ["RED", "ORNG", "YLW", "GRN", "BLU", "PRPL", "BLK", "SLV", "BRN"]

let objList = emojiList.map((emoji, idx) => {
let color = colors[idx]
let hexColor = tinycolor(color).toHexString()
let colorCode = colorCodes[idx]
let hexCode = emoji.codePointAt(0).toString(16)
// let reDraw = String.fromCodePoint("0x"+hexCode);

let l10 = tinycolor(color).lighten(10).toString()
let l20 = tinycolor(color).lighten(20).toString()
let l30 = tinycolor(color).lighten(30).toString()

let cbHxClrs = [l30, l20, l10, hexColor]

let clrClass = `.emj-clr-${idx}{--right: ${cbHxClrs[0]}; --top: ${cbHxClrs[1]}; --left: ${cbHxClrs[2]}; --stroke: ${cbHxClrs[3]};}`
return {emoji, color, hexColor, colorCode, hexCode, cbHxClrs, clrClass}
})

return objList
}
Insert cell
emojiColors3D = {
return swatches([].concat(...(emojiBlocksIndex.map(c => c.cbHxClrs))))
}
Insert cell
emjBlckClasses = {
let blcEmjClasses = emojiBlocksIndex.map(c => c.clrClass)
let cssClasses = blcEmjClasses.join('\n')
return html`<style>${cssClasses}</style>`
}
Insert cell
Insert cell
tinyColorPallet = {
let tcArray = Object.values(tcColorNames)
let hex = tcArray.map(c => '#' + c)
return swatches(hex)
}
Insert cell
<style>

.line {
font-family: "Roboto", Monospace; /* Specify the font family */
margin: 0; /* Reset default margin */
line-height: 1.2; /* Adjust the line spacing */
font-size: 17px;
margin-bottom: 8px;
}

.sm {
font-size: 17px;
}

.frame-txt {
display: inline-block;
padding: 2px 9px;
border: 1px solid silver;
}

.for-txt {
display: inline-block;
padding: 4px 9px;
background-color: whitesmoke; /* Set your desired background color */
color: grey; /* Set the text color */
border-radius: 30px; /* Adjust the border-radius to control the roundness */
}

.group-txt {
display: inline-block;
padding: 2px 9px;
border: 1px solid silver;
color: grey;
border-radius: 30px;
}

.blc-type {
display: inline-block;
padding: 3px 9px;
color: grey;
background-color: whitesmoke; /* Set your desired background color */
}

.details {
color: silver
}

</style>
<p class="line"> 🟧🟧🟧🟧🟧 Coded HTML / CSS / JS 👨🏻‍💻
<span class="frame-txt">Better Economy</span>
<span class="for-txt sm">Lindsay</span>
<span class="group-txt sm">Economy Pilot</span>
<span class="blc-type sm">Doing</span>
<span class="details sm">Lots of extra details about why this block is so amazing here. It is really very cool and it is worth sharing with the world. </span>
</p>
Insert cell
<style>

.toggle {
--width: 40px;
--height: calc(var(--width) / 2);
--border-radius: calc(var(--height) / 2);
display: inline-block;
cursor: pointer;
}

.toggle-input {
display: none;
}

.toggle-details {
display: none;
}

.toggle-fill {
position: relative;
width: var(--width);
height: var(--height);
border-radius: var(--border-radius);
background: #dddddd;
transition: background 0.2s;
}

.toggle-fill::after {
content: "";
position: absolute;
top: 0;
left: 0;
height: var(--height);
width: var(--height);
background: #ffffff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.25);
border-radius: var(--border-radius);
transition: transform 0.2s;
}

.toggle-input:checked ~ .toggle-fill {
background: yellowgreen;
}
.toggle-input:checked ~ .toggle-fill::after {
transform: translateX(var(--height));
}

.toggle-input:checked ~ .toggle-details {
display: block;
}
</style>

<label class="toggle" for="myToggle">
<input class="toggle-input" name="" type="checkbox" id="myToggle">
<div class="toggle-fill"></div>
<p class="toggle-details"> Some extra words </p>
<p> Other text </p>
</label>
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