Public
Edited
Oct 10, 2024
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Flow = {
dayjs.extend(dayjsUtcPlugin);
dayjs.extend(dayjsTzPlugin);
dayjs.extend(dayjsCustomParseFormatPlugin);

return {
// The Flo.w RDF context
context: globalContext,

// Dataset functions
datasetTable,
datasetAttributes,
datasetSource,
datasetSummary,
datasetQuery,
datasetQueryTable,
queryBuilder: flowQueryBuilder,

// Tilesource functions
tileSourceTable,

// Map functions
mapStyleTable,
mapClickedFeature,
mapSelectedFeatureId,
mapSelectedFeature,

// Reactive state functions
stateGet,
stateSet,
stateMerge,
stateTransaction,

// Misc functions
fullscreen,

// Misc 3rd-party library added for convenience
toc,
vega,
turf,
dayjs
};
}
Insert cell
Insert cell
Insert cell
config = ({
apiKey: "b6b77cc4-904a-46d1-aa0f-3bf3848ce4c7",
host: "https://flow.emu-analytics.net",
version: "3.0.5"
})
Insert cell
Insert cell
globalContext = initializeFlow(
config.apiKey,
config.version,
config.host,
config.initialState
)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
/** Initialize Flo.w
*
* apiKey: string - a valid Flo.w API key (uuid)
* version: string (optional) - Flo.w RDF version number (e.g. '0.4.10'). If omitted, the latest version
* is used.
* host: string (optional) - Flo.w host URL (defaults to https://flow.emu-analytics.net)
*
* Returns the Flo.w context
*/
initializeFlow = async function (
apiKeyOrConfig,
version,
host = "https://flow.emu-analytics.net",
initialState = {}
) {
let config;

// Wrangle argument for backwards compatibility
if (typeof apiKeyOrConfig === "string") {
config = {
apiKey: apiKeyOrConfig,
host,
initialState,
version
};
} else {
config = apiKeyOrConfig;
}

// Apply defaults
config = {
host: "https://flow.emu-analytics.net",
enableDebug: true,
initialState: {},
...config
};

version = version ? "@" + version : "";

// Add Flo.w RDF script tag to register custom web components
const script = document.createElement("script");
script.type = "module";
script.src = `https://cdn.jsdelivr.net/npm/@emuanalytics/flow-rdf${version}/dist/flow-rdf/flow-rdf.esm.js`;
document.getElementsByTagName("head")[0].appendChild(script);

// Inject Flow core CSS
const coreStyles = document.createElement("link");
coreStyles.rel = "stylesheet";
coreStyles.href = `https://cdn.jsdelivr.net/npm/@emuanalytics/flow-rdf${version}/dist/flow-rdf/css/core.css`;
document.getElementsByTagName("head")[0].appendChild(coreStyles);

const flowLib = await import(
`https://cdn.jsdelivr.net/npm/@emuanalytics/flow-rdf${version}/dist/flow-rdf/index.esm.js`
);

// Initialize the context
const ctx = flowLib.flow.initializeContext(config);

// Wait for custom elements to be registered
await customElements.whenDefined("flow-context");

ctx[contextSymbol] = true;

return ctx;
}
Insert cell
Insert cell
Insert cell
Insert cell
/**
* Display a table of datasets defined in supplied context
*/
datasetTable = async function (context, options) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
console.log("using global context");
options = context;
context = globalContext;
}

const datasets = await context.flowClient.datasets.list({ sort: "id" });

if (datasets.length) {
const idFilter = options?.ignoreUnderscores
? (ds) => !ds.id.startsWith("_")
: (ds) => true;

const dsInfo = datasets.filter(idFilter).map((ds) => ({
"Dataset ID": ds.id,
Name: ds.name,
Description: ds.description,
Type: ds.type
}));

return Inputs.table(dsInfo, { layout: "auto" });
} else {
return md`No datasets`;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
/**
* Display a table of tile sources defined in the current context
*/
tileSourceTable = async function(context, options) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
options = context;
context = globalContext;
}
const tileSources = await context.flowClient.tileSources.list();

if (tileSources.length) {
const tsInfo = tileSources.map(ts => ({
'TileSource ID': ts.id,
Type: ts.type,
Source: ts.source
}));

return Inputs.table(tsInfo, {layout: 'auto'});
} else {
return md`No tile sources`;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
/**
* Display a table of basemap styles defined in the current context
*/
mapStyleTable = async function (context, options) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
options = context;
context = globalContext;
}

