SVG рендеринг в неправильный формат - PullRequest
0 голосов
/ 14 марта 2019

У меня есть круговая диаграмма с надписями и легендами, которая прекрасно выглядит в SVG, но как только я использую canvg для преобразования ее в canvas, некоторые форматы пропадают / ошибаются. SVG: svg image

Canvas:

Canvas image

Я уже включил CSS, чтобы применить все настройки CSS, но по-прежнему формат не совпадает.

Есть идеи? Это ошибка (canvg) или я делаю s.th. неправильно?

var columns = ['data11', 'data2', 'data347', 'data40098'];
var data = [150, 250, 300, 50];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;

/**
 * global C3Styles object
 */
var C3Styles = null;

var legendData = [];
var sumTotal = 0

//prepare pie data

var columnData = [];
var columnNames = {};
for (i = 0; i < columns.length; i++) {
  columnData.push([columns[i]].concat(data[i]));
  var val = (Array.isArray(data[i])) ? data[i].reduce(function(pv, cv) {
    return pv + cv;
  }, 0) : data[i];
  sumTotal += val;
  legendData.push({
    id: columns[i],
    value: val,
    ratio: 0.0
  });
}
legendData.forEach(function(el, i) {
  el.ratio = el.value / sumTotal
  columnNames[el.id] = [el.id, d3.format(",.0f")(el.value), d3.format(",.1%")(el.ratio)].join(';');
});

var chart = c3.generate({
  bindto: d3.select('#chart'),
  data: {
    columns: [
      [columns[0]].concat(data[0])
    ],
    names: columnNames,
    type: 'pie',
  },
  legend: {
    position: 'right',
    show: true
  },
  pie: {
    label: {
      threshold: 0.001,
      format: function(value, ratio, id) {
        return [id, d3.format(",.0f")(value), "[" + d3.format(",.1%")(ratio) + "]"].join(';');
      }
    }
  },
  color: {
    pattern: colors
  },
  onrendered: function() {
    redrawLabelBackgrounds();
    redrawLegend();
  }
});



function addLabelBackground(index) {
  /*d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-')+".c3-chart-arc")
    .insert("rect", "text")
    .style("fill", colors[index]);*/
    
  var p = d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-') + ".c3-chart-arc");
  var g = p.append("g");
  g.append("rect")
    .style("fill", colors[index]);
  g.append(function() {
  	return p.select("text").remove().node();
  });
}

for (var i = 0; i < columns.length; i++) {
  if (i > 0) {

    setTimeout(function(column) {
      chart.load({
        columns: [
          columnData[column],
        ]
      });
      //chart.data.names(columnNames[column])
      addLabelBackground(column);

    }, (i * 5000 / columnData.length), i);
  } else {
    addLabelBackground(i);
  }
}

function redrawLegend() {

  d3.select('#chart').selectAll(".c3-legend-item > text").each(function() {
    // get d3 node
    var legendItem = d3.select(this);
    var legendItemNode = legendItem.node();
    //check if label is drawn
    if (legendItemNode) {
      if (legendItemNode.childElementCount === 0 && legendItemNode.innerHTML.length > 0) {
        //build data
        var data = legendItemNode.innerHTML.split(';');
        legendItem.text("");
        //TODO format legend dynamically depending on text
        legendItem.append("tspan")
          .text(data[0] + ": ")
          .attr("class", "id-row")
          .attr("text-anchor", "start");
        legendItem.append("tspan")
          .text(data[1] + " = ")
          .attr("class", "value-row")
          .attr("x", 160)
          .attr("text-anchor", "end");

        legendItem.append("tspan")
          .text(data[2])
          .attr("class", "ratio-row")
          .attr("x", 190)
          .attr("text-anchor", "end");

      }
    }
  });
  d3.select('#chart').selectAll(".c3-legend-item > rect").each(function() {
    var legendItem = d3.select(this);
    legendItem.attr("width", 190);
  });


}

