block by micahstubbs 01d888adcefb92b8842e50bde9f95316

Download SVG Button

Full Screen

an example that uses SVG Crowbar to extract an SVG from the page and download it as an .svg file

inspired by this stackoverflow answer

an alternate method that works with Firefox is demonstrated in @currankelleher‘s nice Download SVG from Data URL example


A d3js ROC Chart. Might be useful for evaluating models.

the data shown are from models that predict various tennis stats. curious about the source data? browse through the R script that @ilanthedataman uses to generate the data.

check out the animated version

inspired by the Interactive ROC Curve bl.ock from ilanman

index.html

<!DOCTYPE html>
<meta charset='utf-8'>
<head>
  <link href='https://fonts.googleapis.com/css?family=Roboto:400,700,300,100,900|Open+Sans:400,300,700,600,800' rel='stylesheet' type='text/css'>
  <script src='https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.22.1/babel.min.js'></script>
  <script src='https://d3js.org/d3.v4.min.js' charset='utf-8'></script>
  <script src='rocChart.js'></script>
</head>
<style>
body { 
  font-size: 12px; 
  font-family: 'Open Sans';
}

path { 
    stroke-width: 3;
    fill: none;
    opacity: .7;
}

.axis path,
.axis line {
    fill: none;
    stroke: grey;
    stroke-width: 2;
    shape-rendering: crispEdges;
    opacity: 1;
}

.d3-tip {
    font-family: Verdana;
    background: rgba(0, 0, 0, 0.8);
    padding: 8px;
    color: #fff; 
    z-index: 5070;
  }

.download { 
  background: darkgray; 
  color: #FFF; 
  font-weight: 600; 
  border: 2px solid #FFF; 
  border-radius: 8px;
  padding: 6px; 
  margin:4px;
  position: fixed;
  left: 187px;
}
</style>
<body>
  <div id='roc'> </div>
  <button class="download" onClick="(function () { var e = document.createElement('script'); e.setAttribute('src', 'svg-crowbar.js'); e.setAttribute('class', 'svg-crowbar'); document.body.appendChild(e); })();">
    <big></big> Download SVG
  </button>
<script>
const margin = {top: 30, right: 61, bottom: 70, left: 61}; 
const width = 470 - margin.left - margin.right;
const height = 450 - margin.top - margin.bottom;

// fpr for 'false positive rate'
// tpr for 'true positive rate'

const rocChartOptions = {
  'margin': margin,
  'width': width,
  'height': height,
  'interpolationMode': 'basis',
  'fpr': 'X',
  'tprVariables': [
    {
      'name': 'BPC',
      'label': 'Break Points'
    },
    {
      'name': 'WNR',
      'label': 'Winners'
    },
    {
      'name': 'FSP',
      'label': 'First Serve %',
    },
    {
      'name': 'NPW',
      'label': 'Net Points Won'
    }
  ], 
  'animate': false,
  'smooth': true
}

d3.json('data.json', (error, data) => rocChart('#roc', data, rocChartOptions))
</script>
</body>

rocChart.js