const styles = await context.flowClient.styles.list();

if (styles.length) {
const info = styles.map((s) => ({
"Style ID": s.id,
Name: s.name,
Description: s.description,
Type: s.type,
}));

return Inputs.table(info, { layout: "auto" });
} else {
return md`No map styles`;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
/**
* Display dataset source with SQL syntax highlighting
*/
datasetSource = async function (datasetIdOrObj, context) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
context = globalContext;
}

const dataset = await getDataset(datasetIdOrObj, context);

if (dataset) {
return md`
~~~sql
${dataset.source}
~~~
`;
} else {
return md`Dataset not found`;
}
}
Insert cell
Insert cell
Insert cell
datasetAttributes("airquality")
Insert cell
/**
* Display table of dataset attributes
*/
datasetAttributes = async function (datasetIdOrObj, tableOptions, context) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
context = globalContext;
}

const dataset = await getDataset(datasetIdOrObj, context);

if (dataset) {
const attrs = dataset.attributes;

return Inputs.table(
attrs.map((a) => ({
attribute: a.attribute,
type: a.type,
"DB Type": a.dbType,
description: a.description || "-"
})),
{ layout: "auto", ...tableOptions }
);
} else {
return md`Dataset not found`;
}
}
Insert cell
Insert cell
Insert cell
/**
* Display full summary of supplied dataset
*/
datasetSummary = async function (datasetIdOrObj, options, context) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
context = globalContext;
}

options = {
headingLevel: 2,
...options
};

const paramRegex = /{{([a-zA-Z_\-\d]+)}}/gm;
const dataset = await getDataset(datasetIdOrObj, context);

if (dataset) {
const heading = md`${"#".repeat(options.headingLevel)} Dataset \`${
dataset.id
}\``;

const description = md`${dataset.description || "No description"}`;

const propertyTable = table(
[
{ property: "Name", value: dataset.name },
{ property: "Type", value: dataset.type },
{
property: "Created",
value: humanDate.prettyPrint(dataset.createdAt)
},
{
property: "Updated",
value: humanDate.relativeTime(dataset.updatedAt)
}
],
{ header: false }
);

const params = Array.from(
new Set(
dataset.type === "sql"
? ((dataset.source || "").match(paramRegex) || []).map((p) =>
p.replace(/[{}]/g, "")
)
: []
)
);

const parameterTable = params.length
? table(params.map((p) => ({ Parameters: p })))
: "";

const source = await datasetSource(dataset, context);
const attributeTable = await datasetAttributes(dataset, context);

return htl.html`
${heading}
${description}
${propertyTable}
${parameterTable}
<h5 style="font-family: sans-serif;">SOURCE</h5>
${source}
${attributeTable}
`;
} else {
return md`Dataset not found`;
}
}
Insert cell
Insert cell
datasetQuery({
datasetId: "airquality",
limit: 10
})
Insert cell
datasetQuery = function(query, context) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
context = globalContext;
}

return context.flowClient.datasets.query(query);
}
Insert cell
Insert cell
Insert cell
// Display query results as a table
viewof queryResults = datasetQueryTable(
{
datasetId: "airquality",
limit: 10
},
{ layout: "auto" }
)
Insert cell
// Use queryResults in another cell
queryResults
Insert cell
datasetQueryTable = async function (query, tableOptions, context) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
context = globalContext;
}

const results = await context.flowClient.datasets.query(query);
const table = Inputs.table(results, { ...tableOptions });
//table.value = results;

