Published
Edited
May 7, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
selectedBuildingId
Insert cell
Insert cell
Insert cell
md`## Selections`
Insert cell


slide0 = pitch.fade`
${selectedBuildingId && buildingIdToName[selectedBuildingId]? `

<!-- Ref: https://medium.com/@chrisnager/center-and-crop-images-with-a-single-line-of-css-ad140d5b4a87 -->
<img src="${selectedBuilding.icon.url}" style="
width: ${0.2*width}px;
height: ${0.2*width}px;
object-fit: cover;
border-radius: 5%">
${ (groupIdToName[selectedGroupId] != buildingIdToName[selectedBuildingId]) ?`
<div class="title" style="font-size:${0.06*width}px"> ${groupIdToName[selectedGroupId]}</div>
<div class="title" style="font-size:${0.06*.618*width}px"> ${buildingIdToName[selectedBuildingId]} </div>
`:
`<div style="font-size:${0.06*width}px"> ${buildingIdToName[selectedBuildingId]}</div>`
}
<div style="font-size:${0.06*.618*width}px">Smart Building Report with Negawatt BOS</div>
`
: ""}`

Insert cell

slide1 = pitch.inplace(slide0).fade`
<style>
table.bos-usage {
width: 100%;
}
table.bos-usage tr{
border: none;
}
table.bos-usage img.module-logo {
width: ${50 * width/1000}px;
height: ${50 * width/1000}px;
object-fit: cover;
border-radius: 10%;
display: inline-block;
}
table
</style>

<table class="bos-usage">
<tr style="width: 200px;">
<td rowspan=3>
<img src="https://bos.negawatt.co/assets/images/logo-loading-fast.gif" style="
width: ${200 * width/1000}px;
height: ${200 * width/1000}px;
object-fit: cover;
border-radius: 5%;
background-color: #d9ebf4;
display: inline-block;
margin: 10px;
"
>
</td>
<td>
<img
class="module-logo"
src="https://bos.negawatt.co/assets/images/module_logo/bsm-L-color-bg.svg"
style="background-color: rgb(19, 140, 239);
">
</td>
</tr>
<tr>
<td>
<img
class="module-logo"
src="https://bos.negawatt.co/assets/images/module_logo/erm-L-color-bg.svg"
style="background-color: rgb(227, 115, 54)">
</td>
</tr>
<tr>
<td>
<img
class="module-logo"
src="https://bos.negawatt.co/assets/images/module_logo/cam-L-color-bg.svg"
style="background-color: purple;">
</td>
</tr>
</table>

`

Insert cell
selectedBuilding
Insert cell
(await FileAttachment("Screenshot from 2020-04-27 16-54-42.png")).url()