function rocChart(id, data, options) {
  // set default configuration
  const cfg = {
    'margin': {top: 30, right: 20, bottom: 70, left: 61},
    'width': 470,
    'height': 450,
    'interpolationMode': 'basis',
    'ticks': undefined,
    'tickValues': [0, .1, .25, .5, .75, .9, 1],
    'fpr': 'fpr',
    'tprVariables': [{
      'name': 'tpr0',
    }], 
    'animate': true
  };

  //Put all of the options into a variable called cfg
  if('undefined' !== typeof options){
    Object.keys(options).forEach(key => {
      if('undefined' !== typeof options[key]){ cfg[key] = options[key]; }
    })
  }

  const tprVariables = cfg['tprVariables'];
  // if values for labels are not specified
  // set the default values for the labels to the corresponding
  // true positive rate variable name
  tprVariables.forEach((d, i) => {
    if('undefined' === typeof d.label){
      d.label = d.name;
    }

  })

  console.log('tprVariables', tprVariables);


  const interpolationMode = cfg['interpolationMode'];
  const fpr = cfg['fpr'];
  const width = cfg['width'];
  const height = cfg['height'];
  const animate = cfg['animate'];

  const format = d3.format('.2');
  const aucFormat = d3.format('.4r');

  const x = d3.scaleLinear().range([0, width]);
  const y = d3.scaleLinear().range([height, 0]);
  const color = d3.scaleOrdinal()
    .range(d3.schemeCategory10); // d3.scaleOrdinal().range(['steelblue', 'red', 'green', 'purple']);

  const xAxis = d3.axisTop()
    .scale(x)
    .tickSizeOuter(0);

  const yAxis = d3.axisRight()
    .scale(y)
    .tickSizeOuter(0);

  // set the axis ticks based on input parameters,
  // if ticks or tickValues are specified
  if('undefined' !== typeof cfg['ticks']) {
    xAxis.ticks(cfg['ticks']);
    yAxis.ticks(cfg['ticks']);
  } else if ('undefined' !== typeof cfg['tickValues']) {
    xAxis.tickValues(cfg['tickValues']);
    yAxis.tickValues(cfg['tickValues']);
  } else {
    xAxis.ticks(5);
    yAxis.ticks(5);
  }

  // apply the format to the ticks we chose
  xAxis.tickFormat(format);
  yAxis.tickFormat(format);

  // a function that returns a line generator
  function curve(data, tpr) {

     const lineGenerator = d3.line()
      .curve(d3.curveBasis)
      .x(d => x(d[fpr]))
      .y(d => y(d[tpr]));

    return lineGenerator(data);
  }

  // a function that returns an area generator
  function areaUnderCurve(data, tpr) {

    const areaGenerator = d3.area()
      .x(d => x(d[fpr]))
      .y0(height)
      .y1(d => y(d[tpr]));

    return areaGenerator(data);
  }

  const svg = d3.select('#roc')
    .append('svg')
      .attr('width', width + margin.left + margin.right)
      .attr('height', height + margin.top + margin.bottom)
      .append('g')
        .attr('transform', `translate(${margin.left},${margin.top})`);

  x.domain([0, 1]);
  y.domain([0, 1]);

  svg.append('g')
      .attr('class', 'x axis')
      .attr('transform', `translate(0,${height})`)
      .call(xAxis)
      .append('text')            
        .attr('x', width / 2)
        .attr('y', 40 )
        .style('text-anchor', 'middle')
        .text('False Positive Rate')

  const xAxisG = svg.select('g.x.axis');

  // draw the top boundary line
  xAxisG.append('line')
    .attr({
      'x1': -1,
      'x2': width + 1,
      'y1': -height,
      'y2': -height
    });

  // draw a bottom boundary line over the existing
  // x-axis domain path to make even corners
  xAxisG.append('line')
    .attr({
      'x1': -1,
      'x2': width + 1,
      'y1': 0,
      'y2': 0
    });


  // position the axis tick labels below the x-axis
  xAxisG.selectAll('.tick text')
    .attr('transform', `translate(0,${25})`);

  // hide the y-axis ticks for 0 and 1
  xAxisG.selectAll('g.tick line')
    .style('opacity', d => // if d is an integer
  (d % 1 === 0 ? 0 : 1));

  svg.append('g')
    .attr('class', 'y axis')
    .call(yAxis)
    .append('text')            
      .attr('transform', 'rotate(-90)')
      .attr('y', -35)
      // manually configured so that the label is centered vertically 
      .attr('x', 0 - height/1.56)  
      .style('font-size','12px')              
      .style('text-anchor', 'left')
      .text('True Positive Rate');

  yAxisG = svg.select('g.y.axis');

  // add the right boundary line
  yAxisG.append('line')
    .attr({
      'x1': width,
      'x2': width,
      'y1': 0,
      'y2': height
    })

  // position the axis tick labels to the right of 
  // the y-axis and
  // translate the first and the last tick labels
  // so that they are right aligned
  // or even with the 2nd digit of the decimal number
  // tick labels
  yAxisG.selectAll('g.tick text')
    .attr('transform', d => {
      if(d % 1 === 0) { // if d is an integer
        return `translate(${-22},0)`;
      } else if((d*10) % 1 === 0) { // if d is a 1 place decimal
        return `translate(${-32},0)`;
      } else {
        return `translate(${-42},0)`;
      }
  })

  // hide the y-axis ticks for 0 and 1
  yAxisG.selectAll('g.tick line')
    .style('opacity', d => // if d is an integer
  (d % 1 === 0 ? 0 : 1));

  // draw the random guess line
  svg.append('line') 
    .attr('class', 'curve')         
    .style('stroke', 'black')
    .attr('x1', 0)
    .attr('x2', width)
    .attr('y1', height)
    .attr('y2', 0)
    .style('stroke-width', 2)
    .style('stroke-dasharray', '8')
    .style('opacity', 0.4);

  // draw the ROC curves
  function drawCurve(data, tpr, stroke){

    svg.append('path')
      .attr('class', 'curve')
      .style('stroke', stroke)
      .attr('d', curve(data, tpr))
      .on('mouseover', d => {

        const areaID = `#${tpr}Area`;
        svg.select(areaID)
          .style('opacity', .4)

        const aucText = `.${tpr}text`; 
        svg.selectAll(aucText)
          .style('opacity', .9)
      })
      .on('mouseout', () => {
        const areaID = `#${tpr}Area`;
        svg.select(areaID)
          .style('opacity', 0)

        const aucText = `.${tpr}text`; 
        svg.selectAll(aucText)
          .style('opacity', 0)


      });
  }

  // draw the area under the ROC curves
  function drawArea(data, tpr, fill) {
    svg.append('path')
      .attr('class', 'area')
      .attr('id', `${tpr}Area`)
      .style('fill', fill)
      .style('opacity', 0)
      .attr('d', areaUnderCurve(data, tpr))
  }

  function drawAUCText(auc, tpr, label) {

    svg.append('g')
      .attr('class', `${tpr}text`)
      .style('opacity', 0)
      .attr('transform', `translate(${.5*width},${.79*height})`)
      .append('text')
        .text(label)
        .style({
          'fill': 'white',
          'font-size': 18
        });

    svg.append('g')
      .attr('class', `${tpr}text`)
      .style('opacity', 0)
      .attr('transform', `translate(${.5*width},${.84*height})`)
      .append('text')
        .text(`AUC = ${aucFormat(auc)}`)
        .style({
          'fill': 'white',
          'font-size': 18
        });

  }

  // calculate the area under each curve
  tprVariables.forEach(d => {
    const tpr = d.name;
    const points = generatePoints(data, fpr, tpr);
    const auc = calculateArea(points);
    d['auc'] = auc;
  })

  console.log('tprVariables', tprVariables);

  // draw curves, areas, and text for each 
  // true-positive rate in the data
  tprVariables.forEach((d, i) => {
    console.log('drawing the curve for', d.label)
    console.log('color(', i, ')', color(i));
    const tpr = d.name;
    drawArea(data, tpr, color(i))
    drawCurve(data, tpr, color(i));
    drawAUCText(d.auc, tpr, d.label);
  })

  ///////////////////////////////////////////////////
  ////// animate through areas for each curve ///////
  ///////////////////////////////////////////////////

  if(animate && animate !== 'false') {
    //sort tprVariables ascending by AUC
    const tprVariablesAscByAUC = tprVariables.sort((a, b) => a.auc - b.auc);

    console.log('tprVariablesAscByAUC', tprVariablesAscByAUC);
    
    for(let i = 0; i < tprVariablesAscByAUC.length; i++) {
      areaID = `#${tprVariablesAscByAUC[i]['name']}Area`;
      svg.select(areaID)
        .transition()
          .delay(2000 * (i+1))
          .duration(250)
          .style('opacity', .4)
        .transition()
          .delay(2000 * (i+2))
          .duration(250)
          .style('opacity', 0)

      textClass = `.${tprVariablesAscByAUC[i]['name']}text`;
      svg.selectAll(textClass)
        .transition()
          .delay(2000 * (i+1))
          .duration(250)
          .style('opacity', .9)
        .transition()
          .delay(2000 * (i+2))
          .duration(250)
          .style('opacity', 0)
    }  
  }

  ///////////////////////////////////////////////////
  ///////////////////////////////////////////////////
  ///////////////////////////////////////////////////

  function generatePoints(data, x, y) {
    const points = [];
    data.forEach(d => {
      points.push([ Number(d[x]), Number(d[y]) ])
    })
    return points;
  }

  // numerical integration
  function calculateArea(points) {
    let area = 0.0;
    const length = points.length;
    if (length <= 2) {
      return area;
    }
    points.forEach((d, i) => {
      const x = 0;
      const y = 1;

      if('undefined' !== typeof points[i-1]){
        area += (points[i][x] - points[i-1][x]) * (points[i-1][y] + points[i][y]) / 2;
      }
    });
    return area;
  }
} // rocChart




















