@atomist/sdm-pack-aspect
Version:
an Atomist SDM Extension Pack for visualizing drift across an organization
269 lines • 10.9 kB
JavaScript
;
/*
* Copyright © 2019 Atomist, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const d3 = require("d3");
const NameOfThisLibrary = "SunburstYo";
/**
* Color palette for d3 to use
*/
const palette = [
"#56b06f",
"#3bbaa8",
"#35848c",
"#173d6d",
"#846473",
"#5F7186",
"#0d560d",
"#1f8045",
"#173d48",
];
// tslint:disable
function sunburst(workspaceId, data, pWidth, pHeight, options) {
const { perLevelDataElementIds, fieldsToDisplay } = options;
const minDiameterInPixels = 100;
const width = Math.max(pWidth || window.innerWidth, minDiameterInPixels), height = Math.max(pHeight || window.innerHeight, minDiameterInPixels), maxRadius = (Math.min(width, height) / 2) - 5;
const viewBoxSide = maxRadius * 2 + 10;
const x = d3.scaleLinear()
.range([0, 2 * Math.PI])
.clamp(true);
const y = d3.scaleSqrt()
.range([maxRadius * .1, maxRadius]);
const chooseColorFromString = d3.scaleOrdinal(palette);
const arc = d3.arc()
.startAngle((d) => x(d.x0))
.endAngle((d) => x(d.x1))
.innerRadius((d) => Math.max(0, y(d.y0)))
.outerRadius((d) => Math.max(0, y(d.y1)));
const middleArcLine = d => {
const halfPi = Math.PI / 2;
const angles = [x(d.x0) - halfPi, x(d.x1) - halfPi];
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const middleAngle = (angles[1] + angles[0]) / 2;
const invertDirection = middleAngle > 0 && middleAngle < Math.PI; // On lower quadrants write text ccw
if (invertDirection) {
angles.reverse();
}
const path = d3.path();
path.arc(0, 0, r, angles[0], angles[1], invertDirection);
return path.toString();
};
const textFits = d => {
const CHAR_SPACE = 6;
const deltaAngle = x(d.x1) - x(d.x0);
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const perimeter = r * deltaAngle;
return d.data.name.length * CHAR_SPACE < perimeter;
};
const svg = d3.select("#putSvgHere").append("svg")
.style("width", viewBoxSide + "px")
.attr("viewBox", `${-viewBoxSide / 2} ${-viewBoxSide / 2} ${viewBoxSide} ${viewBoxSide}`)
.on("click", focusOn); // Reset zoom on canvas click
// now work with the data
const d = data;
if (!d.tree || d.tree.children.length === 0) {
alert("No data");
return;
}
const root = d3.hierarchy(d.tree);
root.sum(d => d.size || 0); // sets a "value" property on each node
const slice = svg.selectAll("g.slice")
.data(d3.partition()(root).descendants());
slice.exit().remove(); // does this remove any extraneous ones?
const perLevelDataElements = perLevelDataElementIds.map(id => d3.select("#" + id)).filter(a => !!a);
const additionalDataElement = d3.select("#additionalDataAboutWhatYouClicked");
const newSlice = slice.enter()
.append("g").attr("class", "slice")
.on("click", d => {
d3.event.stopPropagation();
setFrozenLevelData(workspaceId, perLevelDataElements, d);
focusOn(d);
})
.on("mouseover", (d) => {
populatePerLevelData(perLevelDataElements, d);
populateAdditionalData(additionalDataElement, fieldsToDisplay, d);
});
// This is the hover text
newSlice.append("title")
.text(d => d.data.name);
newSlice.append("path")
.attr("class", "main-arc")
// I think this says, the last ring should use the same color as its parent
.style("fill", (d) => d.data.color || chooseColorFromString((d.children ? d : d.parent).data.name))
.attr("d", arc);
newSlice.append("path")
.attr("class", "hidden-arc")
.attr("id", (_, i) => `hiddenArc${i}`)
.attr("d", middleArcLine);
const text = newSlice.append("text")
.attr("display", d => textFits(d) ? null : "none");
// Add white contour
text.append("textPath")
.attr("class", "textOutline")
.attr("startOffset", "50%")
.attr("xlink:href", (_, i) => `#hiddenArc${i}`)
.text(d => d.data.name);
text.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", (_, i) => `#hiddenArc${i}`)
.text(d => d.data.name);
function focusOn(d = { x0: 0, x1: 1, y0: 0, y1: 1 }) {
// Reset to top-level if no data point specified
const transition = svg.transition()
.duration(750)
.tween("scale", () => {
const xd = d3.interpolate(x.domain(), [d.x0, d.x1]), yd = d3.interpolate(y.domain(), [d.y0, 1]);
return t => {
x.domain(xd(t));
y.domain(yd(t));
};
});
transition.selectAll("path.main-arc")
.attrTween("d", d => () => arc(d));
transition.selectAll("path.hidden-arc")
.attrTween("d", d => () => middleArcLine(d));
transition.selectAll("text")
.attrTween("display", d => () => textFits(d) ? null : "none");
moveStackToFront(d);
function moveStackToFront(elD) {
svg.selectAll(".slice").filter(d => d === elD)
.each(function (d) {
this.parentNode.appendChild(this); // move all parents to the end of the line
if (d.parent) {
moveStackToFront(d.parent);
}
});
}
}
}
exports.sunburst = sunburst;
function hasTags(data) {
return !!data.tags;
}
function populateAdditionalData(additionalDataElement, usefulFields, d) {
if (!additionalDataElement) {
return;
}
let content = "";
const data = d.data;
console.log("Properties on data: " + Object.getOwnPropertyNames(data).join(","));
if (hasTags(data)) {
content = "Tags: <ul>" + data.tags.map(t => `<li>${t.name}</li>`).join("") + "</ul>";
}
usefulFields.forEach(fieldname => {
if (data[fieldname] !== undefined) {
content += `<br />${fieldname}: ${data[fieldname]}`;
}
});
additionalDataElement.html(content);
}
function populatePerLevelData(perLevelDataElements, d) {
const namesUpTree = [d.data.name];
for (let place = d; place = place.parent; !!place) {
namesUpTree.push(place.data.name);
}
const namesDownTree = namesUpTree.reverse();
perLevelDataElements.forEach((e, i) => {
if (e.attr("class") === "frozenLevelData") {
return;
}
const value = namesDownTree[i] || "(various)";
e.html(value);
});
if (namesUpTree.length < perLevelDataElements.length) {
// if this isn't a leaf node, then
// the value property on the node is a count of the leaves under it.
// put that count in the last level data
perLevelDataElements[perLevelDataElements.length - 1].html("(" + d.value + " of them)");
}
}
function setFrozenLevelData(workspaceId, perLevelDataElements, d) {
const htmlUpTree = [formatLevelData(d.data)];
for (let place = d; place = place.parent; !!place) {
htmlUpTree.push(formatLevelData(place.data));
}
const htmlDownTree = htmlUpTree.reverse();
const dataId = d.data.id;
const levelCountAbove = htmlUpTree.length;
perLevelDataElements.forEach((e, i) => {
const className = i >= levelCountAbove ? "unfrozenLevelData" : "frozenLevelData";
e.attr("class", className);
if (!dataId || i !== (levelCountAbove - 1)) {
// no buttons
e.html(htmlDownTree[i]);
return;
}
e.html(htmlDownTree[i] + "<br/>" + htmlForSetIdeal(workspaceId, dataId) + htmlForNoteProblem(workspaceId, dataId));
});
}
function formatLevelData(data) {
console.log("tags: " + data.tags);
const urlToUse = data.viewUrl || data.url;
let tagList = "";
if (data.tags) {
tagList = "<ul>" + data.tags.map(t => `<li>${t.name}</li>`).join("") + "</ul>";
}
return urlToUse ? `<a href="${urlToUse}">${data.name}</a>${tagList}` : data.name;
}
function htmlForSetIdeal(workspaceId, dataId) {
return `<button id="setIdeal" onclick="${NameOfThisLibrary}.postSetIdeal('${workspaceId}','${dataId}')">
Set as ideal
</button><label for="setIdeal" id="setIdealLabel" class="nothingToSay"> </label>`;
}
function htmlForNoteProblem(workspaceId, dataId) {
return `<button id="noteProblem" onclick="${NameOfThisLibrary}.postNoteProblem('${workspaceId}','${dataId}')">
Note as problem
</button><label for="noteProblem" id="noteProblemLabel" class="nothingToSay"> </label>`;
}
function postSetIdeal(workspaceId, fingerprintId) {
const postUrl = `../../api/v1/${workspaceId}/ideal/${fingerprintId}`;
const labelElement = document.getElementById("setIdealLabel");
fetch(postUrl, { method: "PUT" }).then(response => {
if (response.ok) {
labelElement.textContent = `Ideal set`;
labelElement.setAttribute("class", "success");
labelElement.setAttribute("display", "static");
}
else {
labelElement.textContent = "Failed to set. consult the server logaments";
labelElement.setAttribute("class", "error");
}
}, e => {
labelElement.textContent = "Network error";
labelElement.setAttribute("class", "error");
});
}
exports.postSetIdeal = postSetIdeal;
function postNoteProblem(workspaceId, fingerprintId) {
const postUrl = `../../api/v1/${workspaceId}/problem/${fingerprintId}`;
const labelElement = document.getElementById("noteProblemLabel");
fetch(postUrl, { method: "PUT" }).then(response => {
if (response.ok) {
labelElement.textContent = `Problem noted`;
labelElement.setAttribute("class", "success");
labelElement.setAttribute("display", "static");
}
else {
labelElement.textContent = "Failed to set. consult the server logaments";
labelElement.setAttribute("class", "error");
}
}, e => {
labelElement.textContent = "Network error";
labelElement.setAttribute("class", "error");
});
}
exports.postNoteProblem = postNoteProblem;
//# sourceMappingURL=sunburstScript.js.map