Insert cell
/*
<!-- div class="stoppable">
<canvas style="display: none" width="200" height="200"></canvas>
<img src="https://bos.negawatt.co/assets/images/logo-loading-fast.gif" style="
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 5%;
background-color: #d9ebf4;"
>
<script>
var c = $(".stoppable canvas")[0];
var w = c.width;
var h = c.height;
var img = $(".stoppable img")[0];
console.log(c);
console.log(img)
setTimeout(function () {
c.getContext('2d').drawImage(img, 0, 0, w, h);
$(img).hide();
$(c).show();
},1200);
</script>
</div-->
*/
Insert cell
/*
html`
<div style="
background-image: url(https://bos.negawatt.co/assets/images/logo-loading-fast.gif);
background-position: 50% 50%;
">
</div>

`
*/
Insert cell
/*
html`
<div class="stoppable">
<canvas style="display: none" width="100" height="100"></canvas>
<img style="display: block" src="https://bos.negawatt.co/assets/images/logo-loading-fast.gif"/>
<img style="display: none" src="https://bos.negawatt.co/bos-color.fbd7ae875d86f9b4f29e.svg"/>
</div>

<style>

.stoppable {
width: 100px;
height: 100px;
}
.stoppable canvas, .stoppable img {
width: 100%;
height: 100%;
}
img {
width: 100px;
height: 100px;
}
</style>

<script>
</script>

`
*/
Insert cell
/*
html`
<style>
#mapid {
height: 530px;
}

.leaflet-container .leaflet-control-attribution {
background: transparent;
}
</style>

<div id="mapid"></div>`
*/
Insert cell
/*
{
// resize map
const mapdiv = document.getElementById('mapid');
var margin;
if (document.all) {
margin = parseInt(document.body.currentStyle.marginTop, 10) + parseInt(document.body.currentStyle.marginBottom, 10);
} else {
margin = parseInt(document.defaultView.getComputedStyle(document.body, '').getPropertyValue('margin-top')) + parseInt(document.defaultView.getComputedStyle(document.body, '').getPropertyValue('margin-bottom'));
}
mapdiv.style.height = (window.innerHeight - margin) + 'px';

/////
var map = L.map('mapid').setView([22.29227, 114.20847], 16);

var apikey = '584b2fa686f14ba283874318b3b8d6b0'
L.tileLayer('https://api.hkmapservice.gov.hk/osm/xyz/basemap/WGS84/tile/{z}/{x}/{y}.png?key=' + apikey, {
attribution: "<a href='https://api.portal.hkmapservice.gov.hk/disclaimer' target='_blank'>&copy; Map from Lands Department <img src='https://api.hkmapservice.gov.hk/mapapi/landsdlogo.jpg' style='width:25px;height:25px'/></a>",
maxZoom: 19,
id: 'APIKEY'
}).addTo(map);


L.tileLayer('https://api.hkmapservice.gov.hk/osm/xyz/label-tc/WGS84/tile/{z}/{x}/{y}.png?key=' + apikey, {
maxZoom: 19,
id: 'APIKEY'
}).addTo(map);

}
*/
Insert cell
/*

map = {
// You'll often see Leaflet examples initializing a map like L.map('map'),
// which tells the library to look for a div with the id 'map' on the page.
// In Observable, we instead create a div from scratch in this cell, so it's
// completely self-contained.
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
container.style.backgroundColor = "white";
// Note that I'm yielding the container pretty early here: this allows the
// div to be placed on the page. This is important, because Leaflet uses
// the div's .offsetWidth and .offsetHeight to size the map. If I were
// to only return the container at the end of this method, Leaflet might
// get the wrong idea about the map's size.
yield container;
var apikey = '584b2fa686f14ba283874318b3b8d6b0'
// Now we create a map object and add a layer to it.
let map = L.map(container, {
crs: L.CRS.Simple,
minZoom: -5
}).setView( [-100, 130], 1.5);
let osmLayer = L.tileLayer('https://api.hkmapservice.gov.hk/osm/xyz/basemap/WGS84/tile/{z}/{x}/{y}.png?key=' + apikey,
//'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}@2x.png',
{ attribution: 'Wikimedia maps beta | &copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
id: 'APIKEY'
}).addTo(map);
L.control.scale().addTo(map);
console.log(map);
//heatmap.setData(dataPoints);
//map.addLayer(heatmap);
return map;
}
*/
Insert cell
/*
mapBsm = {
// You'll often see Leaflet examples initializing a map like L.map('map'),
// which tells the library to look for a div with the id 'map' on the page.
// In Observable, we instead create a div from scratch in this cell, so it's
// completely self-contained.
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
container.style.backgroundColor = "white";
// Note that I'm yielding the container pretty early here: this allows the
// div to be placed on the page. This is important, because Leaflet uses
// the div's .offsetWidth and .offsetHeight to size the map. If I were
// to only return the container at the end of this method, Leaflet might
// get the wrong idea about the map's size.
yield container;
// Now we create a map object and add a layer to it.
let map = L.map(container, {
crs: L.CRS.Simple,
minZoom: -5
}).setView( [-100, 130], 1.5);
let osmLayer = L.tileLayer(
`https://api.negawatt.co/v1/asset/sites/${getSiteId( selectedBuildingId )}/buildings/${selectedBuildingId}/blocks/54a980fcb2e4ec9573d10e11/floors/54a981903e6eb434b7115f02/tiles/5a9cec6958e55539d8e039ad/z/{z}/y/{y}/x/{x}?token={token}`,
//'https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}@2x.png',
{ attribution: 'Wikimedia maps beta | &copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
token: selectedBuildingAccessToken
}).addTo(map);
L.control.scale().addTo(map);
console.log(map);
//heatmap.setData(dataPoints);
//map.addLayer(heatmap);
return map;
}
*/
Insert cell
selectedBuilding
Insert cell
L.map
Insert cell
Insert cell
Insert cell
/**
* Request resource from BOS with its API; Store result by default if parameter `store` is given.
* Ref: {@link https:// en.wikipedia.org/wiki/Representational_state_transfer}
*/
function requestBOS(resource, store, forceUpdate, config, /*authInputs*/) {
let url = `https://api.negawatt.co/v1/${resource}`
forceUpdate = forceUpdate == true ? forceUpdate : false;
//authInputs = authInputs || [[undefined], [undefined], [undefined]];
// authentication
function getBosEmail() {
try { return Secret("BOS_EMAIL"); }
catch { return authInputs[0][0]; }
}
function getBosPassword() {
try { return Secret("BOS_PASSWORD"); }
catch { return authInputs[1][0]; }
}
function getBosUserId() {
try { return Secret("BOS_USER_ID"); }
catch { return authInputs[2][0]; }
}
// Request with result storing locally
async function requestWithStore (token, body) {
let method = (token && (body === undefined || body == {})) ? "GET" : "POST";
if (store) {
let key;
if (config) {
key = JSON.stringify([resource, config])
}
else {
key = resource
}
let r = await store.getItem(key);

if (!r || forceUpdate) {
r = await request(url, method, body, token);
store.setItem(key, r);
}
return r;
}
else {
return await request(url, method, body, token);
}
}

// Aliases
const rs = requestWithStore;
const getToken = async () => (await requestBOS("auth/token", store, forceUpdate, config)).accessToken;

let methods = {
"users/login":
() => rs(null, { email: getBosEmail(), password: getBosPassword() } ),
"auth/token":
async() => await rs(null, {
..._.omit(await requestBOS("users/login", store, forceUpdate), ["accessToken"]),
user: getBosUserId(),
scope: config || {}
} ),
"users/me/groups":
async() => await rs(await getToken() ),
"users/me":
async() => await rs(await getToken() ),
"asset/sites":
async() => await rs(await getToken() ),
"asset/buildings":
async() => await rs(await getToken() ),
"statistics/buildings":
async() => await rs(await getToken(), config),
};
if ( resource.search(/asset\/sites\/(.*)\/buildings\/(.*)/) == 0 && "group" in config) {
let buildingId = resource.replace(/asset\/sites\/(.*)\/buildings\/(.*)/, `$1,$2`).split(',')[1];
config["building"] = buildingId
methods[resource] =
async() => await rs(await getToken() );
console.log(resource)
getToken().then(console.log);
}
try {
return methods[resource]();
}
catch {
forceUpdate = true;
return methods[resource]();
}
}

