jsPDF + html2PDF - how do I append html code to footer - html

I know how to append my footer with text but now I need to append dynamic HTML code. I can't find anything in the documentation or in any forum.
const page_margin_left = 20;
const page_margin_right = 15;
const page_margin_bottom = 20;
const page_margin_top = 5;
html2pdf()
.set({
margin: [page_margin_top, page_margin_left, page_margin_bottom, page_margin_right],
filename: 'test.pdf',
image: { type: 'jpeg', quality: 1 },
html2canvas: { dpi: 192, scale: 2, letterRendering: true, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
//pageBreak: {mode: ['avoid-all', 'css'], avoid: ['.pi-row']},
})
.from(print_target_container[0].innerHTML)
.toPdf().get('pdf').then(function (pdf) {
var totalPages = pdf.internal.getNumberOfPages();
var pageHeight = pdf.internal.pageSize.height || pdf.internal.pageSize.getHeight();
var pageWidth = pdf.internal.pageSize.width || pdf.internal.pageSize.getWidth();
var footerText = offer_template_page_footer;
for (i = 1; i <= totalPages; i++) {
if (i > 1) {
pdf.setPage(i);
pdf.setFontSize(8);
pdf.setTextColor(150);
pdf.line(25, pageHeight - 15, pageWidth - page_margin_right, pageHeight - 15); // horizontal line x1, y1, x2, y2, style
pdf.text(i + ' (' + totalPages + ')\n' + footerText, pageWidth / 2, pageHeight - 10, {align: 'center'});
//pdf.text(i + ' (' + totalPages + ')\n' + offer_template_page_footer, pdf.internal.pageSize.getWidth() / 2, pdf.internal.pageSize.getHeight() - 8);
}
}
}).save();

Related

How can I display the global UTM grid on a map in openlayers?

I want to display the global UTM Grid on a map in openlayers. There is no proj definition for the whole system, just the individual zones. For the projection of my map I want to use EPSG:3857.Im using the Ol-Ext graticule to use a different projection for map and grid as that is needed in my project. Is there any way to do this with the ol-ext graticule or do i have to develop a custom solution?
You could define a projection which combines all the UTM projections into one (e.g. by adding 1000000 to the x coordinate at each increase in zone) while still using the appropriate UTM transforms for the zone.
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.6.1/css/ol.css" type="text/css">
<style>
html, body, .map {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
</style>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.6.1/build/ol.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.7.5/proj4.js"></script>
<link rel="stylesheet" href="https://viglino.github.io/ol-ext/dist/ol-ext.css" type="text/css">
<script src="https://viglino.github.io/ol-ext/dist/ol-ext.js"></script>
</head>
<body>
<div id="map" class="map"></div>
<script type="text/javascript">
const utmProjs = [];
for (let zone = 1; zone <= 60; zone++) {
const code = "EPSG:" + (32600 + zone);
proj4.defs(code, "+proj=utm +zone=" + zone + " +ellps=WGS84 +datum=WGS84 +units=m +no_defs");
ol.proj.proj4.register(proj4);
utmProjs[zone] = ol.proj.get(code);
}
const llProj = ol.proj.get("EPSG:4326");
const midpointX = 500000;
const width = midpointX * 2;
function ll2utm(ll) {
const lon = (((ll[0] % 360) + 540) % 360) - 180; // normalise any wrapx
const lat = ll[1];
const zone = Math.floor((180 + lon) / 6) + 1;
const zoneCoord = ol.proj.transform([lon, lat], llProj, utmProjs[zone]);
return [zoneCoord[0] + (zone - 1) * width, zoneCoord[1]];
}
function utm2ll(coord) {
const zone = Math.floor(coord[0] / width) % 60 + 1;
const c0 = coord[0] % width;
const c1 = coord[1];
const ll = ol.proj.transform([c0, c1], utmProjs[zone], llProj);
if (Math.floor((180 + ll[0]) / 6) + 1 != zone) {
ll[0] = (zone - (c0 < midpointX ? 1 : 0)) * 6 - 180;
}
return ll;
}
const UTM = new ol.proj.Projection({
code: 'GlobalUTM',
units: 'm',
extent: [0, -10000000, 60 * width, 10000000]
});
ol.proj.addProjection(UTM);
ol.proj.addCoordinateTransforms(
llProj,
UTM,
ll2utm,
utm2ll
);
const map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.fromLonLat([37.41, 8.82]),
zoom: 4
})
});
const viewProj = map.getView().getProjection();
ol.proj.addCoordinateTransforms(
viewProj,
UTM,
function(coord) {
return ll2utm(ol.proj.toLonLat(coord, viewProj));
},
function(coord) {
return ol.proj.fromLonLat(utm2ll(coord), viewProj);
},
);
map.addControl(
new ol.control.Graticule({
projection: UTM,
step: 1000,
stepCoord: 1
})
);
</script>
</body>
</html>
If the ol-ext graticule had an intervals option similar to that in the OpenLayers graticule instead of drawing lines at random multiples of the step setting using
intervals: [1000, 5000, 10000, 50000, 100000, 500000]
would make the zone boundaries clear while still showing details when zoomed in. In the absence of that option you could potentially use one graticule with a step setting of 1000km with a wider stroke to highlight and label the zones, and another with a step of 1km for detail within the zones. Unfortunately the ol-ext graticule simply draws straight lines between the intersection points instead of calculating the correct curve, so the 1000km y values lines which would also be produced would be misplaced relative to the more accurate 1km detail. It can however be used the show the labels, but because it lacks separate label formatters for x and y coordinates an extra x offset is needed to be able to distinguish between x and y coordinates. In an EPSG:3857 projection the zone boundary lines can be easily added in a separate vector layer.
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.6.1/css/ol.css" type="text/css">
<style>
html, body, .map {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
</style>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.6.1/build/ol.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.7.5/proj4.js"></script>
<link rel="stylesheet" href="https://viglino.github.io/ol-ext/dist/ol-ext.css" type="text/css">
<script src="https://viglino.github.io/ol-ext/dist/ol-ext.js"></script>
</head>
<body>
<div id="map" class="map"></div>
<script type="text/javascript">
const utmProjs = [];
for (let zone = 1; zone <= 60; zone++) {
const code = "EPSG:" + (32600 + zone);
proj4.defs(code, "+proj=utm +zone=" + zone + " +ellps=WGS84 +datum=WGS84 +units=m +no_defs");
ol.proj.proj4.register(proj4);
utmProjs[zone] = ol.proj.get(code);
}
const llProj = ol.proj.get("EPSG:4326");
const midpointX = 500000;
const width = midpointX * 2;
const xOffset = 100 * 60 * width;
function ll2utm(ll) {
//const world = Math.floor((ll[0] + 180) / 360);
const lon = (((ll[0] % 360) + 540) % 360) - 180; // normalise any wrapx
const lat = ll[1];
const zone = Math.floor((180 + lon) / 6) + 1;
const zoneCoord = ol.proj.transform([lon, lat], llProj, utmProjs[zone]);
const coord = [xOffset + zoneCoord[0] + (zone - 1) * width, zoneCoord[1]];
//coord[0] += world * 60 * width;
return coord;
}
function utm2ll(coord) {
//const world = Math.floor((coord[0] - xOffset) / (60 * width));
const zone = Math.floor(coord[0] / width) % 60 + 1;
const c0 = coord[0] % width;
const c1 = coord[1];
const ll = ol.proj.transform([c0, c1], utmProjs[zone], llProj);
if (Math.floor((180 + ll[0]) / 6) + 1 != zone) {
ll[0] = (zone - (c0 < midpointX ? 1 : 0)) * 6 - 180;
}
//ll[0] += world * 360;
return ll;
}
const UTM = new ol.proj.Projection({
code: 'GlobalUTM',
units: 'm',
extent: [xOffset, -10000000, xOffset + 60 * width, 10000000],
//global: true
});
ol.proj.addProjection(UTM);
ol.proj.addCoordinateTransforms(
llProj,
UTM,
ll2utm,
utm2ll
);
const map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.fromLonLat([37.41, 8.82]),
zoom: 4
})
});
const viewProj = map.getView().getProjection();
ol.proj.addCoordinateTransforms(
viewProj,
UTM,
function(coord) {
return ll2utm(ol.proj.toLonLat(coord, viewProj));
},
function(coord) {
return ol.proj.fromLonLat(utm2ll(coord), viewProj);
},
);
const features = [
new ol.Feature(
new ol.geom.LineString([[-180, 0], [180, 0]]).transform(llProj, viewProj)
)
];
for (let i = -180; i <= 180; i += 6) {
features.push(
new ol.Feature(
new ol.geom.LineString([[i, -85], [i, 85]]).transform(llProj, viewProj)
)
);
};
map.addLayer(
new ol.layer.Vector({
source: new ol.source.Vector({
features: features
}),
style: new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'black',
width: 3
}),
})
})
);
map.addControl(
new ol.control.Graticule({
projection: UTM,
step: 1000,
stepCoord: 1,
formatCoord: function(coord) {
if (coord % width == 0) {
return '';
} else {
return (20000 + (coord % width) / 1000).toString().slice(2);
}
},
style: new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'black',
width: 1
}),
fill: new ol.style.Fill({
color: 'white'
}),
text: new ol.style.Text({
stroke: new ol.style.Stroke({
color: 'white',
width: 3
}),
fill: new ol.style.Fill({
color: 'black'
}),
font: 'bold 12px Arial, Helvetica, Helvetica, sans-serif',
})
})
})
);
map.addControl(
new ol.control.Graticule({
projection: UTM,
step: width,
stepCoord: 1,
formatCoord: function(coord) {
if (coord < 0) {
return 'S' + (20 + coord / width).toString().slice(1);
} else if (coord < xOffset) {
return 'N' + (20 + coord / width).toString().slice(1);
} else {
return 'Z' + (100 + Math.floor(coord / width) % 60 + 1).toString().slice(1);
}
},
style: new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'transparent',
width: 1
}),
fill: new ol.style.Fill({
color: 'transparent'
}),
text: new ol.style.Text({
stroke: new ol.style.Stroke({
color: 'white',
width: 3
}),
fill: new ol.style.Fill({
color: 'black'
}),
font: 'bold 15px Arial, Helvetica, Helvetica, sans-serif',
})
})
})
);
</script>
</body>
</html>

