postmanlabs / postman-app-support

Postman is an API platform for building and using APIs. Postman simplifies each step of the API lifecycle and streamlines collaboration so you can create better APIs—faster.
https://www.postman.com
5.82k stars 838 forks source link

Ability to export or print/save Visualizations #7766

Open lee2026 opened 4 years ago

lee2026 commented 4 years ago

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.

p3jitnath commented 3 years ago

Follow these steps:

Step 0

Prepare your API request.

Step 1

Insert this script in Tests part of the request window

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()))
});

Step 2

Click Send

Step 3

Go to the "Visualize" tab in the result window

Step 4

Right click and click "Inspect Visualization"

Step 5

Click on <svg ...> and hit the "Delete" key of the keyboard

Step 6

Click on <html> (i.e. the root tag of the DOM) and right click to "Copy" -> "Copy Element"

Step 7

Paste in a text editor (eg. Notepad, TextEdit, VSCode, etc) and save it as .html

Step 8

Open the html file from the browser.

prcdeveloper commented 2 years ago

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.."

1Bryan1 commented 6 months ago

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()