function redrawLabelBackgrounds() {
  //for all label texts drawn yet
  //for all label texts drawn yet
  d3.select('#chart').selectAll(".c3-chart-arc > g > text").each(function(v) {
    // get d3 node
    var label = d3.select(this);
    var labelNode = label.node();
    //check if label is drawn
    if (labelNode) {
      if (labelNode.childElementCount === 0 && labelNode.innerHTML.length > 0) {
        //build data
        var data = labelNode.innerHTML.split(';');
        label.text("");
        data.forEach(function(i, n) {
          label.append("tspan")
            .text(i)
            .attr("dy", (n === 0) ? 0 : "1.2em")
            .attr("x", 0)
            .attr("text-anchor", "middle");
        }, label);
      }
      //check if element is visible
      if (d3.select(labelNode.parentNode).style("display") !== 'none') {

        //get pos of the label text
        var pos = label.attr("transform").match(/-?\d+(\.\d+)?/g);
        if (pos) {
          // TODO: mofify the pos of the text
          //            pos[0] = (pos[0]/h*90000);
          //            pos[1] = (pos[1]/h*90000);
          // remove dy and move label
          //d3.select(this).attr("dy", 0);
          //d3.select(this).attr("transform", "translate(" + pos[0] + "," + pos[1] + ")");

          //get surrounding box of the label
          var bbox = labelNode.getBBox();

          //now draw and move the rects
          d3.select(labelNode.parentNode).select("rect")
            .attr("transform", "translate(" + (pos[0] - (bbox.width + padding) / 2) +
              "," + (pos[1] - bbox.height / labelNode.childElementCount) + ")")
            .attr("width", bbox.width + padding)
            .attr("height", bbox.height + padding);
        }
      }
    }
  });
}



document.getElementById("exportButton").onclick = function() {
  exportChartToImage();
};

function exportChartToImage() {
  var createImagePromise = new Promise(function(resolve, reject) {
    var images = [];
    d3.selectAll('svg').each(function() {
      if (this.parentNode) {
        images.push(getSvgImage(this.parentNode, true));
      }
    });
    if (images.length > 0)
      resolve(images);
    else
      reject(images);
  });
  createImagePromise.then(function(images) {
      /*images.forEach(function(img, n) {
        img.toBlob(function(blob) {
          saveAs(blob, "image_" + (n + 1) + ".png");
        });
      });*/
    })
    .catch(function(error) {
      throw error;
    });
};

/**
 * Converts a SVG-Chart to a canvas and returns it.
 */