Insert cell
Insert cell
//bosStore.clear()
Insert cell
bosStore.keys()
Insert cell
Insert cell
bosStore.keys()
Insert cell
Insert cell
requestBOS("users/login", bosStore, forceUpdate)
Insert cell
requestBOS("auth/token", bosStore, forceUpdate)
Insert cell
requestBOS("users/me", bosStore, forceUpdate)
Insert cell
requestBOS("users/me", bosStore, forceUpdate)
Insert cell
Insert cell
groups = requestBOS("users/me/groups", bosStore, forceUpdate)
Insert cell
sites = requestBOS("asset/sites", bosStore, forceUpdate, selectedGroupId && {group: selectedGroupId} )
Insert cell
buildings = requestBOS("asset/buildings", bosStore, forceUpdate, selectedGroupId && {group: selectedGroupId} )
Insert cell
Insert cell
Insert cell
Insert cell
selectedBuildingAccessToken = requestBOS("auth/token", bosStore, forceUpdate,
{"group": selectedGroupId, "building": selectedBuildingId})
.then(x=>x.accessToken).catch(()=>"")
Insert cell
selectedBuilding = selectedGroupId && selectedBuildingId && requestBOS(
`asset/sites/${getSiteId( selectedBuildingId )}/buildings/${selectedBuildingId}`,
bosStore, forceUpdate,
{"group": selectedGroupId}
);
Insert cell
blocks = selectedGroupId && selectedBuildingId && requestBOS(
`asset/sites/${getSiteId( selectedBuildingId )}/buildings/${selectedBuildingId}/blocks`,
bosStore, forceUpdate,
{"group": selectedGroupId}
);
Insert cell
urlTest = `asset/sites/${getSiteId( selectedBuildingId )}/buildings/${selectedBuildingId}/blocks`
Insert cell
selectedBuildingBlocks2 = selectedGroupId && selectedBuildingId && requestBOS(
`asset/sites/${getSiteId( selectedBuildingId )}/buildings/${selectedBuildingId}`,
bosStore, forceUpdate,
{"group": selectedGroupId}
);
Insert cell
buildings
Insert cell
/*

selectedBuildingBlocks = selectedGroupId && selectedBuildingId && requestBOS(
`asset/sites/${getSiteId( selectedBuildingId )}/buildings/${selectedBuildingId}/blocks`,
bosStore, forceUpdate,
{"group": selectedGroupId}
);
*/
Insert cell
request("https://api.negawatt.co/v1/" + urlTest, "GET", undefined, selectedBuildingAccessToken)
Insert cell
/**
* Facillate JSON request using D3
* Ref: {@link https://github.com/d3/d3-fetch}
*/
function request2 (url, method, body, token) {
//console.log(url + " " + "method" + " " + body + " " + (auth? "Bearer " + auth : undefined))
// return promise by d3.json
return d3.json(url, {
method,
body: method == "POST"? JSON.stringify(body): undefined,
headers: {
"Content-type": "application/json; charset=UTF-8",
"Transfer-Encoding": "gzip",
"Authorization": token? "Bearer " + token : undefined
}
});
}

