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

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>

Related

JoinJS - fromJSON method error: "dia.ElementView: markup required"

I have a problem that I can't solve. I want to use JointJS fromJSON function to reconstruct the flowchart from a JSON (previously exported using JoinJS's toJSON function.
The problem is that the call to the fromJSON function always returns the following error:
Whether I call it inside the hook mounted () or call it from the click of a button.
For completeness I also want to say that I am using Vue.js.
The code I'm using instead is the following:
<template>
<div class="wrapper">
<button v-on:click="getGraphJSON">Get graph JSON</button>
<button v-on:click="resetGraphJSON">Restore graph from JSON</button>
<div id="myholder"></div>
</div>
</template>
<script>
const _ = require('lodash')
const joint = require('jointjs')
const g = require('../../node_modules/jointjs/dist/geometry.js')
const backbone = require('../../node_modules/backbone/backbone.js')
const $ = require('../../node_modules/jquery/dist/jquery.js')
import '../../node_modules/jointjs/dist/joint.css';
var CustomRectangle = joint.shapes.standard.Rectangle.define('CustomRectangle', {
type: 'CustomRectangle',
attrs: {
body: {
rx: 10, // add a corner radius
ry: 10,
strokeWidth: 1,
fill: 'cornflowerblue'
},
label: {
textAnchor: 'left', // align text to left
refX: 10, // offset text from right edge of model bbox
fill: 'white',
fontSize: 18
}
}
}, {
markup: [{
tagName: 'rect',
selector: 'body',
}, {
tagName: 'text',
selector: 'label'
}]
}, {
createRandom: function() {
var rectangle = new this();
var fill = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6);
var stroke = '#' + ('000000' + Math.floor(Math.random() * 16777215).toString(16)).slice(-6);
var strokeWidth = Math.floor(Math.random() * 6);
var strokeDasharray = Math.floor(Math.random() * 6) + ' ' + Math.floor(Math.random() * 6);
var radius = Math.floor(Math.random() * 21);
rectangle.attr({
body: {
fill: fill,
stroke: stroke,
strokeWidth: strokeWidth,
strokeDasharray: strokeDasharray,
rx: radius,
ry: radius
},
label: { // ensure visibility on dark backgrounds
fill: 'black',
stroke: 'white',
strokeWidth: 1,
fontWeight: 'bold'
}
});
return rectangle;
}
});
export default {
name: 'JointChartRestorable',
data() {
return {
graph: null,
paper: null,
// graphJSON: JSON.parse('{"cells":[{"type":"standard.Rectangle","position":{"x":100,"y":30},"size":{"width":100,"height":40},"angle":0,"id":"049776c9-7b6d-4aaa-8b02-1edc3bea9852","z":1,"attrs":{"body":{"fill":"blue"},"label":{"fill":"white","text":"Rect #1"}}},{"type":"standard.Rectangle","position":{"x":400,"y":30},"size":{"width":100,"height":40},"angle":0,"id":"b6e77973-1195-4749-99e1-728549329b11","z":2,"attrs":{"body":{"fill":"#2C3E50","rx":5,"ry":5},"label":{"fontSize":18,"fill":"#3498DB","text":"Rect #2","fontWeight":"bold","fontVariant":"small-caps"}}},{"type":"standard.Link","source":{"id":"049776c9-7b6d-4aaa-8b02-1edc3bea9852"},"target":{"id":"b6e77973-1195-4749-99e1-728549329b11"},"id":"4ed8e3b3-55de-4ad2-b79e-d4848adc4a58","labels":[{"attrs":{"text":{"text":"Hello, World!"}}}],"z":3,"attrs":{"line":{"stroke":"blue","strokeWidth":1,"targetMarker":{"d":"M 10 -5 0 0 10 5 Z","stroke":"black","fill":"yellow"},"sourceMarker":{"type":"path","stroke":"black","fill":"red","d":"M 10 -5 0 0 10 5 Z"}}}}],"graphCustomProperty":true,"graphExportTime":1563951791966}')
// graphJSON: JSON.parse('{"cells":[{"type":"examples.CustomRectangle","position":{"x":90,"y":30},"size":{"width":100,"height":40},"angle":0,"id":"faa7f957-4691-4bb2-b907-b2054f7e07de","z":1,"attrs":{"body":{"fill":"blue"},"label":{"text":"Rect #1"}}}]}')
graphJSON: JSON.parse('{"cells":[{"type":"CustomRectangle","position":{"x":100,"y":30},"size":{"width":100,"height":40},"angle":0,"id":"f02da591-c03c-479f-88cf-55c291064ca8","z":1,"attrs":{"body":{"fill":"blue"},"label":{"text":"Rect #1"}}}]}')
};
},
methods: {
getGraphJSON: function() {
this.graphJSON = this.graph.toJSON();
console.log(JSON.stringify(this.graphJSON));
this.graph.get('graphCustomProperty'); // true
this.graph.get('graphExportTime');
},
resetGraphJSON: function() {
if(this.graphJSON !== undefined && this.graphJSON !== null && this.graphJSON !== '') {
this.graph.fromJSON(this.graphJSON);
// this.paper.model.set(this.graphJSON);
} else {
alert('Devi prima cliccare sul tasto "Get graph JSON" almeno una volta');
}
}
},
mounted() {
this.graph = new joint.dia.Graph();
this.graph.fromJSON(this.graphJSON);
// this.graph.set('graphCustomProperty', true);
// this.graph.set('graphExportTime', Date.now());
this.paper = new joint.dia.Paper({
el: document.getElementById('myholder'),
model: this.graph,
width: '100%',
height: 600,
gridSize: 10,
drawGrid: true,
background: {
color: 'rgba(0, 255, 0, 0.3)'
},
// interactive: false, // disable default interaction (e.g. dragging)
/*elementView: joint.dia.ElementView.extend({
pointerdblclick: function(evt, x, y) {
joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments);
this.notify('element:pointerdblclick', evt, x, y);
this.model.remove();
}
}),
linkView: joint.dia.LinkView.extend({
pointerdblclick: function(evt, x, y) {
joint.dia.CellView.prototype.pointerdblclick.apply(this, arguments);
this.notify('link:pointerdblclick', evt, x, y);
this.model.remove();
}
})*/
});
/*this.paper.on('cell:pointerdblclick', function(cellView) {
var isElement = cellView.model.isElement();
var message = (isElement ? 'Element' : 'Link') + ' removed';
eventOutputLink.attr('label/text', message);
eventOutputLink.attr('body/visibility', 'visible');
eventOutputLink.attr('label/visibility', 'visible');
});*/
/***************************************************/
/************** GRAPH ELEMENT SAMPLE ***************/
/***************************************************/
// var rect = new joint.shapes.standard.Rectangle();
// var rect = new CustomRectangle();
// rect.position(100, 30);
// rect.resize(100, 40);
// rect.attr({
// body: {
// fill: 'blue'
// },
// label: {
// text: 'Rect #1',
// fill: 'white'
// }
// });
// rect.addTo(this.graph);
/***************************************************/
/************** GRAPH ELEMENT SAMPLE ***************/
/***************************************************/
}
}
</script>
Right now I'm using a custom element, previously defined, but I've also done tests using the standard Rectangle element of JointJS.
Can anyone tell me if I'm doing something wrong?
Many thanks in advance.
Markup object could not be found in element that's reason why this error is getting. After it's imported jointjs to the vueJS project through jointjs or rabbit dependency;
import * as joint from 'jointjs' or import * as joint from 'rabbit'
window.joint = joint;
joint should be adjusted as global in environment by using window.