return table;
}
Insert cell
Insert cell
Insert cell
mapClickedFeature = function (map) {
return Generators.observe((notify) => {

notify(undefined);
const clicked = (ev) => {
notify(ev.detail.features[0]);
};

map.addEventListener("flowMapClick", clicked);

return () => map.removeEventListener("flowMapClick", clicked);
});
}
Insert cell
Insert cell
Insert cell
mapSelectedFeatureId = function (map, dataSourceId) {
return Generators.observe((notify) => {

notify(undefined);
const stateChanged = (ev) => {
if (dataSourceId === ev.detail.dataSourceId) {
notify(ev.detail.selection);
}
};

map.addEventListener("flowMapSelectionStateChanged", stateChanged);

return () =>
map.removeEventListener("flowMapSelectionStateChanged", stateChanged);
});
}
Insert cell
Insert cell
Insert cell
mapSelectedFeature = function (map, dataSourceId) {
return Generators.observe((notify) => {

notify(undefined);
const stateChanged = (ev) => {
if (dataSourceId === ev.detail.dataSourceId) {
notify(ev.detail.selection);
}
};

map.addEventListener("flowMapSelectionFeatureStateChanged", stateChanged);

return () =>
map.removeEventListener("flowMapSelectionFeatureStateChanged", stateChanged);
});
}
Insert cell
Insert cell
stateGet = function (prop, context) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
context = globalContext;
}

return Generators.observe((notify) => {
const subscription = context.state.toStream(prop).subscribe(notify);

return () => subscription.unsubscribe();
});
}
Insert cell
Insert cell
stateSet = function (prop, val, context) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
context = globalContext;
}
context.state.set(prop, val);
}
Insert cell
Insert cell
stateMerge = function (prop, val, context) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
context = globalContext;
}
context.state.merge(prop, val);
}
Insert cell
Insert cell
stateTransaction = function (fn, context) {
// Wrangle arguments for backwards compatibility
if (!context || !context[contextSymbol]) {
context = globalContext;
}
context.state.transaction(fn);
}
Insert cell
fullscreen = function (cell, options) {
if (
!document.body.requestFullscreen &&
!document.body.webkitRequestFullscreen
) {
return false;
}
if (document.body.className === "fullscreen") {
return false;
}

if (cell.fullscreenicon) {
cell.fullscreenicon.remove();
}

cell.fullscreenicon = fullScreenButton();
cell.fullscreenicon.onclick = () => {
const container = cell.parentNode;
if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.webkitRequestFullscreen) {
container.webkitRequestFullscreen();
}
};
if (cell.parentNode) {
cell.parentNode.appendChild(cell.fullscreenicon, cell);
console.log("adding fullscreen button");
if (options?.center) cell.parentNode.className += " center";
}

if (!fullScreenStyle.attached) {
fullScreenStyle.attached = true;
document.getElementsByTagName("head")[0].appendChild(fullScreenStyle());
}

return true;
}
Insert cell
Insert cell
getDataset = async function (datasetIdOrObject, context) {
return typeof datasetIdOrObject === "string"
? context.flowClient.datasets.get(datasetIdOrObject)
: datasetIdOrObject;
}
Insert cell
fullScreenButton = () =>
html`<div class="fullscreen" title="View in Fullscreen"></div>`
Insert cell
fullScreenStyle = () => html`<style>
div.fullscreen {
background-image: url('data:image/svg+xml;charset=US-ASCII,${fullScreenIcon}');
background-repeat: no-repeat;
position: absolute;
right: -16px;
top: 0px;
width: 16px;
height: 16px;
}
:fullscreen div.fullscreen { display: none; }
:fullscreen.center {
display: flex;
align-items: center;
}

:fullscreen flow-map {
width: 100% !important;
height: 100% !important;
}
</style>
`
Insert cell
fullScreenIcon = encodeURI(
`<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' class='mr2'><path d='M8 2L3 2H2L2 3L2 8L4 8L4 5.47903L6.93541 8.41443L8.34963 7.00022L5.34941 4L8 4L8 2ZM8 14H13H14V13V8H12V10.6122L9.5 8.15797L8.1 9.58317L10.5589 12H8V14Z' fill='currentColor'></path></svg>`
).replace(/'/g, '"')
Insert cell
Insert cell
Insert cell
import { table } from "@tmcw/tables"
Insert cell
Insert cell
Insert cell
Insert cell
vega = require("vega-embed@6.25")
Insert cell
turf = require("@turf/turf@prerelease")
Insert cell
dayjs = require("dayjs")
Insert cell
dayjsUtcPlugin = require("dayjs/plugin/utc")
Insert cell
dayjsTzPlugin = require("dayjs/plugin/timezone")
Insert cell
dayjsCustomParseFormatPlugin = require("dayjs/plugin/customParseFormat")
Insert cell
import { flowQueryBuilder } from "@emuanalytics/flow-query-builder"
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