Insert cell
md`### IV. &nbsp; \`Statistics\``
Insert cell
async function getBosStat(buildingId, buildingAccessToken, start, end) {
buildingId = buildingId || selectedBuildingId;
buildingAccessToken = buildingAccessToken || selectedBuildingAccessToken;
start = start || "2000-01-01T00:00:00+00:00";
end = end || moment().format();

return await request("https://api.negawatt.co/v1/statistics/buildings", "POST", {
from: start,
to: end,
buildings: [buildingId]
}, buildingAccessToken);
}
Insert cell
async function getBosStatOverPastMonths (nMonth, buildingId, buildingAccessToken) {
buildingId = buildingId || selectedBuildingId;
buildingAccessToken = buildingAccessToken || selectedBuildingAccessToken;
const format = "YYYY-MM-DDT00:00:00+08:00";
let starts = _.range(nMonth).map( d => moment().subtract(d+1, 'month').format(format) ),
ends = _.range(nMonth).map( d => moment().subtract(d , 'month').format(format) );
let result = await Promise.all( _.reverse(_.zip( starts, ends )).map(
d=>getBosStat(buildingId, buildingAccessToken, d[0],d[1]))
);
result = _.flatten(result);
result = z.addCol("start", starts, result);
result = z.addCol("end", ends , result);
return result;
}
Insert cell
bosPastStat = getBosStatOverPastMonths(24)
Insert cell
sparkline( z.getCol("dataCount", bosPastStat) )
Insert cell
z.getCol("dataCount", bosPastStat)
Insert cell
bosCurrentStat = getBosStat()
Insert cell
getBosStatOverPastMonths(12)
Insert cell
/*
request("https://api.negawatt.co/v1/statistics/buildings", "POST", {'from': '2019-01-01T00:00:00.000Z', 'to': '2021-01-01T00:00:00.000Z'}, selectedBuildingAccessToken)
*/
Insert cell
requestBOS("statistics/buildings", bosStore, forceUpdate,
{from: '2019-01-01T00:00:00.000Z',
to: '2021-01-01T00:00:00.000Z',
buildings: [selectedBuildingId]
})

Insert cell
requestBOS("statistics/buildings", bosStore, forceUpdate,
{from: '2019-01-01T00:00:00.000Z',
to: '2021-01-01T00:00:00.000Z',
buildings: [selectedBuildingId]
})