Geowebcache + Openlayers3 with XYZ source: error by zooming the map

I'm trying the do the same map as here:
Openlayers3: tile grid incorrect with pixelratio=3 using Geoserver/Geowebcache as backend
but using the TMS protocol instead of WMS.
The map works good but there is a little problem by zooming in, only by changing from zoom level 4 to zoom level 5: the map seems to "jump upwards". The problem occurs with all pixel ratio.
This is my source code. Any help is appreciated:
<!DOCTYPE html>
<html>
<head>
<title>WMS Tiles</title>
<link rel="stylesheet" href="https://openlayers.org/en/v3.19.1/css/ol.css" type="text/css">
<!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList,URL"></script>
<script src="https://openlayers.org/en/v3.19.1/build/ol.js"></script>
</head>
<style type="text/css">
body { font-family: sans-serif; font-weight: bold; font-size: .8em; }
body { border: 0px; margin: 0px; padding: 0px; }
</style>
<body>
<div id="map" class="map" style="width: 85%; height: 85%;border: 0px; padding: 0px"> </div>
<script>
console.log("DOMContentLoaded");
try {
var config = {
"bounds": {
"left" : 475279.2689196961,
"bottom" : 5473193.572300382,
"right" : 476655.1750108673,
"top" : 5474594.636365802
}
};
var bounds = [config.bounds.left, config.bounds.bottom, config.bounds.right, config.bounds.top];
var resolutions = [
1.4,
0.7,
0.35,
0.175,
0.0875,
0.04375,
];
var tilePixelRatio = 1;
if (ol.has.DEVICE_PIXEL_RATIO > 2.5) {
tilePixelRatio = 3;
} else if (ol.has.DEVICE_PIXEL_RATIO > 1.5) {
tilePixelRatio = 2;
}
var matrixSet = 'unihd15';
// available gridsets in backend: unihd15, unihd15_512 and unihd15_768
if (tilePixelRatio > 1){
matrixSet = matrixSet + '_' + (256 * tilePixelRatio).toString();
}
var res_length = resolutions.length;
var matrixIds = new Array(res_length );
for (var z = 0; z < res_length ; ++z) {
matrixIds[z] = matrixSet + ':'+ z;
}
console.log('matrixSet is: ' + matrixSet);
console.log(matrixIds);
console.log(matrixIds[0] + ' '+ resolutions[0]);
console.log('Center: ' + ol.extent.getCenter(bounds));
console.log('Pixel ratio: ' + window.devicePixelRatio);
console.log('Bounds: ' + bounds);
console.log('TopLeft: ' + ol.extent.getTopLeft(bounds));
var projection = new ol.proj.Projection({
code: 'EPSG:32632',
units: 'm',
extent: [166021.4431, 0.0000, 833978.5569, 9329005.1825]
});
var tileGrid = new ol.tilegrid.TileGrid({
extent: bounds,
resolutions: resolutions,
origin: ol.extent.getTopLeft(bounds),
tileSize: [256, 256]
});
var view = new ol.View({
extent: bounds,
zoom: 0,
center: ol.extent.getCenter(bounds),
projection: projection,
resolutions: resolutions
});
var layerName = 'unihd15:unihd15_0_basemap';
var tms_source = new ol.source.XYZ({
projection: projection,
tileGrid: tileGrid,
tilePixelRatio: tilePixelRatio,
url: 'http://ssp.deepmap.de/geo/gwc/service/tms/1.0.0/' + layerName + '#' + matrixSet + '#png/{z}/{x}/{-y}.png'
});
var layer = new ol.layer.Tile({
source: tms_source,
extent: bounds
});
var map = new ol.Map({
projection: projection,
controls: ol.control.defaults(
{
rotate: false,
attributionOptions: {
collapsible: false
}
}
),
view: view,
layers: [layer],
target: 'map'
});
console.log("no error");
} catch (error) {
console.log("error");
console.log(error);
}
</script>
</body>
</html>
Usually this kind of problem comes from an incorrect extent. To get the correct extent, go to http://ssp.deepmap.de/geo/gwc/service/tms/1.0.0/unihd15:unihd15_0_basemap#EPSG:32632#png (currently not working on your GeoServer, but that's the URL), and use the <BoundingBox> from there.

