
<mvc:View
controllerName="ns.ZGROWTHMAP.controller.Main"
xmlns:mvc="sap.ui.core.mvc"
displayBlock="true"
xmlns="sap.m"
xmlns:core="sap.ui.core"
xmlns:controls="ns.ZGROWTHMAP.controls"
xmlns:base="sap.ui.base"
xmlns:html="http://www.w3.org/1999/xhtml">
<Page id="page" title="{i18n>title}">
<!--Scrubber-->
<html:form id="scrubberfrm" style="font: 12px var(--sans-serif); font-variant-numeric: tabular-nums; display: flex; height: 33px; align-items: center;">
<html:button name="b" type="button" style="margin-right: 0.4em; width: 5em;"></html:button>
<html:label style="display: flex; align-items: center;">
<html:input name="i" type="range" min='0' max='0' value='0' step='1' style="width: 180px;"></html:input>
<html:output name="o" style="margin-left: 0.4em;"></html:output>
</html:label>
</html:form>
<!--Map-->
<controls:D3GrowthViz data="{/GrowthData}">
<controls:data>
<base:ManagedObject />
</controls:data>
</controls:D3GrowthViz>
</Page>
</mvc:View>
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel",
"jquery.sap.global"
], function (Controller, JSONModel, jQuery) {
"use strict";
return Controller.extend("ns.ZGROWTHMAP.controller.Main", {
onInit: function () {
this.getGemometryData();
this.getGrowthData();
},
getGemometryData: function () {
//Read data from JSON file
var oModel = new JSONModel();
var sPath = jQuery.sap.getModulePath("ns.ZGROWTHMAP", "/model/usatlas.json");
jQuery.ajax({
url: sPath,
dataType: "json",
async: false, // Synchronous loading for simplicity (not recommended in production)
success: function (oData) {
oModel.setData(oData);
},
error: function (err) {
console.log(err);
},
});
this.getView().setModel(oModel, "USAtlasData"); // Set the JSON data model
},
getGrowthData: function () {
//Read data from JSON file
var oModel = new JSONModel();
var sPath = jQuery.sap.getModulePath("ns.ZGROWTHMAP", "/model/WalmartGrowthData.json");
jQuery.ajax({
url: sPath,
dataType: "json",
async: false, // Synchronous loading for simplicity (not recommended in production)
success: function (oData) {
oModel.setData(oData);
},
error: function (err) {
console.log(err);
},
});
this.getView().setModel(oModel, "GrowthData"); // Set the JSON data model
}
});
});
sap.ui.define([
"sap/ui/core/Control",
"sap/ui/core/HTML",
"sap/ui/core/ResizeHandler",
"ns/ZGROWTHMAP/thirdparty/d3",
"ns/ZGROWTHMAP/thirdparty/topojson"
], function (Control, HTML, ResizeHandler) {
"use strict";
var dot;
let previousDate = -Infinity;
return Control.extend("ns.ZGROWTHMAP.controls.D3GrowthViz", {
metadata: {
aggregations: {
_html: {
type: "sap.ui.core.HTML",
multiple: false,
visibility: "hidden"
},
data: {
type: "sap.ui.base.ManagedObject"
}
}
},
init: function () {
this._sContainerId = this.getId() + "--container"
this.setAggregation("_html", new HTML({
content: "<svg id='" + this._sContainerId + "'></svg>"
}));
},
exit: function () {
ResizeHandler.deregister(this._sResizeHandlerId);
},
renderer: {
apiVersion: 2,
render: function (oRm, oControl) {
oRm.openStart('div', oControl);
oRm.openEnd();
oRm.openStart('p').openEnd();
oRm.close('p');
oRm.renderControl(oControl.getAggregation('_html'));
oRm.close('div');
}
},
_onResize: function () {
this._renderViz();
},
onBeforeRendering: function () {
ResizeHandler.deregister(this._sResizeHandlerId);
},
onAfterRendering: function () {
this._sResizeHandlerId = ResizeHandler.register(this, this._onResize.bind(this));
this._renderViz();
},
_parseDate: function (d3) {
return d3.utcParse("%m/%d/%Y");
//return d3.utcParse("%Y-%m-%dT%H:%M:%S.%LZ");
},
_projection: function (d3) {
return d3.geoAlbersUsa().scale(1280).translate([480, 300]);
},
_renderViz: function () {
const height = 620;
const width = this.$().width();
const svg = d3.select('#' + this._sContainerId);
svg.attr("height", height).attr("width", width);
svg.attr("viewBox", [0, 0, width, height]);
/** The following snippet is based on the Walmart's growth of Mike Bostock
https://observablehq.com/@d3/walmarts-growth?intent=fork*/
const us = this.getModel("USAtlasData").getData(); //get Geometry data
us.objects.lower48 = {
type: "GeometryCollection",
geometries: us.objects.states.geometries.filter(d => d.id !== "02" && d.id !== "15")
};
svg.append("path")
.datum(topojson.merge(us, us.objects.lower48.geometries))
.attr("fill", "#ddd")
.attr("d", d3.geoPath());
svg.append("path")
.datum(topojson.mesh(us, us.objects.lower48, (a, b) => a !== b))
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-linejoin", "round")
.attr("d", d3.geoPath());
const g = svg.append("g")
.attr("fill", "none")
.attr("stroke", "black");
const growthdata = this.getModel("GrowthData").getData(); //get growth data
const projection = that._projection(d3);
const parseDate = that._parseDate(d3);
const data = growthdata.map(d => {
const p = projection(d);
p.date = parseDate(d.date);
return p;
})
.sort((a, b) => a.date - b.date);
// const
dot = g.selectAll("circle")
.data(data)
.join("circle")
.attr("transform", d => `translate(${d})`);
svg.append("circle")
.attr("fill", "blue")
.attr("transform", `translate(${data[0]})`)
.attr("r", 3);
var dates = d3.utcWeek.every(2).range(...d3.extent(data, d => d.date));
var lastdate = d3.extent(data, d => d.date)[1];
dates.push(lastdate);
that.scrubber(dates, {
format: d3.utcFormat("%Y %b %-d"),
loop: false,
autoplay: false
});
},
updatechart: function (date) {
dot // enter
.filter(d => d.date > previousDate && d.date <= date)
.transition().attr("r", 3);
dot // exit
.filter(d => d.date <= previousDate && d.date > date)
.transition().attr("r", 0);
previousDate = date;
},
scrubber: function (values, {
format = value => value,
initial = 0,
direction = 1,
delay = null,
autoplay = true,
loop = true,
loopDelay = null,
alternate = false
} = {}) {
/** this logic is based on Scrubber by Mike
Bostock https://observablehq.com/@mbostock/scrubber */
const form = document.getElementById("container-ZGROWTHMAP---Main--scrubberfrm");
form.i.max = values.length - 1;
form.i.value = initial;
values = Array.from(values);
let frame = null;
let timer = null;
let interval = null;
function start() {
form.b.textContent = "Pause";
if (delay === null) frame = requestAnimationFrame(tick);
else interval = setInterval(tick, delay);
}
function stop() {
form.b.textContent = "Play";
if (frame !== null) cancelAnimationFrame(frame), frame = null;
if (timer !== null) clearTimeout(timer), timer = null;
if (interval !== null) clearInterval(interval), interval = null;
}
function running() {
return frame !== null || timer !== null || interval !== null;
}
function tick() {
if (form.i.valueAsNumber === (direction > 0 ? values.length - 1 : direction < 0 ? 0 : NaN)) {
if (!loop) return stop();
if (alternate) direction = -direction;
if (loopDelay !== null) {
if (frame !== null) cancelAnimationFrame(frame), frame = null;
if (interval !== null) clearInterval(interval), interval = null;
timer = setTimeout(() => (step(), start()), loopDelay);
return;
}
}
if (delay === null) frame = requestAnimationFrame(tick);
step();
}
function step() {
form.i.valueAsNumber = (form.i.valueAsNumber + direction + values.length) % values.length;
form.i.dispatchEvent(new CustomEvent("input", {
bubbles: true
}));
}
form.i.oninput = event => {
if (event && event.isTrusted && running()) stop();
form.value = values[form.i.valueAsNumber];
form.o.value = format(form.value, form.i.valueAsNumber, values);
this.updatechart(form.value);
};
form.b.onclick = () => {
if (running()) return stop();
direction = alternate && form.i.valueAsNumber === values.length - 1 ? -1 : 1;
form.i.valueAsNumber = (form.i.valueAsNumber + direction) % values.length;
form.i.dispatchEvent(new CustomEvent("input", {
bubbles: true
}));
start();
};
form.i.oninput();
if (autoplay) start();
else stop();
}
});
});
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
11 | |
10 | |
7 | |
7 | |
7 | |
5 | |
5 | |
4 | |
4 | |
4 |