svg-crowbar.js

(function() {
  var doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';

  window.URL = (window.URL || window.webkitURL);

  var body = document.body;

  var prefix = {
    xmlns: "http://www.w3.org/2000/xmlns/",
    xlink: "http://www.w3.org/1999/xlink",
    svg: "http://www.w3.org/2000/svg"
  }

  initialize();

  function initialize() {
    var documents = [window.document],
        SVGSources = [];
        iframes = document.querySelectorAll("iframe"),
        objects = document.querySelectorAll("object");

    [].forEach.call(iframes, function(el) {
      try {
        if (el.contentDocument) {
          documents.push(el.contentDocument);
        }
      } catch(err) {
        console.log(err)
      }
    });

    [].forEach.call(objects, function(el) {
      try {
        if (el.contentDocument) {
          documents.push(el.contentDocument);
        }
      } catch(err) {
        console.log(err)
      }
    });

    documents.forEach(function(doc) {
      var styles = getStyles(doc);
      var newSources = getSources(doc, styles);
      // because of prototype on NYT pages
      for (var i = 0; i < newSources.length; i++) {
        SVGSources.push(newSources[i]);
      };
    })
    if (SVGSources.length > 1) {
      createPopover(SVGSources);
    } else if (SVGSources.length > 0) {
      download(SVGSources[0]);
    } else {
      alert("The Crowbar couldn’t find any SVG nodes.");
    }
  }

  function createPopover(sources) {
    cleanup();

    sources.forEach(function(s1) {
      sources.forEach(function(s2) {
        if (s1 !== s2) {
          if ((Math.abs(s1.top - s2.top) < 38) && (Math.abs(s1.left - s2.left) < 38)) {
            s2.top += 38;
            s2.left += 38;
          }
        }
      })
    });

    var buttonsContainer = document.createElement("div");
    body.appendChild(buttonsContainer);

    buttonsContainer.setAttribute("class", "svg-crowbar");
    buttonsContainer.style["z-index"] = 1e7;
    buttonsContainer.style["position"] = "absolute";
    buttonsContainer.style["top"] = 0;
    buttonsContainer.style["left"] = 0;



    var background = document.createElement("div");
    body.appendChild(background);

    background.setAttribute("class", "svg-crowbar");
    background.style["background"] = "rgba(255, 255, 255, 0.7)";
    background.style["position"] = "fixed";
    background.style["left"] = 0;
    background.style["top"] = 0;
    background.style["width"] = "100%";
    background.style["height"] = "100%";

    sources.forEach(function(d, i) {
      var buttonWrapper = document.createElement("div");
      buttonsContainer.appendChild(buttonWrapper);
      buttonWrapper.setAttribute("class", "svg-crowbar");
      buttonWrapper.style["position"] = "absolute";
      buttonWrapper.style["top"] = (d.top + document.body.scrollTop) + "px";
      buttonWrapper.style["left"] = (document.body.scrollLeft + d.left) + "px";
      buttonWrapper.style["padding"] = "4px";
      buttonWrapper.style["border-radius"] = "3px";
      buttonWrapper.style["color"] = "white";
      buttonWrapper.style["text-align"] = "center";
      buttonWrapper.style["font-family"] = "'Helvetica Neue'";
      buttonWrapper.style["background"] = "rgba(0, 0, 0, 0.8)";
      buttonWrapper.style["box-shadow"] = "0px 4px 18px rgba(0, 0, 0, 0.4)";
      buttonWrapper.style["cursor"] = "move";
      buttonWrapper.textContent =  "SVG #" + i + ": " + (d.id ? "#" + d.id : "") + (d.class ? "." + d.class : "");

      var button = document.createElement("button");
      buttonWrapper.appendChild(button);
      button.setAttribute("data-source-id", i)
      button.style["width"] = "150px";
      button.style["font-size"] = "12px";
      button.style["line-height"] = "1.4em";
      button.style["margin"] = "5px 0 0 0";
      button.textContent = "Download";

      button.onclick = function(el) {
        // console.log(el, d, i, sources)
        download(d);
      };

    });

  }

  function cleanup() {
    var crowbarElements = document.querySelectorAll(".svg-crowbar");

    [].forEach.call(crowbarElements, function(el) {
      el.parentNode.removeChild(el);
    });
  }


  function getSources(doc, styles) {
    var svgInfo = [],
        svgs = doc.querySelectorAll("svg");

    styles = (styles === undefined) ? "" : styles;

    [].forEach.call(svgs, function (svg) {

      svg.setAttribute("version", "1.1");

      var defsEl = document.createElement("defs");
      svg.insertBefore(defsEl, svg.firstChild); //TODO   .insert("defs", ":first-child")
      // defsEl.setAttribute("class", "svg-crowbar");

      var styleEl = document.createElement("style")
      defsEl.appendChild(styleEl);
      styleEl.setAttribute("type", "text/css");


      // removing attributes so they aren't doubled up
      svg.removeAttribute("xmlns");
      svg.removeAttribute("xlink");

      // These are needed for the svg
      if (!svg.hasAttributeNS(prefix.xmlns, "xmlns")) {
        svg.setAttributeNS(prefix.xmlns, "xmlns", prefix.svg);
      }

      if (!svg.hasAttributeNS(prefix.xmlns, "xmlns:xlink")) {
        svg.setAttributeNS(prefix.xmlns, "xmlns:xlink", prefix.xlink);
      }

      var source = (new XMLSerializer()).serializeToString(svg).replace('</style>', '<![CDATA[' + styles + ']]></style>');
      var rect = svg.getBoundingClientRect();
      svgInfo.push({
        top: rect.top,
        left: rect.left,
        width: rect.width,
        height: rect.height,
        class: svg.getAttribute("class"),
        id: svg.getAttribute("id"),
        childElementCount: svg.childElementCount,
        source: [doctype + source]
      });
    });
    return svgInfo;
  }

  function download(source) {
    var filename = "untitled";

    if (source.id) {
      filename = source.id;
    } else if (source.class) {
      filename = source.class;
    } else if (window.document.title) {
      filename = window.document.title.replace(/[^a-z0-9]/gi, '-').toLowerCase();
    }

    var url = window.URL.createObjectURL(new Blob(source.source, { "type" : "text\/xml" }));

    var a = document.createElement("a");
    body.appendChild(a);
    a.setAttribute("class", "svg-crowbar");
    a.setAttribute("download", filename + ".svg");
    a.setAttribute("href", url);
    a.style["display"] = "none";
    a.click();

    setTimeout(function() {
      window.URL.revokeObjectURL(url);
    }, 10);
  }

  function getStyles(doc) {
    var styles = "",
        styleSheets = doc.styleSheets;

    if (styleSheets) {
      for (var i = 0; i < styleSheets.length; i++) {
        processStyleSheet(styleSheets[i]);
      }
    }

    function processStyleSheet(ss) {
      if (ss.cssRules) {
        for (var i = 0; i < ss.cssRules.length; i++) {
          var rule = ss.cssRules[i];
          if (rule.type === 3) {
            // Import Rule
            processStyleSheet(rule.styleSheet);
          } else {
            // hack for illustrator crashing on descendent selectors
            if (rule.selectorText) {
              if (rule.selectorText.indexOf(">") === -1) {
                styles += "\n" + rule.cssText;
              }
            }
          }
        }
      }
    }
    return styles;
  }

})();