how to make real time multi line chart using d3 while getting data from Json?

I am new to D3 ,i want to make a multi line chart using d3.js while getting data from a json object can anyone guide me.here is working sample code
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.graph .axis {
stroke-width: 1;
}
.graph .axis .tick line {
stroke: black;
}
.graph .axis .tick text {
fill: black;
font-size: 0.7em;
}
.graph .axis .domain {
fill: none;
stroke: black;
}
.graph .group {
fill: none;
stroke: black;
stroke-width: 1.5;
}
</style>
</head>
<body>
<div class="graph"></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var limit = 60 * 1,
duration = 750,
now = new Date(Date.now() - duration)
var width = 500,
height = 200
var groups = {
current: {
value: 0,
color: 'orange',
data: d3.range(limit).map(function() {
return 0
})
},
target: {
value: 0,
color: 'green',
data: d3.range(limit).map(function() {
return 0
})
},
output: {
value: 0,
color: 'grey',
data: d3.range(limit).map(function() {
return 0
})
}
}
var x = d3.time.scale()
.domain([now - (limit - 2), now - duration])
.range([0, width])
var y = d3.scale.linear()
.domain([0, 100])
.range([height, 0])
var line = d3.svg.line()
.interpolate('basis')
.x(function(d, i) {
return x(now - (limit - 1 - i) * duration)
})
.y(function(d) {
return y(d)
})
var svg = d3.select('.graph').append('svg')
.attr('class', 'chart')
.attr('width', width)
.attr('height', height + 50)
var axis = svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(x.axis = d3.svg.axis().scale(x).orient('bottom'))
var paths = svg.append('g')
for (var name in groups) {
var group = groups[name]
group.path = paths.append('path')
.data([group.data])
.attr('class', name + ' group')
.style('stroke', group.color)
}
function tick() {
now = new Date()
// Add new values
for (var name in groups) {
var group = groups[name]
//group.data.push(group.value) // Real values arrive at irregular intervals
group.data.push(20 + Math.random() * 100)
group.path.attr('d', line)
}
// Shift domain
x.domain([now - (limit - 2) * duration, now - duration])
// Slide x-axis left
axis.transition()
.duration(duration)
.ease('linear')
.call(x.axis)
// Slide paths left
paths.attr('transform', null)
.transition()
.duration(duration)
.ease('linear')
.attr('transform', 'translate(' + x(now - (limit - 1) * duration) + ')')
.each('end', tick)
// Remove oldest data point from each group
for (var name in groups) {
var group = groups[name]
group.data.shift()
}
}
tick()
</script>
</body>
but i need straight line running line chart with bum on some point depending on values so here is image of chart i wanted any help will be greatly appreciated.

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))) + ' ';
}
}
}