Insert cell
buildingIdToName[ selectedBuildingId ]
Insert cell
sparkline([0, 8, 3, 2, 6, 5, 1])
Insert cell
/*
bosStatPast12Months = Promise.all( _.reverse(_.zip(
_.range(12).map( d => moment().subtract(d+1, 'month').format() ),
_.range(12).map( d => moment().subtract(d, 'month').format() )
)).map( d=>getBosStat(d[0],d[1])) )
*/
Insert cell
Insert cell
groupIdToName = dictionary(groups, "_id", "name")
Insert cell
groupNameToId = dictionary(groups, "name", "_id")
Insert cell
buildingIdToName = dictionary(buildings, "_id", "name")
Insert cell
buildingNameToId = dictionary(buildings, "name", "_id")
Insert cell
getBOS = ({
groups: forceUpdate => requestBOS("users/me/groups", bosStore, forceUpdate),
sites: (selectedGroupId, forceUpdate) =>
requestBOS("asset/sites", bosStore, forceUpdate, selectedGroupId && {group: selectedGroupId} ),
buildings: (selectedGroupId, forceUpdate) =>
requestBOS("asset/buildings", bosStore, forceUpdate, selectedGroupId && {group: selectedGroupId} ),
getSiteId: buildings => function getSiteId(buildingId) {
let building = _.find( buildings, {_id: selectedBuildingId});
return building && building.site._id
},
groupIdToName: groups => dictionary(groups, "_id", "name"),
groupNameToId: groups => dictionary(groups, "name", "_id"),
buildingIdToName: buildings => dictionary(buildings, "_id", "name"),
buildingNameToId: buildings => dictionary(buildings, "name", "_id"),
selectedBuilding: (selectedBuildingId, selectedSiteId, selectedGroupId, forceUpdate) =>
selectedGroupId && selectedBuildingId && requestBOS(
`asset/sites/${selectedSiteId}/buildings/${selectedBuildingId}`,
bosStore, forceUpdate,
{"group": selectedGroupId}
),
selectedBuildingAccessToken: (selectedBuildingId, selectedGroupId, forceUpdate) =>
requestBOS("auth/token", bosStore, forceUpdate,
{"group": selectedGroupId, "building": selectedBuildingId})
.then(x=>x.accessToken).catch(()=>"")
})
Insert cell
selectedSiteId = getSiteId(selectedBuildingId)
Insert cell
getBOS.selectedBuilding(selectedBuildingId, selectedSiteId, selectedGroupId)
Insert cell
{ return (await bosStore.keys() )}
Insert cell
bosStore.getItem((await bosStore.keys() )[10])
Insert cell
requestBOS("auth/token", bosStore, {group: groupNameToId["(ICM) Gaw Capital"]})
Insert cell
groupNameToId[selectedGroupId]
Insert cell
md`---`
Insert cell
group_id = "5dc283981984eae9d1ee6316"
Insert cell
building_id = "5dc283e51984eaa017ee6317"// "5c7cbc7a44737842e1187bfb"
Insert cell
port_id = "5de7faaaafe9f2c94f184838"//"5c7fa378f0ac5b5d172aafc4"
Insert cell
Insert cell
start = "2020-03-31T16:00:00.000Z"
Insert cell
end = "2020-04-06T04:22:00.000Z"
Insert cell
Insert cell
Insert cell
Insert cell
localStorage = window.localStorage
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
md`### Secret local storage`[selectedBuildingId]
Insert cell
Insert cell
Insert cell
_ = require("lodash");
Insert cell
Insert cell
Insert cell
moment = require("moment")
Insert cell
Insert cell
Insert cell
function sparkline(values, width = 64, height = 17) {
const x = d3.scaleLinear().domain([0, values.length - 1]).range([0.5, width - 0.5]);
const y = d3.scaleLinear().domain(d3.extent(values)).range([height - 0.5, 0.5]);
const context = DOM.context2d(width, height);
const line = d3.line().x((d, i) => x(i)).y(y).context(context);
context.beginPath(), line(values), context.stroke();
return context.canvas;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function inputsGroup(views,names){
const form = html`<div>${
views.map(row => html`<div>${
row.map(input => html`<div style="
display:inline-block;
vertical-align:middle;
margin: 5px">${input}</div>`)
}</div>`)
}</div>`;
form.oninput = () => {
form.value = views.map(row => row.map(input => input.value))
if(names){
names.forEach((row,i)=>row.forEach((c,j)=> form.value[i][j] && (form.value[c]=form.value[i][j])))
}
};
form.oninput();
return form;
}
Insert cell
import {pitch} from "@enkimute/pitch"
Insert cell
pitch.styles = html`
<style>
.slide { width: calc(100% + 28px); margin: 0 -14px; padding: 10%;
background: #333; color: #eee; min-height: 65vw; font-size: 5vw; overflow:hidden;
line-height: 1.15; display: flex; align-items: center; box-sizing:border-box;
transition: opacity 1s ease-in-out, margin 1s ease-in-out, padding 1s ease-in-out, filter 1.5s ease-in-out;
}
.slide.fade { opacity: 0; }
.slide.left { margin-left: calc(-100% - 42px); }
.slide.right { padding-left: calc(100% + 42px); overflow:hidden; }
.slide.blur { filter: blur(20px); }
.slide.sepia { filter: sepia(100%); }
.slide.blur.sepia { filter: blur(20px) sepia(100%);}
.slide h1, .slide h2, .slide h3 { color:#eee }
.slide p, .slide pre, .slide img { max-width: 100%; }
.slide blockquote, .slide ol, .slide ul { max-width: none; }
.slide code { font-size: 3vw; }
.slide.code { background: #eee; color: #444; padding-top:0; }
.slide.img { max-height:80vw; overflow:hidden; }
.slide.full { padding:0; background:transparent; }
_slide { overflow:hidden; position:relative; min-height: 65vw; }

.slide .title {
font-weight: 700;
font-family: "Source Serif Pro",Iowan Old Style,Apple Garamond,Palatino Linotype,Times New Roman,"Droid Serif",Times,serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;";
}
</style>`;
Insert cell
L = require("leaflet@1.6.0")
Insert cell
leafletCSS = html`<link href='${resolve('leaflet@1.2.0/dist/leaflet.css')}' rel='stylesheet' />`
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