simplest way to update data in VegaEmbed

I made a small graph to show some data from a bluetooth device.
I used a sample I found for VegaEmbed, it was all very easy.
But the sample uses a timer to get data, so even if there is no data the dataset will be changed. What is the simples way to update data inside VegaEmbed from another part of the website ?
I cannot call res.view.change('table', changeSet).run(); from outside VegaEmbded..
Here is snappshot of the code :
(the function handleDataChanged is called when there is bluetooth data.)
function handleDataChanged(event) {
var value = event.target.value;
value = value.buffer ? value : new DataView(value);
let result = {};
let index = 1;
datapointx = value.getInt16(index, /*littleEndian=*/false);
console.log('X: ' + value.getInt16(index, /*littleEndian=*/false));
index += 2;
datapointy = value.getInt16(index, /*littleEndian=*/true);
console.log('Y: ' + value.getInt16(index, /*littleEndian=*/false));
index += 2;
datapointz = value.getInt16(index, /*littleEndian=*/true);
console.log('Z: ' + value.getInt16(index, /*littleEndian=*/false));
index += 2;
}
</script>
<script>
document.querySelector('button').addEventListener('click', function() {
onButtonClick();
});
</script>
<script type="text/javascript">
var vlSpec = {
$schema: 'https://vega.github.io/schema/vega-lite/v3.json',
data: {name: 'table'},
width: 400,
mark: 'line',
encoding: {
x: {field: 'x', type: 'quantitative', scale: {zero: false}},
y: {field: 'y', type: 'quantitative'},
color: {field: 'category', type: 'nominal'}
}
};
vegaEmbed('#chart', vlSpec).then(function(res) {
/**
* Generates a new tuple with random walk.
*/
function newGenerator() {
var counter = -1;
var previousY = [5, 5, 5];
return function() {
counter++;
var newVals = previousY.map(function(v, c)
{
console.log('c = ' + c);
var yval = 0;
if (c == 0)
yval = datapointx;
if (c == 1)
yval = datapointy;
if (c == 2)
yval = datapointz;
return {
x: counter,
// y: v + Math.round(Math.random() * 10 - c * 3),
y: yval,
category: c
};
});
previousY = newVals.map(function(v) {
return v.y;
});
return newVals;
};
}
var valueGenerator = newGenerator();
var minimumX = -100;
window.setInterval(function() {
minimumX++;
var changeSet = vega
.changeset()
.insert(valueGenerator())
.remove(function(t) {
return t.x < minimumX;
});
res.view.change('table', changeSet).run();
}, 100);
});
</script>
The simplest way to update data in an existing vega-lite chart is to use a streaming data model. There is an example in the Vega-Lite documentation here: https://vega.github.io/vega-lite/tutorials/streaming.html

