export type SampleT = { type: ".omd" | ".ojs", content: string }; export const samples: { [key: string]: SampleT } = { "Covid-Globe (.omd)": { "type": ".omd", "content": "\n# Coronavirus (COVID-19) Globe\n\n\nLatest data regarding Corona Virus cases, provided by the [World Health Organization](https://www.who.int/). Available in API form via [https://github.com/NovelCOVID/API](https://github.com/NovelCOVID/API).\n\nPlotted using [globe.gl](globe.gl).\n\n```\nviewof valMode = radio({\n title: 'Color by',\n options: [\n { label: 'Cases', value: 'cases' },\n { label: 'Cases Today', value: 'todayCases' },\n { label: 'Deaths', value: 'deaths' },\n { label: 'Deaths Today', value: 'todayDeaths' },\n { label: 'Recovered', value: 'recovered' },\n { label: 'Critical', value: 'critical' }\n ],\n value: 'cases'\n})\n\nviewof popRel = checkbox({\n options: [\n { value: \"true\", label: \"Relative to population\" }\n ],\n value: \"true\"\n})\n\n{\n const domEl = document.createElement('div');\n covidGlobe(domEl);\n covidGlobe.pointOfView({ lat: 32, altitude: 1.8 }, 2000); // tilt globe slightly north\n \n return domEl;\n}\n \n```\n${covidData === cachedCovidData ? '(*using cached data*)' : ''}\n```\n{ // globe configuration\n // set color domain\n colorScale.domain([0, Math.max(...covidFeatureData.map(getFeatureVal))]);\n \n covidGlobe\n .width(640)\n .height(640)\n .polygonsData(covidFeatureData)\n .polygonAltitude(0.06)\n .polygonCapColor(feat => colorScale(getFeatureVal(feat)))\n .polygonSideColor(() => 'rgba(0, 100, 0, 0.15)')\n .polygonStrokeColor(() => '#111')\n .polygonLabel(({ covidData, properties }) => { \n const capIt = str => str.charAt(0).toUpperCase() + str.slice(1);\n const relToPop = v => `${d3.format('.2')(v / properties.POP_EST * 100)}%`;\n const popRatio = v => d3.format('.3~s')(properties.POP_EST / v);\n const relToCases = v => `${d3.format('.2')(v / covidData.cases * 100)}%`;\n \n const formatVal = prop => `${covidData[prop]} ${prop === 'cases' ? `(1 in every ${popRatio(covidData[prop])} ppl)` : covidData[prop] ? `(${relToCases(covidData[prop])} of cases)` : ''}${covidData[prop] && covidData.hasOwnProperty(`today${capIt(prop)}`) ? ` | new today: ${covidData[`today${capIt(prop)}`]}` : ''}`;\n \n return`\n ${covidData ? covidData.country : properties.NAME} (${properties.ISO_A2}):
\n ${(!covidData\n ? [['Cases', 0]] \n : [\n ['Cases', formatVal('cases')],\n ['Deaths', formatVal('deaths')],\n ['Recovered', formatVal('recovered')],\n ['Critical', formatVal('critical')],\n ['Population', d3.format(\".3s\")(properties.POP_EST)]\n ]\n ).map(([label, val]) => `${label}: ${val}`)\n .join('
')\n }\n ` })\n .onPolygonHover(hoverD => covidGlobe\n .polygonAltitude(d => d === hoverD ? 0.12 : 0.06)\n .polygonCapColor(d => d === hoverD ? 'steelblue' : colorScale(getFeatureVal(d)))\n )\n .polygonsTransitionDuration(300);\n}\n\ncovidGlobe = Globe()\n .height(640)\n .globeImageUrl('https://cdn.jsdelivr.net/npm/three-globe/example/img/earth-night.jpg')\n // .backgroundImageUrl('//cdn.jsdelivr.net/npm/three-globe/example/img/night-sky.png')\n\ngetFeatureVal = feat => (feat.covidData? feat.covidData[valMode] : 0) / (popRel ? feat.properties.POP_EST : 1)\n```\n**Style**\n```\nhtml``\n```\n**Settings**\n```\ncolorScale = d3.scaleSequentialPow(d3.interpolateYlOrRd)\n .exponent(1/4);\n```\n**Data**\n```\ncovidData = fetch(\n 'https://cors-anywhere.herokuapp.com/corona.lmao.ninja/v2/countries', \n { headers: { 'x-requested-with': 'observablehq.com' }}\n)\n .then(r => r.json())\n .catch(() => cachedCovidData) // use cached data if api is unavailable\n\ncachedCovidData = FileAttachment(/* \"covidData-cached@2.json\" */\"https://static.observableusercontent.com/files/61f477377c4aabc49e40014f77f8758e9de4a58e1e1d46bc2303ef6933b5dfb4f92575f5579996306be447936e9fb2bc140e72d8b712b77f8c6cc2472d784b21\").json()\n\ncountryNameMapper = ({\n 'bosnia and herzegovina': 'bosnia and herz.',\n drc: 'dem. rep. congo',\n 'dominican republic': 'dominican rep.',\n 'ivory coast': 'Côte d\\'Ivoire',\n 's. korea': 'south korea',\n taiwan: 'Taiwan, Province of China',\n uae: 'United Arab Emirates',\n uk: 'United Kingdom',\n usa: 'United States of America'\n})\n\ncovidFeatureData = {\n const dataByCountry = indexBy(covidData, d => (countryNameMapper[d.country.toLowerCase()] || d.country).toLowerCase(), false);\n \n return countries110m.features\n .filter(d => d.properties && d.properties.NAME)\n .map(d => ({\n ...d,\n covidData: dataByCountry[d.properties.NAME.toLowerCase()]\n })); \n}\n\ncountries110m = fetch('https://unpkg.com/globe.gl/example/datasets/ne_110m_admin_0_countries.geojson').then(r => r.json())\n```\n**Logs**\n```\ncountriesMissingGeoJson = {\n const dataByCountry = indexBy(covidData, d => (countryNameMapper[d.country.toLowerCase()] || d.country).toLowerCase(), false);\n const geoJsonCountries = new Set(countries110m.features.map(d => d.properties.NAME.toLowerCase()));\n \n return Object.keys(dataByCountry).filter(country => !geoJsonCountries.has(country)).sort();\n}\n\ncountriesMissingData = {\n const dataByCountry = indexBy(covidData, d => (countryNameMapper[d.country.toLowerCase()] || d.country).toLowerCase(), false);\n const geoJsonCountries = new Set(countries110m.features.map(d => d.properties.NAME.toLowerCase()));\n \n return [...geoJsonCountries].filter(country => !dataByCountry.hasOwnProperty(country)).sort();\n}\n```\n**Dependencies**\n```\nGlobe = require('globe.gl@2.8.6')\n\nindexBy = require('index-array-by')\n\nd3 = require('d3')\n\nimport {radio, checkbox} from '@jashkenas/inputs'\n```" }, "Data-Wrangler (.omd)": { "type": ".omd", "content": "\n---\n```\nWrangler(data)\n``` \n---\n```\n// Function to wrangle data\nWrangler = (data = [], data2 = []) => {\n // Keep track of all the inputs\n let inputs = [];\n\n // Set up the three panels for the UI\n let wrapper = html`
`;\n let wrangler_container = html`
`;\n let right_panel = html`
Transformations Applied
`;\n let left_panel = html`
`;\n let bottom_panel = html`
`;\n wrangler_container.appendChild(left_panel);\n wrangler_container.appendChild(right_panel);\n wrapper.appendChild(wrangler_container);\n wrapper.appendChild(bottom_panel);\n\n // Function to add an operation (callback for the render_operations_panel)\n const addOperation = (operation) => {\n // Get current state of data and columns for building next input\n let data, cols;\n if (operation.includes(\"join\")) {\n data = [wrapper.value.data.objects(), data2];\n cols = data.map(get_cols);\n } else {\n data = wrapper.value.data.objects();\n cols = get_cols(data);\n }\n\n const id = id_generator();\n\n // Get default value\n const value = operations[operation].get_default_value\n ? operations[operation].get_default_value(cols, data)\n : [cols[0].label];\n\n const options = operation === \"filter\" ? { data } : {};\n inputs.push({ id, cols, value, options, type: operation });\n update_inputs();\n };\n\n const update_wrapper_value = (value) => {\n wrapper.value = value;\n wrapper.dispatchEvent(new Event(\"input\", { bubbles: true }));\n };\n\n // Update inputs panel\n const update_inputs = () => {\n right_panel.replaceChild(\n render_inputs_panel(inputs, update_code_and_results),\n right_panel.querySelector(\".inputs_wrapper\")\n );\n };\n\n // Update coee and results\n const update_code_and_results = (updated_inputs) => {\n inputs = updated_inputs;\n const expression = get_code_expression(inputs);\n right_panel.replaceChild(\n render_code_panel(expression),\n right_panel.querySelector(\".code_wrapper\")\n );\n\n bottom_panel.replaceChild(\n render_results_panel(expression, data, data2, update_wrapper_value),\n bottom_panel.querySelector(\".results_wrapper\")\n );\n };\n\n // Append the initial state\n left_panel.appendChild(\n html`${render_operations_panel(operations, addOperation)}`\n );\n right_panel.appendChild(\n html`${render_inputs_panel(inputs, update_code_and_results)}`\n );\n\n right_panel.appendChild(html`${render_code_panel(get_code_expression(inputs))}`);\n bottom_panel.appendChild(html`${render_results_panel(\"data\", data, data2)}`);\n\n wrapper.value = { data: aq.from(data), code: \"\" }; // initial value\n\n // Append styles\n wrapper.appendChild(make_styles());\n return wrapper;\n}\n\n// Render the panel displaying available operations, firing a callback function on click\nrender_operations_panel = (operations = [], callback = () => null) => {\n // Visual elements\n const wrapper = html`
`;\n const search = html`
`;\n\n // Search through operations\n search.oninput = () => { \n const value = search.op_search.value.toLowerCase();\n const filtered_operations = {};\n Object.keys(operations)\n .filter(\n (d) =>\n d.includes(value) ||\n operations[d].description?.includes(value) ||\n operations[d].type?.includes(value)\n )\n .forEach((d) => (filtered_operations[d] = operations[d]));\n left_panel.replaceChild(\n OperationsMenu(filtered_operations, callback),\n left_panel.querySelector(\".menu_wrapper\")\n ); \n };\n\n const left_panel = html`
\n
\n Data Wrangler\n \n ${make_docs_icon()}Docs \n \n
\n ${search}\n
\n
`;\n\n search.oninput();\n wrapper.appendChild(left_panel);\n return wrapper;\n}\n\n// Render the formatted code, including a button to copy it\nrender_code_panel = (expression = \"\") => {\n // Panel to contain the transformations and code\n const comment = `// To use copied code replace \"data\" with your own variable\\n`;\n\n const end = `\\n\\t// Call \".objects()\" to return an array of objects`;\n \n const wrapper = html`\n
\n
Code \n
\n Copy into a cell to save work\n ${copy_code_button(comment + expression + end)}\n
\n
\n ${format_code(comment + expression)}\n
`;\n\n return wrapper;\n}\n\nrender_results_panel = (expression, data, data2, callback = () => null) => {\n const wrapper = html`
Results
`;\n // Update the displayed data\n let wrangled_data;\n let err_message;\n try {\n wrangled_data = evaluate(\"return \" + expression, { data, aq, op, data2 });\n } catch (err) {\n err_message = display_error(err);\n }\n\n // Passing an error if there are no results\n if (!data.length) err_message = display_error({ message: \"No results\" });\n\n // Display the error or results\n let results;\n if(err_message) {\n results = err_message\n } else {\n results = Inputs.table(wrangled_data, { height: 150});\n // Set a minimum width to the table for mobile\n results.appendChild(html``)\n }\n \n wrapper.appendChild(results);\n\n // Use a callback to set the value of the parent\n callback({ data: wrangled_data, code: expression });\n return wrapper;\n}\n\n// Accepts an array of inputs ({id, type, cols, options})\nrender_inputs_panel = (inputs = [], callback = () => null) => {\n // Panel to contain the transformations and code\n const wrapper = html`
\n
`;\n\n const inputs_wrapper = html`
`;\n\n wrapper.append(inputs_wrapper);\n // Prompt to add operations\n const prompt = html`
Add transformations using the list aboveto the left.
`;\n\n const clear = html`
Clear all
`;\n clear.onclick = () => {\n inputs = [];\n callback(inputs);\n inputs_wrapper.innerHTML = \"\";\n inputs_wrapper.appendChild(prompt);\n };\n // Event for the form (when events are deleted or changed through the UI itself)\n inputs_wrapper.oninput = (trigger) => {\n // If there aren't any inputs, prompt the user to create inputs\n if (inputs?.length === 0) {\n inputs_wrapper.appendChild(prompt);\n inputs_wrapper.removeChild(clear);\n }\n // Update the inputs array if the event comes from dragging (reorders them)\n if (trigger?.type === \"drag\") inputs = trigger.inputs;\n // Otherwise, get id and value of form, update the inputs array\n else if (trigger.target?.form?.id) {\n const id = trigger.target.form.id;\n const value = trigger.target.form.value;\n const index = inputs.findIndex((d) => d.id === id);\n inputs[index].value = value;\n }\n // Run callback function (to pass information to parent)\n callback(inputs);\n };\n\n // Draw the initial state (prompt or inputs)\n if (inputs?.length) {\n render_inputs(inputs, inputs_wrapper);\n inputs_wrapper.appendChild(clear);\n } else inputs_wrapper.appendChild(prompt);\n\n return wrapper;\n}\n\n// Function to make draggable inputs inside of a wrapper\nrender_inputs = (input_arr, wrapper) => {\n function redraw(data) {\n d3.select(wrapper)\n .selectAll(\".item\")\n .data(data, (d) => d.id)\n .join(\"div\")\n .selectAll(\"span\")\n .style(\"transform\", `translate(0px, 0px)`);\n\n // Fire an event on the parent\n wrapper.oninput({ type: \"drag\", inputs: data });\n }\n\n function dragged(event, d) {\n const parent = this.parentNode; // wrapper span around the hamburger menu\n const nextSiblingY = parent.parentNode.nextElementSibling?.offsetTop;\n const y = parent.offsetTop;\n const prevSiblingY = parent.parentNode.previousElementSibling?.offsetTop;\n\n // Move all the child nodes\n d3.select(parent) // content\n .selectAll(function () {\n return parent.childNodes;\n })\n .style(\"transform\", `translate(0px, ${event.y - this.offsetTop - 10}px)`);\n\n // Shift up or down\n if (y + event.y > nextSiblingY || y + event.y < prevSiblingY) {\n const index = input_arr.findIndex((ele) => ele.id === d.id);\n const new_pos = y + event.y > nextSiblingY ? index + 1 : index - 1;\n input_arr.splice(new_pos, 0, input_arr.splice(index, 1)[0]);\n redraw(input_arr);\n }\n }\n\n function dragended(event, d) {\n redraw(input_arr);\n }\n\n const drag = d3.drag().on(\"drag\", dragged).on(\"end\", dragended);\n\n const divs = d3\n .select(wrapper)\n .selectAll(\"div\")\n .data(input_arr, (d) => d.id)\n .join(\n (enter) => {\n const divs = enter.append(\"div\").attr(\"class\", \"item\");\n\n // Content\n const content = divs\n .append(\"span\")\n .style(\"display\", \"inline-block\")\n .style(\"transform\", `translate(0px, 0px)`)\n .style(\"width\", \"100%\")\n .attr(\"class\", \"content\");\n\n // Hamburger icons\n content\n .append(\"span\")\n .style(\"vertical-align\", \"top\")\n .style(\"cursor\", \"pointer\")\n .style(\"display\", \"inline-block\")\n .call(drag)\n .each(function (d) {\n this.innerHTML = make_hamburger_icon();\n });\n\n // Remove icons\n content\n .append(\"span\")\n .attr(\"class\", \"remove\")\n .text(\"x\")\n .on(\"click\", (event, value) => {\n const index = input_arr.findIndex((ele) => ele.id === value.id);\n input_arr.splice(index, 1);\n redraw(input_arr);\n });\n\n // Craete the visual elements (forms)\n content.each(function (d) {\n this.appendChild(\n html`${operations[\n d.type\n ].make_input(d)}`\n );\n });\n\n return divs;\n },\n (update) => update,\n (exit) => exit.remove()\n );\n wrapper.dispatchEvent(new Event(\"input\", { bubbles: true }));\n}\n\n// Build the list of operations\nOperationsMenu = (\n operations = [\"semijoin\"],\n update = (d) => d,\n hoverUpdate = (d) => d\n) => {\n const icon_width = 85;\n const padding = 15;\n // const categories = Object.keys(operations)\n const categories = [\"core\", \"join\", \"reshape\", \"clean\", \"set\"];\n const wrapper = html`
`;\n categories.forEach((category) => {\n const ops = Object.keys(operations).filter(\n (d) => operations[d].type === category\n );\n let warning = \"\";\n if (category === \"join\") {\n warning = html`
To join, pass in a second dataset:
Wrangler(data, data2)
`;\n } else if (category === \"reshape\") {\n warning = html`
These functions can dramatically increase data size.`;\n } else if (category === \"set\") {\n warning = html`
For sets, pass in a second data set: Wrangler(data, data2)`;\n }\n const category_wrapper = html`
\n ${capitalize(\n category\n )} operations\n ${warning}
`;\n\n ops.forEach((d) => {\n const value = operations[d];\n const text = value.description;\n const control = html`
\n ${icons[d]}\n

\n ${d}\\n\n ${text}\n

\n `;\n control.onclick = () => update(d);\n \n category_wrapper.appendChild(control);\n });\n wrapper.appendChild(category_wrapper);\n });\n return wrapper;\n}\n\n// List of all operations, including how to build their input, get the syntax, icon, description\noperations = ({\n select: {\n make_input: (config) => SelectInput(config),\n description: \"choose columns\",\n type: \"core\",\n get_syntax: (d) => {\n if (!d.value) return \"\";\n // Wrap in quotes\n const quoted_values = d.value.map((d) => `'${d}'`).join(\",\");\n if (!quoted_values) return \"\";\n return `.select(${quoted_values})`;\n }\n },\n filter: {\n make_input: (config) => FilterInput(config),\n description: \"choose rows\",\n type: \"core\",\n get_default_value: (cols, data) => {\n const comparison_type = cols[0].type === \"categorical\" ? \"match\" : \"gt\";\n const starter_value =\n cols[0].type === \"categorical\"\n ? \"\"\n : d3.mean(data, (d) => +d[cols[0].label]);\n return [cols[0].label, comparison_type, starter_value];\n },\n get_syntax: (d) => {\n // Implement this logic better\n const [col, comparison, value] = d.value;\n if (value === \"\") return \"\";\n if (comparison.includes(\"equal\")) {\n const comparison_symbol = comparison === \"equal\" ? \"===\" : \"!==\";\n return `.filter(d => d[\"${col}\"] ${comparison_symbol} \"${value}\")`;\n } else if (comparison === \"match\") {\n return `.filter(d => op.match(d[\"${col}\"], \"${value}\"))`;\n } else if (comparison.endsWith(\"with\")) {\n return `.filter(d => op.${comparison}(d[\"${col}\"], \"${value}\"))`;\n } else if (comparison === \"lt\" || comparison === \"gt\") {\n const comparison_symbol = comparison === \"lt\" ? \"<\" : \">\";\n return `.filter(d => d[\"${col}\"] ${comparison_symbol} ${+value})`;\n }\n return \"\";\n }\n },\n orderby: {\n make_input: (config) => OrderbyInput(config),\n description: \"sort rows\",\n type: \"core\",\n get_syntax: (d) => {\n const [order_col, order] = d.value;\n if (order === \"Descending\") {\n return `.orderby(aq.desc(\"${order_col}\"))`;\n } else {\n return `.orderby(\"${order_col}\")`;\n }\n }\n },\n derive: {\n make_input: (config) => DeriveInput(config),\n description: \"add a new column\",\n type: \"core\",\n get_default_value: (cols) => [],\n get_syntax: (d) => {\n const [col_name, col_value] = d.value;\n if (!col_name || !col_value) return \"\";\n return `.derive({${col_name}: d => ${prefixVarsInFormula(col_value)}})`;\n }\n },\n rename: {\n make_input: (config) => RenameInput(config),\n description: \"change a column's name\",\n type: \"core\",\n get_syntax: (d) => {\n let [old_name, new_name] = d.value;\n if (!new_name) return \"\";\n if (old_name.includes(\" \")) old_name = `\"${old_name}\"`;\n return `.rename({${old_name}: \"${new_name}\"})`;\n }\n },\n groupby: {\n make_input: (config) => GroupbyInput(config),\n description: \"associate rows\",\n type: \"core\",\n get_syntax: (d) => {\n const quoted_groups = d.value.map((d) => `'${d}'`).join(\",\");\n return `.groupby(${quoted_groups})`;\n }\n },\n rollup: {\n make_input: (config) => RollupInput(config),\n description: \"compute column statistics\",\n type: \"core\",\n get_default_value: (cols) => [\n cols.filter((d) => d.type === \"continuous\")[0].label,\n [\"mean\", \"count\"]\n ],\n get_syntax: (d) => {\n const [summary_col, metrics] = d.value;\n if (!metrics.length) return \"\";\n const str = metrics.map((d, i) => {\n const end = i === metrics.length - 1 ? \"\" : \",\\n\\t\\t\";\n const arg = d === \"count\" ? `` : `d[\"${summary_col}\"]`;\n return `${d}:d => op.${d}(${arg})${end}`;\n });\n return `.rollup({${str.join(\"\\t\")}})`;\n }\n },\n count: {\n make_input: (config) => CountInput(config),\n description: \"count rows\",\n type: \"core\",\n get_syntax: () => `.count()`\n },\n sample: {\n make_input: (config) => SampleInput(config),\n description: \"retrieve random rows\",\n type: \"core\",\n get_default_value: () => [10, \"\"],\n get_syntax: (d) => {\n const [n_sample, replacement] = d.value;\n const end = replacement === \"replacement\" ? \", {replace:true})\" : \")\";\n return `.sample(${n_sample}${end}`;\n }\n },\n slice: {\n make_input: (config) => SliceInput(config),\n description: \"retrieve rows by index\",\n type: \"core\",\n get_default_value: () => [0, 10],\n get_syntax: (d) => {\n const [lower, upper] = d.value;\n return `.slice(${lower}, ${upper})`;\n }\n },\n\n relocate: {\n make_input: (config) => RelocateInput(config),\n description: \"move a column\",\n type: \"core\",\n get_default_value: (cols) => [cols[0].label, \"after\", cols[1].label],\n get_syntax: (d) => {\n const [selected, position, reference] = d.value;\n return `.relocate(\"${selected}\", {${position}:\"${reference}\"})`;\n }\n },\n reify: {\n make_input: (config) => ReifyInput(config),\n description: \"materialize the table\",\n type: \"core\",\n get_syntax: () => `.reify()`\n },\n ungroup: {\n make_input: (config) => UngroupInput(config),\n description: \"remove groupings from data\",\n type: \"core\",\n get_syntax: () => `.ungroup()`\n },\n unorder: {\n make_input: (config) => UnorderInput(config),\n description: \"remove orderings from data\",\n type: \"core\",\n get_syntax: () => `.unorder()`\n },\n join: {\n make_input: (config) => JoinInput(config),\n description: \"join two tables\",\n type: \"join\",\n get_default_value: (cols) => [cols[0][0]?.label, cols[1][0]?.label],\n get_syntax: (d) => {\n const [left_col, right_col] = d.value;\n return `.${d.type}(aq.from(data2), [\"${left_col}\", \"${right_col}\"])`;\n }\n },\n join_left: {\n make_input: (config) => JoinInput(config),\n description: \"left join two tables\",\n type: \"join\",\n get_default_value: (cols) => [cols[0][0]?.label, cols[1][0]?.label],\n get_syntax: (d) => {\n const [left_col, right_col] = d.value;\n return `.${d.type}(aq.from(data2), [\"${left_col}\", \"${right_col}\"])`;\n }\n },\n join_right: {\n make_input: (config) => JoinInput(config),\n description: \"right join two tables\",\n type: \"join\",\n get_default_value: (cols) => [cols[0][0]?.label, cols[1][0]?.label],\n get_syntax: (d) => {\n const [left_col, right_col] = d.value;\n return `.${d.type}(aq.from(data2), [\"${left_col}\", \"${right_col}\"])`;\n }\n },\n join_full: {\n make_input: (config) => JoinInput(config),\n description: \"full join two tables\",\n type: \"join\",\n get_default_value: (cols) => [cols[0][0]?.label, cols[1][0]?.label],\n get_syntax: (d) => {\n const [left_col, right_col] = d.value;\n return `.${d.type}(aq.from(data2), [\"${left_col}\", \"${right_col}\"])`;\n }\n },\n semijoin: {\n make_input: (config) => JoinInput(config),\n description: \"return matching left rows\",\n type: \"join\",\n get_default_value: (cols) => [cols[0][0]?.label, cols[1][0]?.label],\n get_syntax: (d) => {\n const [left_col, right_col] = d.value;\n return `.${d.type}(aq.from(data2), [\"${left_col}\", \"${right_col}\"])`;\n }\n },\n antijoin: {\n make_input: (config) => JoinInput(config),\n description: \"return rows not in right table\",\n type: \"join\",\n get_default_value: (cols) => [cols[0][0]?.label, cols[1][0]?.label],\n get_syntax: (d) => {\n const [left_col, right_col] = d.value;\n return `.${d.type}(aq.from(data2), [\"${left_col}\", \"${right_col}\"])`;\n }\n },\n fold: {\n make_input: (config) => FoldInput(config),\n description: \"transform into key-value pairs\",\n type: \"reshape\",\n get_syntax: (d) => {\n const fold_cols = d.value;\n // Wrap in quotes\n const fold_quoted = d.value.map((d) => `'${d}'`).join(\",\");\n if (!fold_quoted) return;\n return d.value.length > 1\n ? `.fold([${fold_quoted}])`\n : `.fold(${fold_quoted})`;\n }\n },\n\n pivot: {\n make_input: (config) => PivotInput(config),\n description: \"create new columns for keys\",\n type: \"reshape\",\n get_syntax: (d) => {\n const [key_col, value_col] = d.value;\n return `.pivot(\"${key_col}\", \"${value_col}\")`;\n }\n },\n spread: {\n make_input: (config) => SpreadInput(config),\n description: \"separate column of arrays\",\n type: \"reshape\",\n get_syntax: (d) => `.spread(\"${d.value}\")`\n },\n unroll: {\n make_input: (config) => UnrollInput(config),\n description: \"separate array column (rows)\",\n type: \"reshape\",\n get_syntax: (d) => `.unroll(\"${d.value}\")`\n },\n dedupe: {\n make_input: (config) => DedupeInput(config),\n description: \"remove duplicate rows\",\n type: \"clean\",\n get_syntax: (d) => {\n if (!d.value) return \"\";\n // Wrap in quotes\n const deduplicate_values = d.value.map((d) => `'${d}'`).join(\",\");\n return `.dedupe(${deduplicate_values})`;\n }\n },\n impute: {\n make_input: (config) => ImputeInput(config),\n description: \"fill in missing values\",\n type: \"clean\",\n get_default_value: (cols) => [\n cols.filter((d) => d.type === \"continuous\")[0].label,\n \"mean\"\n ],\n get_syntax: (d) => {\n const [impute_col, operation] = d.value;\n const [quoted_name, col_ref] = impute_col.includes(\" \")\n ? [`\"${impute_col}\"`, `[\"${impute_col}\"]`]\n : [impute_col, `.${impute_col}`];\n return `.impute({${quoted_name}: d => op.${operation}(d${col_ref})})`;\n }\n },\n\n concat: {\n make_input: (config) => SetInput(config),\n description: \"append a second table\",\n type: \"set\",\n get_syntax: (d) => `.${d.type}(aq.from(data2))`\n },\n\n union: {\n make_input: (config) => SetInput(config),\n description: \"concat and remove duplicates\",\n type: \"set\",\n get_syntax: (d) => `.${d.type}(aq.from(data2))`\n },\n\n intersect: {\n make_input: (config) => SetInput(config),\n description: \"keep only matching rows\",\n type: \"set\",\n get_syntax: (d) => `.${d.type}(aq.from(data2))`\n },\n except: {\n make_input: (config) => SetInput(config),\n description: \"Compute table set difference\",\n type: \"set\",\n get_syntax: (d) => `.${d.type}(aq.from(data2))`\n }\n})\n```\n\n```\n// Input to create a `Slice` operation\nSliceInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const lower = html``;\n const upper = html``;\n const form = html`
Slice: Retrieve rows between\n ${lower}\n and\n ${upper}\n
`;\n form.oninput = (event) => {\n // Update the value\n form.value = [form.lower.value, form.upper.value];\n };\n form.oninput({});\n return form;\n}\n\n// Input to create a `filter` operation -- certainly the most complex input\nFilterInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n // Get column types\n let col_data = cols;\n\n if (col_data.length === 0)\n return html`
filter not possible: there are no categorical or continuous columns present
`;\n\n const type_map = new Map(col_data.map((d) => [d.label, d.type]));\n\n const select = make_select(\n \"filter_col\",\n col_data.map((d) => d.label),\n value[0]\n );\n\n // Comparison operation (e.g., equal, not equal, etc.)\n const comparisonOptions = {\n categorical: [\n { label: \"matches\", value: \"match\" },\n { label: \"starts with\", value: \"startswith\" },\n { label: \"ends with\", value: \"endswith\" },\n { label: \"is equal to\", value: \"equal\" },\n { label: \"is *not* equal to\", value: \"not_equal\" }\n ],\n continuous: [\n { label: \"is less than\", value: \"lt\" },\n { label: \"is greater than\", value: \"gt\" }\n ],\n date: [\n { label: \"is less than\", value: \"lt\" },\n { label: \"is greater than\", value: \"gt\" }\n ]\n };\n\n // Element for selecting comparison type\n const buildComparison = (type) => {\n return make_select(\"comparison\", comparisonOptions[type], value[1]);\n };\n\n const comparison = buildComparison(type_map.get(value[0]));\n\n const buildComparisonValue = (col) => {\n const type = type_map.get(col);\n let ele;\n switch (type) {\n case \"categorical\":\n const inputVal = typeof value[2] === \"string\" ? value[2] : \"\"\n ele = html``;\n break;\n case \"date\":\n case \"continuous\":\n const min = d3.min(options.data, (d) => +d[col]);\n const max = d3.max(options.data, (d) => +d[col]);\n ele = html``;\n break;\n }\n return ele;\n };\n const comparisonValue = buildComparisonValue(value[0]);\n const filterForm = html`
Filter rows:\n ${select}\n ${comparison}\n ${comparisonValue}\n
`;\n\n filterForm.oninput = (event) => {\n if (event.target && event.target.name === \"filter_col\") {\n // Rebuild the other options\n const new_col = filterForm.filter_col.value;\n const type = type_map.get(new_col);\n const new_comparison = buildComparison(type);\n const new_comparison_value = buildComparisonValue(new_col);\n filterForm.comparison.parentNode.replaceChild(\n new_comparison,\n filterForm.comparison\n );\n filterForm.comparisonValue.parentNode.replaceChild(\n new_comparison_value,\n filterForm.comparisonValue\n );\n }\n\n // Update the value\n filterForm.value = [\n filterForm.filter_col.value,\n filterForm.comparison.value,\n filterForm.comparisonValue.value\n ];\n };\n filterForm.oninput({});\n return filterForm;\n}\n\n// Input to create a `Sample` operation\nSampleInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const ele = html``;\n const order_value = value[1] ? \"With Replacement\" : \"\";\n const order = make_checkboxes(\n \"replacement\",\n [\"With Replacement\"],\n [order_value]\n );\n const form = html`
Sample Rows:\n ${ele}\n ${order}\n
`;\n form.oninput = (event) => {\n const replacement = form.replacement.checked === true ? \"replacement\" : \"\";\n // Update the value \n form.value = [form.n_sample.value, replacement]; \n };\n form.oninput({});\n return form;\n}\n\n// Input to create a `derive` operation\nDeriveInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const ele = html`is defined as`;\n\n const form = html`
Derive:\n ${ele}\n
`;\n form.oninput = (event) => {\n // Update the value\n form.value = [form.col_name.value, form.col_value.value];\n };\n form.oninput({});\n return form;\n}\n\n// Input to create a `fold` operation\nFoldInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const labels = cols.map((d) => d.label);\n const checkboxes = make_checkboxes(\"select\", labels, value);\n const form = html`
Fold columns:\n${checkboxes}\n
`;\n form.oninput = () =>\n (form.value = Object.keys(form.select)\n .filter((d) => form.select[d].checked === true)\n .map((d) => form.select[d].value));\n\n form.oninput();\n return form;\n}\n\n// Input to create a `join` operation\nJoinInput = ({\n id = \"\",\n type = \"\",\n cols = [],\n value = [],\n options = {}\n} = {}) => {\n const left_labels = cols[0].map((d) => d.label);\n const left_select = make_select(\"left_col\", left_labels, value[0]);\n const right_labels = cols[1].map((d) => d.label);\n const right_select = make_select(\"right_col\", right_labels, value[1]);\n\n // Join data input\n const label =\n type.includes(\"semi\") || type.includes(\"anti\")\n ? `${capitalize(type.replace(\"join\", \"\"))} Join`\n : type.includes(\"join_\")\n ? `Join ${type.replace(\"join_\", \"\")}`\n : \"Join\";\n const form = html`
${label}\n data2\n on:\n left column ${left_select} matches right column ${right_select}\n
`;\n\n // Event handling\n form.oninput = () => {\n form.value = [form.left_col.value, form.right_col.value];\n };\n form.oninput();\n return form;\n}\n\nSpreadInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const categories = cols\n .filter((d) => d.type === \"categorical\")\n .map((d) => d.label);\n const col_input = make_select(\"col\", categories, value);\n const form = html`
Spread: ${col_input}
`;\n\n form.oninput = () => {\n form.value = form.col.value;\n };\n\n form.oninput();\n return form;\n}\n\nUnrollInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const categories = cols\n .filter((d) => d.type === \"categorical\")\n .map((d) => d.label);\n const col_input = make_select(\"col\", categories);\n\n const form = html`
Unroll: ${col_input}
`;\n\n form.oninput = () => {\n form.value = form.col.value;\n };\n\n form.oninput();\n return form;\n}\n\nPivotInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const categories = cols\n .filter((d) => d.type === \"categorical\" || d.type === \"date\")\n .map((d) => d.label);\n const key_input = make_select(\"key_col\", categories, value[0]);\n const value_input = make_select(\n \"value_col\",\n cols.map((d) => d.label),\n value[1]\n );\n const form = html`
Pivot: ${key_input} ${value_input}
`;\n\n form.oninput = () => {\n form.value = [form.key_col.value, form.value_col.value];\n };\n form.oninput();\n return form;\n}\n\nImputeInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const col_input = make_select(\n \"col\",\n cols.filter((d) => d.type === \"continuous\").map((d) => d.label),\n value[0]\n );\n const operations = [\"max\", \"mean\", \"median\", \"min\", \"mode\"];\n const ele = make_select(\"operation\", operations, value[1]);\n const form = html`
Impute:${col_input} using the column ${ele}\n
`;\n form.oninput = () => {\n form.value = [form.col.value, form.operation.value];\n };\n form.oninput();\n return form;\n}\n\n// Input to create an `rename` operation\nRenameInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const labels = cols.map((d) => d.label);\n const select = make_select(\"col\", labels, [value[0]]);\n const new_label = html``;\n const form = html`
Rename:\n${select}\nto \n${new_label}\n
`;\n form.oninput = () => {\n form.value = [form.col.value, form.new_label.value];\n };\n form.oninput();\n return form;\n}\n\n// Input to create an `relocate` operation\nRelocateInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const labels = cols.map((d) => d.label);\n const selected = make_select(\"selected\", labels, value[0]);\n const position = make_select(\"position\", [\"after\", \"before\"], value[1]);\n const reference = make_select(\"reference\", labels, value[2]);\n const new_label = html``;\n const form = html`
Relocate:\n${selected}\n${position}\n${reference}\n
`;\n form.oninput = () => {\n form.value = [\n form.selected.value,\n form.position.value,\n form.reference.value\n ];\n };\n form.oninput();\n return form;\n}\n\n// Input to create an `orderby` operation\nOrderbyInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const labels = cols.map((d) => d.label);\n const select = make_select(\"col\", labels, value[0]);\n const order = make_checkboxes(\"order\", [\"Descending\"], [value[1]]);\n const form = html`
Order by (sort):\n${select}\n${order}\n
`;\n form.oninput = () => {\n const order = form.order.checked === true ? \"Descending\" : \"ascending\";\n form.value = [form.col.value, order];\n };\n form.oninput();\n return form;\n}\n\n// Input to create a `Rollup` operation\nRollupInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const summarizers = cols\n .filter((d) => d.type === \"continuous\")\n .map((d) => d.label);\n const defaults = value[1] || [\"count\", \"mean\"];\n const select =\n cols.length === 0 ? \"\" : make_select(\"col\", summarizers, [value[0]]);\n const summarize_options =\n summarizers.length === 0\n ? [\"count\"]\n : [\"min\", \"max\", \"mean\", \"median\", \"sum\", \"stdev\", \"count\"];\n const metrics = make_checkboxes(\"metric\", summarize_options, defaults);\n\n const form = html`
Rollup:\n${select}\n${metrics}\n
`;\n form.oninput = () => {\n const checked = !Object.keys(form.metric).length\n ? form.metric.checked\n ? [form.metric.value]\n : \"\"\n : Object.keys(form.metric)\n .filter((d) => form.metric[d].checked === true)\n .map((d) => form.metric[d].value);\n\n const value = cols.length === 0 ? \"\" : form.col.value;\n form.value = [value, checked];\n };\n form.oninput();\n return form;\n}\n\n// Input to create an `groupby` operation\nGroupbyInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const groups = cols\n .filter((d) => d.type === \"categorical\" || d.type === \"date\")\n .map((d) => d.label);\n if (groups.length === 0)\n return html`
groupby not possible: there are no categorical or date columns present
`;\n\n const checkboxes = make_checkboxes(\"groups\", groups, value);\n const form = html`
Group by:\n${checkboxes}\n
`;\n form.oninput = () => {\n if (!form.groups) return;\n const checked = !Object.keys(form.groups).length\n ? form.groups.checked\n ? [form.groups.value]\n : \"\"\n : Object.keys(form.groups)\n .filter((d) => form.groups[d].checked === true)\n .map((d) => form.groups[d].value);\n form.value = checked;\n };\n form.oninput();\n return form;\n}\n\n// Input to create an `ungroup` operation\nUngroupInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const form = html`
Ungroup data ✓
`;\n return form;\n}\n\n// Input to create an `reify` operation\nReifyInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const form = html`
Reify Table ✓
`;\n return form;\n}\n\n// Input to create any `set` operation\nSetInput = ({\n id = \"\",\n type = \"\",\n cols = [],\n value = [],\n options = {}\n} = {}) => {\n const form = html`
${capitalize(\n type\n )} Table data2
`;\n return form;\n}\n\n// Input to create an `count` operation\nCountInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const form = html`
Count Rows ✓
`;\n return form;\n}\n\n// Input to create an `ungroup` operation\nUnorderInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const form = html`
Unorder data ✓
`;\n return form;\n}\n\n// Input to create a `select` operation\nSelectInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const labels = cols.map((d) => d.label);\n const default_cols = value.length === 0 ? [labels[0]] : value;\n const checkboxes = make_checkboxes(\"select\", labels, default_cols);\n const form = html`
Select columns:\n ${checkboxes}\n
`;\n form.oninput = () =>\n (form.value = Object.keys(form.select)\n .filter((d) => form.select[d].checked === true)\n .map((d) => form.select[d].value));\n\n form.oninput();\n return form;\n}\n\n// Input to create a `deduplicate` operation\nDedupeInput = ({ id = \"\", cols = [], value = [], options = {} } = {}) => {\n const labels = cols.map((d) => d.label);\n const checkboxes = make_checkboxes(\"select\", labels, value);\n const form = html`
Remove Duplicates:\n${checkboxes}\n
`;\n form.oninput = () =>\n (form.value = Object.keys(form.select)\n .filter((d) => form.select[d].checked === true)\n .map((d) => form.select[d].value));\n\n form.oninput();\n return form;\n}\n```\n\n```\n// Error to display if operations are dragged into an unexecutable state\ndisplay_error = (err) => {\n let message = \"\";\n switch (true) {\n case err.message.includes(\"column\"):\n message =\n \"Your code statement is no longer valid, as it references a column that doesn't exist. This is often the result of reordering your operations, or changing a select statement. Undoing your previous operation should resolve the issue.\";\n break;\n case err.message.includes(\"Unexpected token\"):\n message =\n \"Your code statement is not valid (yet). You may simply need to fill in the options above.\";\n break;\n case err.message.includes(\"results\"):\n message =\n \"Make sure to pass an array of objects to the Wrangler() function.\";\n break;\n default:\n message = \"You encountered an error: \";\n break;\n }\n\n const styles = `\n background-color:#caeaf985;\n padding: 10px;\n border-radius:5px;\n `;\n\n return html`
${message}\n
     ${err.message}\n  
\n
`;\n}\n\n// Accepts an array of inputs ({id, type, element}) where the element is an HTML input\n// Returns the code expression captured by the input\nget_code_expression = (inputs) => {\n let expression = `aq.from(data)`;\n if (!inputs?.length) return expression;\n inputs.map((d, i) => {\n const next_line = operations[d.type].get_syntax(d);\n if (!next_line) return;\n expression += \"\\n\\t\" + operations[d.type].get_syntax(d);\n });\n return expression;\n}\n\nmake_select = (name = \"select\", options = [\"a\", \"b\"], selected = \"\") => {\n return html``;\n}\n\nmake_checkboxes = (name = \"boxes\", options = [\"a\", \"b\"], selected = []) => {\n return html`${options.map((d) => {\n return ``;\n })}\n`;\n}\n\nget_cols = (data) =>\n Object.keys(data[0] || {}).map((d) => {\n return {\n label: d,\n type: getType(data, d)\n };\n })\n\ncopy_code_button = (code_str) => {\n return html`
${Copier(\n html`${make_copy_icon()}Copy`,\n { value: code_str }\n )}
`;\n}\n\n// Function to get column types\ngetType = (data, column) => {\n for (const d of data) {\n const value = d[column];\n if (value == null) continue;\n if (typeof value === \"number\") return \"continuous\";\n if (value instanceof Date) return \"date\";\n return \"categorical\";\n }\n}\n\n// see unit tests in https://observablehq.com/@fil/prefixvarsinformula\nfunction prefixVarsInFormula(str, arg = \"d\", ignore = [\"aq\", \"op\"]) {\n let shift = 0;\n const insert = `${arg}.`;\n for (const { type, start, end, name } of P.parseCell(str).references) {\n if (type === \"Identifier\" && name !== arg && !ignore.includes(name)) {\n str =\n str.slice(0, start + shift) + insert + name + str.slice(end + shift);\n shift += insert.length;\n }\n }\n return str;\n}\n\n// A function to evaluate a string, given an object\nevaluate = (s, obj) =>\n new Function(...Object.keys(obj), s)(...Object.values(obj))\n\n// To generate a unique ID for each input element (probably a better way to do this)\nid_generator = () => {\n var S4 = function () {\n return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);\n };\n return \"a\" + S4() + S4();\n}\n\nformat_code = (code_str) => {\n return html`
${md`~~~js\n${code_str}\n~~~`}`;\n}\n\ncapitalize = (string) => {\n return string.charAt(0).toUpperCase() + string.slice(1);\n}\n```\n\n```\nmake_styles = () => html``\n```\n\n```\nmake_hamburger_icon = () => {\n return `\n\n\n\n`;\n}\n\nmake_docs_icon = () => svg`\n\n\n\n`\n\nmake_copy_icon = () => svg`\n\n\n\n\n\n`\n\nicons = FileAttachment(/* \"icons@4.json\" */\"https://static.observableusercontent.com/files/3c2fd71a8faeeebda50f3647621d658fd2943e8e9c4661ae86f05309b75b5569b1a6731a3058b51726f88675fa46e9b3b57be9ec5e411fd76bb415c3d7730823\").json()\n```\n\n```\nimport { Copier } from \"@mbostock/copier\"\n\n// Load the arquero function into the notebook\nimport { aq, op } from \"@uwdata/arquero\"\n\nimport {dataInput} from \"@john-guerra/file-input-with-default-value\"\n\n// https://github.com/observablehq/parser\nP = require(\"@observablehq/parser@4.4.4\")\n```\n\n```\n// Load the data (e.g., a .csv file)\ndata = await FileAttachment(\"https://static.observableusercontent.com/files/100ba6c9ab25d7a4687eb6bc46809219508a9faf2b7ade6c7034b0490cc7709fcfca7a50f4b76fda051310f6a4898815c53a702418c3d8644f767e3a6e44ac7e\").csv({ typed: true });\n\n```\n" }, "ECL Playground (.omd)": { "type": ".omd", "content": "# ECL Playground\n\n_A [@hpcc-js/observable-md](https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observable-md) demo - these demos are currently a work in progress and have dependencies which may or may not exist at any given time..._\n\n## 1 - An ECL Editor\n\n```\nviewof eclEditor = editor.ecl();\nviewof eclEditor.text(`\\\n\nLayout_Person := RECORD\n UNSIGNED1 PersonID;\n STRING15 FirstName;\n STRING25 LastName;\nEND;\n\nallPeople := DATASET([ {1,'Fred','Smith'},\n {2,'Joe','Blow'},\n {3,'Jane','Smith'}],Layout_Person);\n\nsomePeople := allPeople(LastName = 'Smith');\n\n// Outputs ---\nsomePeople;\n`)\n```\n\n## 2 - A Submit Button\n\n```\nviewof eclToSubmit = {\n const button = html``;\n button.onclick = () => {\n button.value = eclEditor;\n button.dispatchEvent(new CustomEvent(\"input\"));\n }\n return button;\n}\n```\n\n## 3 - A HPCC Platform\n\n* **Platform**: ${platform.url} \n\n```\nplatform = esp(\"https://play.hpccsystems.com:18010\");\nresults = platform.submit(eclToSubmit);\n```\n\n## 4 - Render results in a Table\n\n```\nviewof resultsTable = table({height:140});\nviewof resultsTable.json(results);\n``` \n" }, "Five-Minute Introduction (.omd)": { "type": ".omd", "content": "# Five-Minute Introduction\n\n_A [@hpcc-js/observable-md](https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observable-md) demo - these demos are currently a work in progress and have dependencies which may or may not exist at any given time..._\n\n---\n↑ This document is best viewed with the \"Show Developer Info\" enabled above ↑\n\n---\n\nWelcome! This notebook gives a quick overview of \"Observable Markdown\" a mashup of the excellent [Observable HQ](https://observablehq.com) + regular Markdown. Here follows a quick introduction to Observable. For a more technical introduction, see [Observable’s not JavaScript](/@observablehq/observables-not-javascript). For hands-on, see our [introductory tutorial series](/collection/@observablehq/introduction). To watch rather than read, see our [short introductory video](https://www.youtube.com/watch?v=uEmDwflQ3xE)!\n\nIts also very easy to embed a value: **${i}** inside the Markdown!!!\n\nObservable Markdown consists of a single markdown document with live \"code\" sections.\n\n```\n2 * 3 * 7\n{\n let sum = 0;\n for (let i = 0; i <= 100; ++i) {\n sum += i;\n }\n return sum;\n}\n```\n\nCells can have names. This allows a cell’s value to be referenced by other cells.\n\n```\ncolor = \"red\";\n`My favorite color is ${color}.`\n```\n\nA cell referencing another cell is re-evaluated automatically when the referenced value changes. Try editing the definition of color above and shift-return to re-evaluate.\n\nCells can generate DOM (HTML, SVG, Canvas, WebGL, etc.). You can use the standard DOM API like document.createElement, or use the built-in html tagged template literal:\n\n```\nhtml`\n My favorite language is HTML.\n`\n```\n\nThere’s a Markdown tagged template literal, too. (This notebook is written in Markdown.)\n\n```\nmd`My favorite language is *Markdown*.`\n```\n\nDOM can be made reactive simply by referring to other cells. The next cell refers to color. (Try editing the definition of color above.)\n\n```\nhtml`My favorite color is ${color}.`\n```\n\nSometimes you need to load data from a remote server, or compute something expensive in a web worker. For that, cells can be defined asynchronously using [promises](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Using_promises):\n\n```\nstatus = new Promise(resolve => {\n setTimeout(() => {\n resolve({resolved: new Date});\n }, 2000);\n})\n```\n\nA cell that refers to a promise cell sees the value when it is resolved; this implicit await means that referencing cells don’t care whether the value is synchronous or not. Edit the status cell above to see the cell below update after two seconds.\n\n```\nstatus\n```\n\nPromises are also useful for loading libraries from npm. Below, require returns a promise that resolves to the d3-fetch library:\n\n```\nd3 = require(\"d3-fetch@1\")\n```\n\nIf you prefer, you can use async and await explicitly (not this ):\n\n```\ncountries = (await d3.tsv(\"https://cdn.jsdelivr.net/npm/world-atlas@1/world/110m.tsv\"))\n .sort((a, b) => b.pop_est - a.pop_est) // Sort by descending estimated population.\n .slice(0, 10) // Take the top ten.\n```\n\nCells can be defined as [generators](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Iterators_and_Generators#Generators); a value is yielded up to sixty times a second.\n\n```\ni = {\n let i = 0;\n while (true) {\n yield ++i;\n }\n}\n`The current value of i is ${i}.`\n```\n\nAny cell that refers to a generator cell sees its current value; the referencing cell is re-evaluated whenever the generator yields a new value. As you might guess, a generator can yield promises for [async iteration](https://github.com/tc39/proposal-async-iteration); referencing cells see the current resolved value.\n\n```\ndate = {\n while (true) {\n yield new Promise(resolve => {\n setTimeout(() => resolve(new Date), 1000);\n });\n }\n}\n```\n\nCombining these primitives—promises, generators and DOM—you can build custom user interfaces. Here’s a slider and a generator that yields the slider’s value:\n\n```\nslider = html``\nsliderValue = Generators.input(slider)\n```\n\nGenerators.input returns a generator that yields promises. The promise resolves whenever the associated input element emits an input event. You don’t need to implement that generator by hand, though. There’s a builtin viewof operator which exposes the current value of a given input element:\n\n```\nviewof value = html``\nvalue\n```\n\nYou can import cells from other notebooks. To demonstrate how custom user interfaces can expose arbitrary values to other cells, here’s a brushable scatterplot of cars showing the inverse relationship between horsepower and fuel efficiency.\n\n```\nimport {viewof selection as cars} from \"@d3/brushable-scatterplot\";\nviewof cars;\ncars\n```\n" }, "Hello World (.omd)": { "type": ".omd", "content": "# Hello World - ${mol}!\n\n_A [@hpcc-js/observable-md](https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observable-md) demo - these demos are currently a work in progress and have dependencies which may or may not exist at any given time..._\n\n* - Render Page (`ctrl+s`).\n* - Download as HTML.\n* - Show Developer Info - _the live code values_.\n* Samples Drop Down - _expect frequent updates, while the plugins get updated with new functionality_\n* - Github Repository.\n\nVery Quick Start:\n1. Edit the Markdown on the left\n2. Click the \"Render\" button (or press `Ctrl+S`).\n3. Explore the samples in the drop down (top right).\n\n```\nmol = 6 * 7; // mol - get it?\n```\n" }, "Import (.omd)": { "type": ".omd", "content": "# Imports\n\n_A [@hpcc-js/observable-md](https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observable-md) demo - these demos are currently a work in progress and have dependencies which may or may not exist at any given time..._\n\nLibraries can be imported directly from [ObservableHQ](https://observablehq.com/)\n\n```\nimport {viewof selection as cars} from \"@d3/brushable-scatterplot\"\nviewof cars\nsel = JSON.stringify(cars, undefined, 2)\n```\n### Selection:\n```json\n${sel}\n```\n" }, "Inline Editor Demo (.omd)": { "type": ".omd", "content": "# Inline Editor Demo\n\n_A [@hpcc-js/observable-md](https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observable-md) demo - these demos are currently a work in progress and have dependencies which may or may not exist at any given time..._\n\n* Full list of editors:\n${mdFormatted}\n\n```\nmdFormatted = Object.keys(editor).map(k => \" * \" + k).join(\"\\n\");\n```\n\n---\n\n## ECL Editor\n_Creating widgets is always broken into two steps, this allows the content of the widget to be modified, without re-rendering the entire widget_\n\n```javascript\n// First Create ECL Editor instance\nviewof eclEditor = editor.ecl();\n\n// Next assign some text:\nviewof eclEditor.text(`a := 'aaa';`)\n\n// The current content will always be available in \"eclEditor\"\neclEditor;\n\n```\n\n```\n// First Create ECL Editor instance\nviewof eclEditor = editor.ecl();\n\n// Next assign some text:\nviewof eclEditor.text(`a := 'aaa';\\n// TYPE HERE NOW!!!`)\n\n// The current content will always be available in \"eclEditor\"\neclEditor;\n```\nTotal characters Entered = ${eclEditor.length}.\n\n...and the actual text:\n\n```ecl\n${eclEditor}\n```\n\n---\n\n## JavaScript\n_A Javascript Editor_\n\n```\nfunction add(a, b) {\n return a + b;\n}\nviewof js = editor.javascript();\nviewof js.text(`function add(a, b) {\n return a + b;\n}\n\nadd(40, 2);\n`);\n```\n\n" }, "Markdown Quick Reference (.omd)": { "type": ".omd", "content": "Markdown Quick Reference\n========================\n\n_A [@hpcc-js/observable-md](https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observable-md) demo - these demos are currently a work in progress and have dependencies which may or may not exist at any given time..._\n\nThis guide is a very brief overview, with examples, of the syntax that [Markdown] supports. It is itself written in Markdown and you can copy the samples over to the left-hand pane for experimentation. It's shown as *text* and not *rendered HTML*.\n\n[Markdown]: http://daringfireball.net/projects/markdown/\n\n\nSimple Text Formatting\n======================\n\nFirst thing is first. You can use *stars* or _underscores_ for italics. **Double stars** and __double underscores__ do bold. ***Three together*** do ___both___.\n\nParagraphs are pretty easy too. Just have a blank line between chunks of text.\n\n> This chunk of text is in a block quote. Its multiple lines will all be\n> indended a bit from the rest of the text.\n>\n> > Multiple levels of block quotes also work.\n\nSometimes you want to include some code, such as when you are explaining how `

` HTML tags work, or maybe you are a programmer and you are discussing `someMethod()`.\n\nIf you want to include some code and have\nnewlines preserved, indent the line with a tab\nor at least four spaces.\n Extra spaces work here too.\nThis is also called preformatted text and it is useful for showing examples.\nThe text will stay as text, so any *markdown* or HTML you add will\nnot show up formatted. This way you can show markdown examples in a\nmarkdown document.\n\n> You can also use preformatted text with your blockquotes\n> as long as you add at least five spaces.\n\n\nHeadings\n========\n\nThere are a couple of ways to make headings. Using three or more equals signs on a line under a heading makes it into an \"h1\" style. Three or more hyphens under a line makes it \"h2\" (slightly smaller). You can also use multiple pound symbols before and after a heading. Pounds after the title are ignored. Here's some examples:\n\nThis is H1\n==========\n\nThis is H2\n----------\n\n# This is H1\n## This is H2\n### This is H3 with some extra pounds ###\n#### You get the idea ####\n##### I don't need extra pounds at the end\n###### H6 is the max\n\n\nLinks\n=====\n\nLet's link to a few sites. First, let's use the bare URL, like . Great for text, but ugly for HTML.\nNext is an inline link to [Google](http://www.google.com). A little nicer.\nThis is a reference-style link to [Wikipedia] [1].\nLastly, here's a pretty link to [Yahoo]. The reference-style and pretty links both automatically use the links defined below, but they could be defined *anywhere* in the markdown and are removed from the HTML. The names are also case insensitive, so you can use [YaHoO] and have it link properly.\n\n[1]: http://www.wikipedia.org/\n[Yahoo]: http://www.yahoo.com/\n\nTitle attributes may be added to links by adding text after a link.\nThis is the [inline link](http://www.bing.com \"Bing\") with a \"Bing\" title.\nYou can also go to [W3C] [2] and maybe visit a [friend].\n\n[2]: http://w3c.org (The W3C puts out specs for web-based things)\n[Friend]: http://facebook.com/ \"Facebook!\"\n\nEmail addresses in plain text are not linked: test@example.com.\nEmail addresses wrapped in angle brackets are linked: .\nThey are also obfuscated so that email harvesting spam robots hopefully won't get them.\n\n\nLists\n=====\n\n* This is a bulleted list\n* Great for shopping lists\n- You can also use hyphens\n+ Or plus symbols\n\nThe above is an \"unordered\" list. Now, on for a bit of order.\n\n1. Numbered lists are also easy\n2. Just start with a number\n3738762. However, the actual number doesn't matter when converted to HTML.\n1. This will still show up as 4.\n\nYou might want a few advanced lists:\n\n- This top-level list is wrapped in paragraph tags\n- This generates an extra space between each top-level item.\n\n- You do it by adding a blank line\n\n- This nested list also has blank lines between the list items.\n\n- How to create nested lists\n1. Start your regular list\n2. Indent nested lists with four spaces\n3. Further nesting means you should indent with four more spaces\n * This line is indented with eight spaces.\n\n- List items can be quite lengthy. You can keep typing and either continue\nthem on the next line with no indentation.\n\n- Alternately, if that looks ugly, you can also\nindent the next line a bit for a prettier look.\n\n- You can put large blocks of text in your list by just indenting with four spaces.\n\nThis is formatted the same as code, but you can inspect the HTML\nand find that it's just wrapped in a `

` tag and *won't* be shown\nas preformatted text.\n\nYou can keep adding more and more paragraphs to a single\nlist item by adding the traditional blank line and then keep\non indenting the paragraphs with four spaces. You really need\nto only indent the first line, but that looks ugly.\n\n- Lists support blockquotes\n\n> Just like this example here. By the way, you can\n> nest lists inside blockquotes!\n> - Fantastic!\n\n- Lists support preformatted text\n\n You just need to indent eight spaces.\n\n\nEven More\n=========\n\nHorizontal Rule\n---------------\n\nIf you need a horizontal rule you just need to put at least three hyphens, asterisks, or underscores on a line by themselves. You can also even put spaces between the characters.\n\n---\n****************************\n_ _ _ _ _ _ _\n\nThose three all produced horizontal lines. Keep in mind that three hyphens under any text turns that text into a heading, so add a blank like if you use hyphens.\n\nImages\n------\n\nImages work exactly like links, but they have exclamation points in front. They work with references and titles too.\n\n![Google Logo](http://www.google.com/images/errors/logo_sm.gif) and ![Happy].\n\n[Happy]: https://s.gravatar.com/avatar/3c5e768a35943391075225aea03443a3?size=64&default=retro (\"Smiley face\")\n\n\nInline HTML\n-----------\n\nIf markdown is too limiting, you can just insert your own crazy HTML. Span-level HTML can *still* use markdown. Block level elements must be separated from text by a blank line and must not have any spaces before the opening and closing HTML.\n\n

\nIt is a pity, but markdown does **not** work in here for most markdown parsers. [Marked] handles it pretty well.\n
\n" }, "Observable's not JavaScript (.omd)": { "type": ".omd", "content": "# Observable’s not JavaScript\n\n_A [@hpcc-js/observable-md](https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observable-md) demo - these demos are currently a work in progress and have dependencies which may or may not exist at any given time..._\n\n---\n↑ This document is best viewed with the \"Show Developer Info\" enabled above ↑\n\n---\n\nAt first glance, Observable appears to be vanilla JavaScript. This is intentional: by building on the native language of the web, Observable is more familiar and you can use the libraries you know and love, such as D3, Three, and TensorFlow. Yet for [dataflow](/@observablehq/how-observable-runs), Observable needed to change JavaScript in a few ways.\n\nHere’s a quick overview of what’s different.\n\n(We’ve also shared our [grammar](/@observablehq/observable-grammar) and [parser](https://github.com/observablehq/parser).)\n\n### Cells are separate scripts.\n\nEach cell in a notebook is a separate script that runs independently. A syntax error in one cell won’t prevent other cells from running.\n\n```\nThis is English, not JavaScript.\n```\n\nSame with a runtime error.\n\n```\n{ throw new Error(\"oopsie\"); }\n```\n\nLikewise, local variables are only visible to the cell that defines them.\n\n```\n{ var local = \"I am a local variable.\"; }\n```\n\n### Cells run in topological order.\n\nIn vanilla JavaScript, code runs from top to bottom. Not so here; Observable runs [like a spreadsheet](/@observablehq/how-observable-runs), so you can define your cells in whatever order makes sense.\n\n```\na + 2 // a is defined below\n```\n\n```\na = 1\n```\n\nBy extension, circular definitions are not allowed.\n\n```\nc1 = c2 - 1\n```\n\n```\nc2 = c1 + 1\n```\n\n### Cells re-run when any referenced cell changes.\n\nYou don’t have to run cells explicitly when you edit or interact—the notebook updates automatically. Run the cell below by clicking the play button , or by focusing and hitting Shift-Enter. Only the referencing cells run, then *their* referencing cells, and so on—other cells are unaffected.\n\n```\nb = Math.random()\n```\n\n```\nb * b // updates automatically!\n```\n\nIf a cell allocates resources that won’t be automatically cleaned up by the garbage collector, such as an animation loop or event listener, use the [invalidation promise](/@observablehq/invalidation) to dispose of these resources manually and avoid leaks.\n\n```\n{ invalidation.then(() => console.log(\"I was invalidated.\")); }\n```\n\n### Cells implicitly await promises.\n\nYou can define a cell whose value is a promise.\n\n```\nhello = new Promise(resolve => {\n setTimeout(() => {\n resolve(\"hello there\");\n }, 30000);\n})\n```\n\nIf you reference such a cell, you don’t need to await; the referencing cell won’t run until the value resolves.\n\n```\nc = {\n yield 1;\n yield 2;\n yield 3;\n}\n```\n\n```\nc\n```\n\nAlso, yields occur no more than once every animation frame: typically sixty times a second, which makes generators handy for [animation](/@mbostock/animation-loops). If you yield a DOM element, it will be added to the DOM before the generator resumes.\n\n### Named cells are declarations, not assignments.\n\nNamed cells look like, and function *almost* like, assignment expressions in vanilla JavaScript. But cells can be defined in any order, so think of them as hoisted function declarations.\n\n```\nfoo = 2\n```\n\nYou can’t assign the value of another cell (though see mutables below).\n\n```\n{ foo = 3; } // not allowed\n```\n\nCell names must also be unique. If two or more cells share the same name, they will all error.\n\n```\ndup = 1\n```\n\n```\ndup = 2\n```\n\n(Observable doesn’t yet support destructuring assignment to declare multiple names, but we hope to add that soon.)\n\n### Statements need curly braces, and return (or yield).\n\nA cell body can be a simple expression, such as a number or string literal, or a function call. But sometimes you want statements, such as for loops. For that you’ll need curly braces, and a return statement to give the cell a value. Think of a cell as a function, except the function has no arguments.\n\n```\n{\n let sum = 0;\n for (let i = 0; i < 10; ++i) {\n sum += i;\n }\n return sum;\n}\n```\n\nFor the same reason, you’ll need to wrap object literals in parentheses, or use a block statement with a return.\n\n```\nlabel = {foo: \"bar\"}\n```\n\n```\nblock = { return {foo: \"bar\"}; }\n```\n\n### Cells can be views.\n\nObservable has a special [`viewof` operator](https://observablehq.com/@observablehq/introduction-to-views) which lets you define interactive values. A view is a cell with two faces: its user interface, and its programmatic value. Try editing the input below, and note that the referencing cell runs automatically.\n\n```\nviewof text = html``\n```\n\n```\ntext\n```\n\n### Cells can be mutables.\n\nObservable has a special [`mutable` operator](/@observablehq/introduction-to-mutable-state) so you can opt-in to mutable state: you can set the value of a mutable from another cell.\n\n```\nmutable thing = 0\n```\n\n```\n++mutable thing // mutates thing\n```\n\n### Observable has a standard library.\n\nObservable provides a small [standard library](https://github.com/observablehq/stdlib/blob/trunk/README.md) for essential features, such as Markdown tagged template literals and reactive width.\n\n```\nmd`Hello, I’m *Markdown*!`\n```\n\n### Static ES imports are not supported; use dynamic imports.\n\nSince everything in Observable is inherently dynamic, there’s not really a need for static ES imports—though, we might add support in the future. Note that only the most-recent browsers support dynamic imports, so you might consider using require for now.\n\n```\n_ = import(\"https://cdn.pika.dev/lodash-es/v4\")\n```\n\n```\n_.camelCase(\"lodash was here\")\n```\n\n### require is AMD, not CommonJS.\n\n[Observable’s require](/@observablehq/introduction-to-require) looks a lot like CommonJS because cells implicitly await promises. But under the hood it uses the [Asynchronous Module Definition (AMD)](https://requirejs.org/docs/whyamd.html). This convention will eventually be replaced with modern ES modules and imports, but it’s still useful for the present as many library authors are not yet shipping ES modules.\n\nWe recommend pinning major versions.\n\n```\nd3 = require(\"d3@5\")\n```\n" }, "Roxie Demo (.omd)": { "type": ".omd", "content": "# Roxie Demo\n\n_A [@hpcc-js/observable-md](https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observable-md) demo - these demos are currently a work in progress and have dependencies which may or may not exist at any given time..._\n\n## Calling a Roxie Service\n_For this example we will be calling the following Roxie Service_\n\n* **baseUrl:** \"${baseUrl}\"\n* **querySet:** \"${querySet}\"\n* **querID:** \"${queryID}\"\n\n```\n// Initialize Target\nbaseUrl = \"https://play.hpccsystems.com:18002\";\nquerySet = \"roxie\";\nqueryID = \"covid19_by_us_states\";\n\n// Initialize Roxie\nr = roxie(\"https://play.hpccsystems.com:18002\");\nq = r.query(\"roxie\", \"covid19_by_countries\");\nrequestFields = q.requestFields();\nresponseFields = q.responseFields();\n```\n\nOnce connected to the server we can query the service for its request/response schemas:\n\n* Request Schema: \n```json\n${JSON.stringify(requestFields, undefined, \" \")}\n```\n\n* Response Schema:\n```json\n${JSON.stringify(responseFields, undefined, \" \")}\n```\n\nAt which point we can make some actual REST requests:\n\n```javascript\nq.submit({\n countriesfilter: \"FRANCE\"\n});\n```\n\n```\nresponse = q.submit({\n countriesfilter: \"FRANCE\"\n});\n```\n\nActual Response:\n```json\n${JSON.stringify(response, undefined, \" \")} \n```\n" }, "Standard Library (.omd)": { "type": ".omd", "content": "# Standard Library\n\n_A [@hpcc-js/observable-md](https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observable-md) demo - these demos are currently a work in progress and have dependencies which may or may not exist at any given time..._\n\n## Visualizations\n\n### Table\n\n_The table visualization is used to display data in a spreadsheet5 like grid. It is based on the [@hpcc-js/dgrid](../../../packages/dgrid) widget._\n\n#### Creation:\n* Type 1:\n viewof myTable = table(...props);\n viewof myTable.json([{col1: \"Hello\", col2:\"World\"}]);\n\n* Type 2:\n viewof myTable = table(...props);\n viewof myTable.json([{col1: \"Hello\", col2:\"World\"}]);\n\n\n```\nviewof myTable = table();\nviewof myTable.mulitSelect = true;\nviewof myTable.json([{col1: \"Hello 1\", col2:\"World 1\"},{col1: \"Hello 2\", col2:\"World 2\"},{col1: \"Hello 3\", col2:\"World 3\"}]);\n```\n\n${JSON.stringify(myTable)}\n\n${viewof myTable.help()}\n\n```\npalID = chart.createOrdinalPalette({Deaths:\"red\", Confirmed: \"Orange\", Confirmed: \"Green\"})\nviewof line = chart.line({title:\"By Date\", height:240, legendVisible:true, widget:{yAxisTitle:\"YYYY\", paletteID: palID}});\nviewof line.json([{col1: \"Deaths\", Deaths:22},{col1: \"Confirmed\", col2:33},{col1: \"Confirmed\", col2:44}]); \n```\n" }, "Workunit Demo (.omd)": { "type": ".omd", "content": "# Workunit Demo\n\n_A [@hpcc-js/observable-md](https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observable-md) demo - these demos are currently a work in progress and have dependencies which may or may not exist at any given time..._\n\n## Attach to a WU on a HPCC-Platform \n* **Platform**: ${platform.url} \n* **WU**: ${wu.wuid}\n* **Result Count**: ${results.length}\n* **Result Names**:\n${fromattedResults}\n\n```\nplatform = esp(\"https://play.hpccsystems.com:18010\");\nwu = platform.wu(\"W20200109-161403\");\nresults = wu.results();\nfromattedResults = results.map(r => ` * ${r.name}\\n`);\n```\n\nIts easy to browse the first result **`${results[0].name}`**:\n```\nresults[0].table(); \n```\n" } };