function getSvgImage(svgContainer) {
  var svgEl = d3.select(svgContainer).select('svg').node();
  var svgCopyEl = svgEl.cloneNode(true);


  if (!svgCopyEl)
    return;

  d3.select("#svgCopyEl").selectAll("*").remove();
  d3.select("#svgCopyEl").node().append(svgCopyEl);

  //apply all CSS styles to SVG

  /* taken from https://gist.github.com/aendrew/1ad2eed6afa29e30d52e#file-exportchart-js
  	      and changed from, angular to D3 functions
        */
  /* Take styles from CSS and put as inline SVG attributes so that Canvg
								 can properly parse them. */

  if (!C3Styles) {
    var chartStyle;
    // Get rules from c3.css
    var styleSheets = document.styleSheets;
    for (var i = 0; i <= styleSheets.length - 1; i++) {
      if (styleSheets[i].href && (styleSheets[i].href.indexOf('c3.min.css') !== -1 || styleSheets[i].href.indexOf('c3.css') !== -1)) {
        try {
          if (styleSheets[i].rules !== undefined) {
            chartStyle = styleSheets[i].rules;
          } else {
            chartStyle = styleSheets[i].cssRules;
          }
          break;
        }
        //Note that SecurityError exception is specific to Firefox.
        catch (e) {
          if (e.name == 'SecurityError') {
            console.log("SecurityError. Cant read: " + styleSheets[i].href);
            continue;
          }
        }
      }
      if (chartStyle !== null && chartStyle !== undefined) {
        C3Styles = {};
        var selector;
        // Inline apply all the CSS rules as inline
        for (i = 0; i < chartStyle.length; i++) {
          if (chartStyle[i].type === 1) {
            selector = chartStyle[i].selectorText;
            var styleDec = chartStyle[i].style;
            for (var s = 0; s < styleDec.length; s++) {
              C3Styles[styleDec[s]] = styleDec[styleDec[s]];
            }

          }

        }
      }
    }
  }
  if (C3Styles) {
    d3.select(svgCopyEl).selectAll('.c3:not(.c3-chart):not(path)').style(C3Styles);
  }
  // SVG doesn't use CSS visibility and opacity is an attribute, not a style property. Change hidden stuff to "display: none"
  d3.select(svgCopyEl).selectAll('*')
    .filter(function(d) {
      return d && d.style && (d.style('visibility') === 'hidden' || d.style('opacity') === '0');
    })
    .style('display', 'none');
  //fix weird back fill
  d3.select(svgCopyEl).selectAll("path").attr("fill", "none");
  //fix no axes
  d3.select(svgCopyEl).selectAll("path.domain").attr("stroke", "black");
  //fix no tick
  d3.select(svgCopyEl).selectAll(".tick line").attr("stroke", "black");
  //apply svg text fill, set color
	d3.select(svgCopyEl).selectAll("text:not(.c3-empty):not(.c3-axis)").attr("opacity", 1);
  
  var canvasComputed = d3.select("#canvasComputed").node();


  // transform SVG to canvas using external canvg
  canvg(canvasComputed, new XMLSerializer().serializeToString(svgCopyEl));

  return canvasComputed;
}
.c3-chart-arc.c3-target text {
  color: white;
  fill: white;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.js"></script>
<!-- Required to convert named colors to RGB -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.4/rgbcolor.min.js"></script>
<!-- Optional if you want blur -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stackblur-canvas/1.4.1/stackblur.min.js"></script>
<!-- Main canvg code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.5/canvg.js"></script>
<script src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h4>
SVG
</h4>
<div id="chart" class "c3">

</div>
<h4>
copy SVG
</h4>
<div id ="svgCopyEl">
</div>
<div>
<h4>
canvas
</h4>
<canvas id="canvasComputed"></canvas>

</div>

<button type="button" id="exportButton">
export to Canvas
</button>

1 Ответ

0 голосов
/ 21 марта 2019

В основном это была комбинация недостатка знаний и ошибки.

Прежде всего, все соответствующие стили CSS должны быть встроены при использовании canvg.Во-вторых, была ошибка в canvg с использованием tspans и text-anchors.

Вот (более или менее) рабочий код:

var columns = ['data11', 'data2', 'data347', 'data40098'];
var data = [150, 250, 300, 50];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;

/**
 * global C3Styles object
 */
var C3Styles = null;

var legendData = [];
var sumTotal = 0

//prepare pie data

var columnData = [];
var columnNames = {};
for (i = 0; i < columns.length; i++) {
  columnData.push([columns[i]].concat(data[i]));
  var val = (Array.isArray(data[i])) ? data[i].reduce(function(pv, cv) {
    return pv + cv;
  }, 0) : data[i];
  sumTotal += val;
  legendData.push({
    id: columns[i],
    value: val,
    ratio: 0.0
  });
}
legendData.forEach(function(el, i) {
  el.ratio = el.value / sumTotal
  columnNames[el.id] = [el.id, d3.format(",.0f")(el.value), d3.format(",.1%")(el.ratio)].join(';');
});

var chart = c3.generate({
  bindto: d3.select('#chart'),
  data: {
    columns: [
      [columns[0]].concat(data[0])
    ],
    names: columnNames,
    type: 'pie',
  },
  legend: {
    position: 'right',
    show: true
  },
  pie: {
    label: {
      threshold: 0.001,
      format: function(value, ratio, id) {
        return [id, d3.format(",.0f")(value), "[" + d3.format(",.1%")(ratio) + "]"].join(';');
      }
    }
  },
  color: {
    pattern: colors
  },
  onrendered: function() {
    redrawLabelBackgrounds();
    redrawLegend();
  }
});



function addLabelBackground(index) {
  /*d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-')+".c3-chart-arc")
    .insert("rect", "text")
    .style("fill", colors[index]);*/

  var p = d3.select('#chart').select("g.c3-target-" + columns[index].replace(/\W+/g, '-') + ".c3-chart-arc");
  var g = p.append("g");
  g.append("rect")
    .style("fill", colors[index]);
  g.append(function() {
    return p.select("text").remove().node();
  });
}

for (var i = 0; i < columns.length; i++) {
  if (i > 0) {

    setTimeout(function(column) {
      chart.load({
        columns: [
          columnData[column],
        ]
      });
      //chart.data.names(columnNames[column])
      addLabelBackground(column);

    }, (i * 5000 / columnData.length), i);
  } else {
    addLabelBackground(i);
  }
}

function redrawLegend() {

  d3.select('#chart').selectAll(".c3-legend-item > text").each(function() {
    // get d3 node
    var legendItem = d3.select(this);
    var legendItemNode = legendItem.node();
    //check if label is drawn
    if (legendItemNode) {
      if (legendItemNode.childElementCount === 0 && legendItemNode.innerHTML.length > 0) {
        //build data
        var data = legendItemNode.innerHTML.split(';');
        legendItem.text("");
        //TODO format legend dynamically depending on text
        legendItem.append("tspan")
          .text(data[0] + ": ")
          .attr("class", "id-row")
          .attr("text-anchor", "start");
        legendItem.append("tspan")
          .text(data[1] + " = ")
          .attr("class", "value-row")
          .attr("x", 160)
          .attr("text-anchor", "end");

        legendItem.append("tspan")
          .text(data[2])
          .attr("class", "ratio-row")
          .attr("x", 190)
          .attr("text-anchor", "end");

      }
    }
  });
  d3.select('#chart').selectAll(".c3-legend-item > rect").each(function() {
    var legendItem = d3.select(this);
    legendItem.attr("width", 190);
  });


}

function redrawLabelBackgrounds() {
  //for all label texts drawn yet
  //for all label texts drawn yet
  d3.select('#chart').selectAll(".c3-chart-arc > g > text").each(function(v) {
    // get d3 node
    var label = d3.select(this);
    var labelNode = label.node();
    //check if label is drawn
    if (labelNode) {
    var bbox = labelNode.getBBox();
    var labelTextHeight = bbox.height;
      if (labelNode.childElementCount === 0 && labelNode.innerHTML.length > 0) {
        //build data
        var data = labelNode.innerHTML.split(';');
        label.html('')
                    .attr("dominant-baseline", "central")
                    .attr("text-anchor", "middle");
        data.forEach(function(i, n) {
          label.append("tspan")
            .text(i)
            .attr("dy", (n === 0) ? 0 : "1.2em")
            .attr("x", 0);
        }, label);
      }
      //check if element is visible
      if (d3.select(labelNode.parentNode).style("display") !== 'none') {

        //get pos of the label text
        var pos = label.attr("transform").match(/-?\d+(\.\d+)?/g);
        if (pos) {
          // TODO: mofify the pos of the text
          //            pos[0] = (pos[0]/h*90000);
          //            pos[1] = (pos[1]/h*90000);
          // remove dy and move label
          //d3.select(this).attr("dy", 0);
          //d3.select(this).attr("transform", "translate(" + pos[0] + "," + pos[1] + ")");

          //get surrounding box of the label
          bbox = labelNode.getBBox();

          //now draw and move the rects
          d3.select(labelNode.parentNode).select("rect")
            .attr("transform", "translate(" + (pos[0] - bbox.width  / 2 - padding) +
              "," + (pos[1] - labelTextHeight/2 - padding)+")")
            .attr("width", bbox.width + 2*padding)
            .attr("height", bbox.height + 2*padding);
        }
      }
    }
  });
}



document.getElementById("exportButton").onclick = function() {
  exportChartToImage();
};

function exportChartToImage() {
  var createImagePromise = new Promise(function(resolve, reject) {
    var images = [];
    d3.selectAll('svg').each(function() {
      if (this.parentNode) {
        images.push(getSvgImage(this.parentNode, true));
      }
    });
    if (images.length > 0)
      resolve(images);
    else
      reject(images);
  });
  createImagePromise.then(function(images) {
      /*images.forEach(function(img, n) {
        img.toBlob(function(blob) {
          saveAs(blob, "image_" + (n + 1) + ".png");
        });
      });*/
    })
    .catch(function(error) {
      throw error;
    });
};

/**
 * Converts a SVG-Chart to a canvas and returns it.
 */
function getSvgImage(svgContainer) {
  var svgEl = d3.select(svgContainer).select('svg').node();
  var svgCopyEl = svgEl.cloneNode(true);


  if (!svgCopyEl)
    return;

  d3.select("#svgCopyEl").selectAll("*").remove();
  d3.select("#svgCopyEl").node().append(svgCopyEl); //.transition().duration(0);

  //apply C3 CSS styles to SVG

  // SVG doesn't use CSS visibility and opacity is an attribute, not a style property. Change hidden stuff to "display: none"
  d3.select("#svgCopyEl").selectAll('*')
    .filter(function(d) {
      return d && d.style && (d.style('visibility') === 'hidden' || d.style('opacity') === '0');
    })
    .style('display', 'none');

  d3.select("#svgCopyEl").selectAll('.c3-chart path')
    .filter(function(d) {
      return d && d.style('fill') === 'none';
    })
    .attr('fill', 'none');

  d3.select("#svgCopyEl").selectAll('.c3-chart path')
    .filter(function(d) {
      return d && d.style('fill') !== 'none';
    })
    .attr('fill', function(d) {
      return d.style('fill');
    });

  //set c3 default font
  d3.select("#svgCopyEl").selectAll('.c3 svg')
    .style('font', 'sans-serif')
    .style('font-size', '10px');

  //set c3 legend font
  d3.select("#svgCopyEl").selectAll('.c3-legend-item > text')
    .style('font', 'sans-serif')
    .style('font-size', '12px');
  d3.select("#svgCopyEl").selectAll('.c3-legend-item > text > tspan')
    .style('font', 'sans-serif')
    .style('font-size', '12px');

  //set c3 arc shapes
  d3.select("#svgCopyEl").selectAll('.c3-chart-arc path,rect')
    .style('stroke', '#fff');
  d3.select("#svgCopyEl").selectAll('.c3-chart-arc text')
    .attr('fill', '#fff')
    .style('font', 'sans-serif')
    .style('font-size', '13px');

  //fix weird back fill
  d3.select("#svgCopyEl").selectAll("path").attr("fill", "none");
  //fix no axes
  d3.select("#svgCopyEl").selectAll("path.domain").attr("stroke", "black");
  //fix no tick
  d3.select("#svgCopyEl").selectAll(".tick line").attr("stroke", "black");

  var canvasComputed = d3.select("#canvasComputed").node();


  // transform SVG to canvas using external canvg
  canvg(canvasComputed, new XMLSerializer().serializeToString(svgCopyEl));

  return canvasComputed;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.js"></script> -->
<!-- Required to convert named colors to RGB -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.4/rgbcolor.min.js"></script>
<!-- Optional if you want blur -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stackblur-canvas/1.4.1/stackblur.min.js"></script>
<!-- Main canvg code -->
<script src="https://cdn.jsdelivr.net/npm/canvg@2.0.0-beta.1/dist/browser/canvg.min.js"></script>
<script src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
 
<h4>
  SVG
</h4>
<div id="chart" class "c3">

</div>
<h4>
  copy SVG
</h4>
<div id="svgCopyEl">
</div>
<div>
  <h4>
    canvas
  </h4>
  <canvas id="canvasComputed"></canvas>

</div>

<button type="button" id="exportButton">
export to Canvas
</button>
...