Open lee2026 opened 4 years ago
Follow these steps:
Prepare your API request.
var template = `
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
body {
display: flex;
align-items: center;
justify-content: center;
background-color: #F5F5F5;
}
.container {
position: absolute;
z-index: -999;
height: 100vh;
width: 100vw;
}
.tooltip {
z-index: 99;
position: absolute;
font-size: 12px;
width: auto;
height: auto;
pointer-events: none;
background-color: white;
padding: 3px;
opacity: 1;
}
.point {
fill: #F5F5F5;
stroke: #F09D51;
stroke-width: 2px;
}
</style>
<div id="tree"></div>
<div class="container">
<script>
const treeData = {{{results}}};
var maxChildren = 0;
var treeLevelSpan = {0:1};
//finds the max number of nodes in a column
function getMaxChildrenSpan(input, level) {
var childrenLength = 0;
if (input.hasOwnProperty("children") && input["children"] != null && typeof input["children"] != undefined ) {
childrenLength = input.children.length;
}
let totalNumChildren = 0;
if (input.hasOwnProperty("children") && input["children"] != null && typeof input["children"] != undefined) {
for (let child of input.children) {
if (child.hasOwnProperty("children") && child["children"] != null && typeof child["children"] != undefined) {
getMaxChildrenSpan(child, level + 1)
}
}
}
if (level in treeLevelSpan) {
treeLevelSpan[level] += childrenLength;
}
else {
treeLevelSpan[level] = childrenLength;
}
}
getMaxChildrenSpan(treeData, 1);
let arrayOfLevelSpan = Object.values(treeLevelSpan)
let maxSpan = Math.max(...arrayOfLevelSpan);
// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left:90},
width = 960 - margin.left - margin.right,
height = 500 + (maxSpan*30) - margin.top - margin.bottom;
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate("
+ margin.left + "," + margin.top + ")");
var i = 0,
duration = 750,
root;
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) { return d.children; });
root.x0 = height / 2;
root.y0 = 0;
// Collapse after the second level
// root.children.forEach(collapse);
update(root);
// Collapse the node and all it's children
function collapse(d) {
if(d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}
function update(source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
//svg height dynamically changed by max number of open nodes in a column
treeLevelSpan = {};
getMaxChildrenSpan(root, 1);
arrayOfLevelSpan = Object.values(treeLevelSpan)
maxSpan = Math.max(...arrayOfLevelSpan);
height = 500 + (maxSpan*30) - margin.top - margin.bottom;
treeData = d3.tree().size([height, width])(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * width * 0.25 });
// ****************** Nodes section ***************************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function(d) {return d.id || (d.id = ++i); });
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
// Create hover tooltip
let tooltip = d3.select("#tree").append("div")
.attr("class", "tooltip")
// tooltip mouseover event handler
let tipMouseover = function(d) {
tooltip.html("Data Type: <br/>" + d.data.type)
.style("left", (d3.event.pageX + 40) + "px")
.style("top", (d3.event.pageY - 15) + "px")
.transition()
.duration(200) // ms
};
// tooltip mouseout event handler
let tipMouseout = function(d){
tooltip.transition()
.duration(300)
.style("opacity", 0);
};
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'point')
.attr('r', 1e-6)
.on("mouseover", tipMouseover)
.on("mouseout", tipMouseout)
.style("fill", function(d) {
return d._children ? "#F4B780" : "#fff";
})
// Add labels for the nodes
nodeEnter.append('text')
.attr("dy", ".35em")
.attr("x", function(d) {
return d.children || d._children ? -20 : 20;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.text(function(d) { return d.data.name; });
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x +")";//this
});
// Update the node attributes and style
nodeUpdate.select('circle.point')
.attr('r', 10)
.style("fill", function(d) {
return d._children ? "#F4B780" : "#F5F5F5";
})
.attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 0);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 0);
// ****************** links section ***************************
// Update the links...
var link = svg.selectAll('path.link')
.data(links, function(d) { return d.id; });
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr('d', function(d){
var o = {x: source.x0, y: source.y0}
return diagonal(o, o)
})
.style("fill", "none")
.style("stroke","#c5c5c5")
.style("stroke-width", "1px");
// UPDATE
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function(d){ return diagonal(d, d.parent) });
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function(d) {
var o = {x: source.x, y: source.y}
return diagonal(o, o)
})
.remove();
// Store the old positions for transition.
nodes.forEach(function(d){
d.x0 = d.x;
d.y0 = d.y;
});
// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(s, d) {
path = "M " + s.y +" " + s.x + " " +
"C " + (s.y + d.y)/2 + " " + s.x +", "
+ (s.y + d.y)/2 +" " + d.x + " , "
+ d.y + " " + d.x;
return path;
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
</script>
`;
/* DATA PARSING */
const response = pm.response.json();
function parseData(jsonInput) {
// Function that checks if object is a dictionary
function isDictionary(obj) {
if (typeof obj == "object" && !Array.isArray(obj) && obj !== null) {
return true;
} else {
return false;
}
}
// Declare and initialize the root node
const dataTree = {};
dataTree["name"] = "response";
dataTree["children"] = [];
dataTree["type"] = typeof(dataTree);
// Recursively reformats the json file
// See documentation for format
function restructure(input, arr) {
for (let node in input) {
const dict = {};
if (isDictionary(input[node])) {
dict.name = node;
dict.type = "dictionary"
dict.children = [];
restructure(input[node], dict.children);
} else {
if (Array.isArray(input[node])) {
dict.type = "array";
}
else if (input[node] === null) {
dict.type = "null";
}
else {
dict.type = typeof(input[node]);
}
dict.name = node;
}
arr.push(dict);
}
}
// Calls restructure on the first object in the response
restructure(jsonInput, dataTree.children);
return dataTree
}
/* FEED DATA INTO TEMPLATE */
pm.visualizer.set(template, {
// Template will receive stringified JSON
/* EDIT THIS LINE: Here we grab the first object from the reponse dictionary as all
objects in the dictionary have the same structure */
results: JSON.stringify(parseData(pm.response.json()))
});
Click Send
Go to the "Visualize" tab in the result window
Right click and click "Inspect Visualization"
Click on <svg ...>
and hit the "Delete" key of the keyboard
Click on <html>
(i.e. the root tag of the DOM) and right click to "Copy" -> "Copy Element"
Paste in a text editor (eg. Notepad, TextEdit, VSCode, etc) and save it as
Open the html file from the browser.
Step 5 - you can directly save generated html, go to Sources tab and right click on html file from left side and click on "Save as.."
Step 5 - you can directly save generated html, go to Sources tab and right click on html file from left side and click on "Save as.." That instruction saves the template only, not the HTML generated for the user. To save HTML generated for the user with that method, you need to embed the data you would send in the pm.visualizer.set() Tests script call into the template, then use the window.addEventListener('load', () => {...}) event to process the data in the same way as you would for pm.getData()
I'm working on a project where I am pulling a json file from an API server and displaying the data in a table form with your visualize tool. It would be nice to be able to save/print or export this display.
I found that the visualization actually creates an html file in the user's temp folder. So I am able to retrieve the visualization there and convert to PDF for my needs. However this is a bit cumbersome as the naming convention seems randomized to me and there doesn't seem to be a way to relate the file name to a collection or request.
Being able to save the visualization as different file formats would be very helpful, send to csv and ability to print would be nice. Thank you!
Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
Describe the solution you'd like A clear and concise description of what you want to happen.
Describe alternatives you've considered A clear and concise description of any alternative solutions or features you've considered.
Additional context Add any other context or screenshots about the feature request here.