How to display count value of each category of Y axis in a graph using Morris.Bar function?

I am displaying data in graphical format and I am using Morris.Bar function in my cshtml page. The Y axis has categories namely: Performance, Maintainability, Others, Portability, Reliability and Security.
I am using the following function:
Morris.Bar({
element: 'category-bar-chart',
data: JSON.parse(''[{"y":"Performance","a":23},{"y":"Maintainability","a":106},{"y":"Others","a":98},{"y":"Portability","a":27},{"y":"Reliability","a":87},{"y":"Security","a":14}]'),'),
xkey: 'y',
ykeys: ['a'],
labels: ['Violation'],
xLabelAngle: 43,
});
But currently it is not displaying the value for each category at the top of each bar. May I know what property I can add to get the values at the top of each bar?
There's no built-in parameter to display the value on top of each Bar.
But you can extend Morris to add this parameter. I've extended Morris, adding a labelTop property for Bar charts. If set to true, a label with the value is added on top of each Bar (I restricted this property for non stacked Bar, as there's multiple values with stacked Bar).
Usage:
labelTop: true
Please try the snippet below to see a working example:
(function() {
var $, MyMorris;
MyMorris = window.MyMorris = {};
$ = jQuery;
MyMorris = Object.create(Morris);
MyMorris.Bar.prototype.defaults["labelTop"] = false;
MyMorris.Bar.prototype.drawLabelTop = function(xPos, yPos, text) {
var label;
return label = this.raphael.text(xPos, yPos, text).attr('font-size', this.options.gridTextSize).attr('font-family', this.options.gridTextFamily).attr('font-weight', this.options.gridTextWeight).attr('fill', this.options.gridTextColor);
};
MyMorris.Bar.prototype.drawSeries = function() {
var barWidth, bottom, groupWidth, idx, lastTop, left, leftPadding, numBars, row, sidx, size, spaceLeft, top, ypos, zeroPos;
groupWidth = this.width / this.options.data.length;
numBars = this.options.stacked ? 1 : this.options.ykeys.length;
barWidth = (groupWidth * this.options.barSizeRatio - this.options.barGap * (numBars - 1)) / numBars;
if (this.options.barSize) {
barWidth = Math.min(barWidth, this.options.barSize);
}
spaceLeft = groupWidth - barWidth * numBars - this.options.barGap * (numBars - 1);
leftPadding = spaceLeft / 2;
zeroPos = this.ymin <= 0 && this.ymax >= 0 ? this.transY(0) : null;
return this.bars = (function() {
var _i, _len, _ref, _results;
_ref = this.data;
_results = [];
for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) {
row = _ref[idx];
lastTop = 0;
_results.push((function() {
var _j, _len1, _ref1, _results1;
_ref1 = row._y;
_results1 = [];
for (sidx = _j = 0, _len1 = _ref1.length; _j < _len1; sidx = ++_j) {
ypos = _ref1[sidx];
if (ypos !== null) {
if (zeroPos) {
top = Math.min(ypos, zeroPos);
bottom = Math.max(ypos, zeroPos);
} else {
top = ypos;
bottom = this.bottom;
}
left = this.left + idx * groupWidth + leftPadding;
if (!this.options.stacked) {
left += sidx * (barWidth + this.options.barGap);
}
size = bottom - top;
if (this.options.verticalGridCondition && this.options.verticalGridCondition(row.x)) {
this.drawBar(this.left + idx * groupWidth, this.top, groupWidth, Math.abs(this.top - this.bottom), this.options.verticalGridColor, this.options.verticalGridOpacity, this.options.barRadius, row.y[sidx]);
}
if (this.options.stacked) {
top -= lastTop;
}
this.drawBar(left, top, barWidth, size, this.colorFor(row, sidx, 'bar'), this.options.barOpacity, this.options.barRadius);
_results1.push(lastTop += size);
if (this.options.labelTop && !this.options.stacked) {
label = this.drawLabelTop((left + (barWidth / 2)), top - 10, row.y[sidx]);
textBox = label.getBBox();
_results.push(textBox);
}
} else {
_results1.push(null);
}
}
return _results1;
}).call(this));
}
return _results;
}).call(this);
};
}).call(this);
Morris.Bar({
element: 'category-bar-chart',
data: [
{ "y": "Performance", "a": 23 },
{ "y": "Maintainability", "a": 106 },
{ "y": "Others", "a": 98 },
{ "y": "Portability", "a": 27 },
{ "y": "Reliability", "a": 87 },
{ "y": "Security", "a": 14 }],
xkey: 'y',
ykeys: ['a'],
labels: ['Violation'],
xLabelAngle: 43,
labelTop: true
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/raphael/2.1.0/raphael-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.min.js"></script>
<link href="//cdnjs.cloudflare.com/ajax/libs/morris.js/0.5.1/morris.css" rel="stylesheet" />
<div id="category-bar-chart"></div>

Chart.js dynamic bar width

I have a requirement to render a set of time series data of contiguous blocks.
I need to describe a series of bars which could span many hours, or just minutes, with their own Y value.
I'm not sure if ChartJS is what I should be using for this, but I have looked at extending the Bar type, but it seems very hard coded for each bar to be the same width. The Scale Class internally is used for labels, chart width etc, not just the bars themselves.
I am trying to achieve something like this that works in Excel: http://peltiertech.com/variable-width-column-charts/
Has anyone else had to come up with something similar?
I found I needed to do this and the answer by #potatopeelings was great, but out of date for version 2 of Chartjs. I did something similar by creating my own controller/chart type via extending bar:
//controller.barw.js
module.exports = function(Chart) {
var helpers = Chart.helpers;
Chart.defaults.barw = {
hover: {
mode: 'label'
},
scales: {
xAxes: [{
type: 'category',
// Specific to Bar Controller
categoryPercentage: 0.8,
barPercentage: 0.9,
// grid line settings
gridLines: {
offsetGridLines: true
}
}],
yAxes: [{
type: 'linear'
}]
}
};
Chart.controllers.barw = Chart.controllers.bar.extend({
/**
* #private
*/
getRuler: function() {
var me = this;
var scale = me.getIndexScale();
var options = scale.options;
var stackCount = me.getStackCount();
var fullSize = scale.isHorizontal()? scale.width : scale.height;
var tickSize = fullSize / scale.ticks.length;
var categorySize = tickSize * options.categoryPercentage;
var fullBarSize = categorySize / stackCount;
var barSize = fullBarSize * options.barPercentage;
barSize = Math.min(
helpers.getValueOrDefault(options.barThickness, barSize),
helpers.getValueOrDefault(options.maxBarThickness, Infinity));
return {
fullSize: fullSize,
stackCount: stackCount,
tickSize: tickSize,
categorySize: categorySize,
categorySpacing: tickSize - categorySize,
fullBarSize: fullBarSize,
barSize: barSize,
barSpacing: fullBarSize - barSize,
scale: scale
};
},
/**
* #private
*/
calculateBarIndexPixels: function(datasetIndex, index, ruler) {
var me = this;
var scale = ruler.scale;
var options = scale.options;
var isCombo = me.chart.isCombo;
var stackIndex = me.getStackIndex(datasetIndex);
var base = scale.getPixelForValue(null, index, datasetIndex, isCombo);
var size = ruler.barSize;
var dataset = me.chart.data.datasets[datasetIndex];
if(dataset.weights) {
var total = dataset.weights.reduce((m, x) => m + x, 0);
var perc = dataset.weights[index] / total;
var offset = 0;
for(var i = 0; i < index; i++) {
offset += dataset.weights[i] / total;
}
var pixelOffset = Math.round(ruler.fullSize * offset);
var base = scale.isHorizontal() ? scale.left : scale.top;
base += pixelOffset;
size = Math.round(ruler.fullSize * perc);
size -= ruler.categorySpacing;
size -= ruler.barSpacing;
}
base -= isCombo? ruler.tickSize / 2 : 0;
base += ruler.fullBarSize * stackIndex;
base += ruler.categorySpacing / 2;
base += ruler.barSpacing / 2;
return {
size: size,
base: base,
head: base + size,
center: base + size / 2
};
},
});
};
Then you need to add it to your chartjs instance like this:
import Chart from 'chart.js'
import barw from 'controller.barw'
barw(Chart); //add plugin to chartjs
and finally, similar to the other answer, the weights of the bar widths need to be added to the data set:
var data = {
labels: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
datasets: [
{
label: "My First dataset",
fillColor: "rgba(220,220,220,0.5)",
strokeColor: "rgba(220,220,220,0.8)",
highlightFill: "rgba(220,220,220,0.7)",
highlightStroke: "rgba(220,220,220,1)",
data: [65, 59, 80, 30, 56, 65, 40],
weights: [1, 0.9, 1, 2, 1, 4, 0.3]
},
]
};
This will hopefully get someone onto the right track. What I have certainly isn't perfect, but if you make sure you have the right number of weight to data points, you should be right.
Best of luck.
This is based on the #Shane's code, I just posted to help, since is a common question.
calculateBarIndexPixels: function (datasetIndex, index, ruler) {
const options = ruler.scale.options;
const range = options.barThickness === 'flex' ? computeFlexCategoryTraits(index, ruler, options) : computeFitCategoryTraits(index, ruler, options);
const barSize = range.chunk;
const stackIndex = this.getStackIndex(datasetIndex, this.getMeta().stack);
let center = range.start + range.chunk * stackIndex + range.chunk / 2;
let size = range.chunk * range.ratio;
let start = range.start;
const dataset = this.chart.data.datasets[datasetIndex];
if (dataset.weights) {
//the max weight should be one
size = barSize * dataset.weights[index];
const meta = this.chart.controller.getDatasetMeta(0);
const lastModel = index > 0 ? meta.data[index - 1]._model : null;
//last column takes the full bar
if (lastModel) {
//start could be last center plus half of last column width
start = lastModel.x + lastModel.width / 2;
}
center = start + size * stackIndex + size / 2;
}
return {
size: size,
base: center - size / 2,
head: center + size / 2,
center: center
};
}
For Chart.js you can create a new extension based on the bar class to do this. It's a bit involved though - however most of it is a copy paste of the bar type library code
Chart.types.Bar.extend({
name: "BarAlt",
// all blocks that don't have a comment are a direct copy paste of the Chart.js library code
initialize: function (data) {
// the sum of all widths
var widthSum = data.datasets[0].data2.reduce(function (a, b) { return a + b }, 0);
// cumulative sum of all preceding widths
var cumulativeSum = [ 0 ];
data.datasets[0].data2.forEach(function (e, i, arr) {
cumulativeSum.push(cumulativeSum[i] + e);
})
var options = this.options;
// completely rewrite this class to calculate the x position and bar width's based on data2
this.ScaleClass = Chart.Scale.extend({
offsetGridLines: true,
calculateBarX: function (barIndex) {
var xSpan = this.width - this.xScalePaddingLeft;
var x = this.xScalePaddingLeft + (cumulativeSum[barIndex] / widthSum * xSpan) - this.calculateBarWidth(barIndex) / 2;
return x + this.calculateBarWidth(barIndex);
},
calculateBarWidth: function (index) {
var xSpan = this.width - this.xScalePaddingLeft;
return (xSpan * data.datasets[0].data2[index] / widthSum);
}
});
this.datasets = [];
if (this.options.showTooltips) {
Chart.helpers.bindEvents(this, this.options.tooltipEvents, function (evt) {
var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : [];
this.eachBars(function (bar) {
bar.restore(['fillColor', 'strokeColor']);
});
Chart.helpers.each(activeBars, function (activeBar) {
activeBar.fillColor = activeBar.highlightFill;
activeBar.strokeColor = activeBar.highlightStroke;
});
this.showTooltip(activeBars);
});
}
this.BarClass = Chart.Rectangle.extend({
strokeWidth: this.options.barStrokeWidth,
showStroke: this.options.barShowStroke,
ctx: this.chart.ctx
});
Chart.helpers.each(data.datasets, function (dataset, datasetIndex) {
var datasetObject = {
label: dataset.label || null,
fillColor: dataset.fillColor,
strokeColor: dataset.strokeColor,
bars: []
};
this.datasets.push(datasetObject);
Chart.helpers.each(dataset.data, function (dataPoint, index) {
datasetObject.bars.push(new this.BarClass({
value: dataPoint,
label: data.labels[index],
datasetLabel: dataset.label,
strokeColor: dataset.strokeColor,
fillColor: dataset.fillColor,
highlightFill: dataset.highlightFill || dataset.fillColor,
highlightStroke: dataset.highlightStroke || dataset.strokeColor
}));
}, this);
}, this);
this.buildScale(data.labels);
// remove the labels - they won't be positioned correctly anyway
this.scale.xLabels.forEach(function (e, i, arr) {
arr[i] = '';
})
this.BarClass.prototype.base = this.scale.endPoint;
this.eachBars(function (bar, index, datasetIndex) {
// change the way the x and width functions are called
Chart.helpers.extend(bar, {
width: this.scale.calculateBarWidth(index),
x: this.scale.calculateBarX(index),
y: this.scale.endPoint
});
bar.save();
}, this);
this.render();
},
draw: function (ease) {
var easingDecimal = ease || 1;
this.clear();
var ctx = this.chart.ctx;
this.scale.draw(1);
Chart.helpers.each(this.datasets, function (dataset, datasetIndex) {
Chart.helpers.each(dataset.bars, function (bar, index) {
if (bar.hasValue()) {
bar.base = this.scale.endPoint;
// change the way the x and width functions are called
bar.transition({
x: this.scale.calculateBarX(index),
y: this.scale.calculateY(bar.value),
width: this.scale.calculateBarWidth(index)
}, easingDecimal).draw();
}
}, this);
}, this);
}
});
You pass in the widths like below
var data = {
labels: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
datasets: [
{
label: "My First dataset",
fillColor: "rgba(220,220,220,0.5)",
strokeColor: "rgba(220,220,220,0.8)",
highlightFill: "rgba(220,220,220,0.7)",
highlightStroke: "rgba(220,220,220,1)",
data: [65, 59, 80, 30, 56, 65, 40],
data2: [10, 20, 30, 20, 10, 40, 10]
},
]
};
and you call it like so
var ctx = document.getElementById('canvas').getContext('2d');
var myLineChart = new Chart(ctx).BarAlt(data);
Fiddle - http://jsfiddle.net/moye0cp4/

Drawing multiple edges between two nodes with d3

I've been following Mike Bostock's code from this example to learn how to draw directed graphs in d3 and was wondering how I would structure the code so that I could add multiple edges between two nodes in the graph. For example, if the dataset in the example above were defined as
var links = [{source: "Microsoft", target: "Amazon", type: "licensing"},
{source: "Microsoft", target: "Amazon", type: "suit"},
{source: "Samsung", target: "Apple", type: "suit"},
{source: "Microsoft", target: "Amazon", type: "resolved"}];
and then run through the code, all I see is one line. All the paths are being drawn correctly in the html code, however they all have the same coordinates and orientation which causes the visual to look like 1 line. What kind of code restructuring would need to be done in this example to allow for the 3 edges to not be drawn on top of each other?
In fact, the original visualization is a prime example of one method to show multiple links between nodes, that is - using arcs rather than direct paths, so you can see both incoming and outgoing links.
This concept can be extended to show multiple of each of these types of links by changing the radius values of subsequent svg path(arc) elements representing the link. A basic example being
dr = 75/d.linknum;
Where d.linknum represents the number of the successive link. dr is later used as the rx and ry amounts for the arc being drawn.
Full implementation here: http://jsfiddle.net/7HZcR/3/
Here is the source for the answer above if anyone ever needs it :
var links = [{source: "Microsoft", target: "Amazon", type: "licensing"},
{source: "Microsoft", target: "Amazon", type: "suit"},
{source: "Samsung", target: "Apple", type: "suit"},
{source: "Microsoft", target: "Amazon", type: "resolved"}];
//sort links by source, then target
links.sort(function(a,b) {
if (a.source > b.source) {return 1;}
else if (a.source < b.source) {return -1;}
else {
if (a.target > b.target) {return 1;}
if (a.target < b.target) {return -1;}
else {return 0;}
}
});
//any links with duplicate source and target get an incremented 'linknum'
for (var i=0; i<links.length; i++) {
if (i != 0 &&
links[i].source == links[i-1].source &&
links[i].target == links[i-1].target) {
links[i].linknum = links[i-1].linknum + 1;
}
else {links[i].linknum = 1;};
};
var nodes = {};
// Compute the distinct nodes from the links.
links.forEach(function(link) {
link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
link.target = nodes[link.target] || (nodes[link.target] = {name: link.target});
});
var w = 600,
h = 600;
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([w, h])
.linkDistance(60)
.charge(-300)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h);
// Per-type markers, as they don't inherit styles.
svg.append("svg:defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var path = svg.append("svg:g").selectAll("path")
.data(force.links())
.enter().append("svg:path")
.attr("class", function(d) { return "link " + d.type; })
.attr("marker-end", function(d) { return "url(#" + d.type + ")"; });
var circle = svg.append("svg:g").selectAll("circle")
.data(force.nodes())
.enter().append("svg:circle")
.attr("r", 6)
.call(force.drag);
var text = svg.append("svg:g").selectAll("g")
.data(force.nodes())
.enter().append("svg:g");
// A copy of the text with a thick white stroke for legibility.
text.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.attr("class", "shadow")
.text(function(d) { return d.name; });
text.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.text(function(d) { return d.name; });
// Use elliptical arc path segments to doubly-encode directionality.
function tick() {
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = 75/d.linknum; //linknum is defined above
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
circle.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
text.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
path.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
marker#licensing {
fill: green;
}
path.link.licensing {
stroke: green;
}
path.link.resolved {
stroke-dasharray: 0,2 1;
}
circle {
fill: #ccc;
stroke: #333;
stroke-width: 1.5px;
}
text {
font: 10px sans-serif;
pointer-events: none;
}
text.shadow {
stroke: #fff;
stroke-width: 3px;
stroke-opacity: .8;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="chart"></div>
And for D3v4 see here : https://bl.ocks.org/mbostock/4600693
Thanks for the answers using linknum, it really worked. however the lines started overlapping after linkum > 10.
Here is a function to generate equidistance quadratic curves
// use it like 'M' + d.source.x + ',' + d.source.y + link_arc2(d) + d.target.x + ',' + d.target.y
function link_arc2(d) {
// draw line for 1st link
if (d.linknum == 1) {
return 'L';
}
else {
let sx = d.source.x;
let sy = d.source.y;
let tx = d.target.x;
let ty = d.target.y;
// distance b/w curve paths
let cd = 30;
// find middle of source and target
let cx = (sx + tx) / 2;
let cy = (sy + ty) / 2;
// find angle of line b/w source and target
var angle = Math.atan2(ty - sy, tx - sx);
// add radian equivalent of 90 degree
var c_angle = angle + 1.5708;
// draw odd and even curves either side of line
if (d.linknum & 1) {
return 'Q ' + (cx - ((d.linknum - 1) * cd * Math.cos(c_angle))) + ',' + (cy - ((d.linknum - 1) * cd * Math.sin(c_angle))) + ' ';
}
else {
return 'Q ' + (cx + (d.linknum * cd * Math.cos(c_angle))) + ',' + (cy + (d.linknum * cd * Math.sin(c_angle))) + ' ';
}
}
}