Chart.js dynamic bar width - bar-chart

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/

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.

Add a rotated satellite image on the map using OpenLayers 5

I'm trying to add a satellite image on my map using OpenLayers 5.
The problem is that I'm not able to do this, because I've just found an option to add an image on the map passing the image extent (xmin, ymin, xmax, ymax) and not the bounding box. The image should fit inside the bounding box. For that reason, the image was distorted.
The image is in JPG file (attribute feature.properties.icon). Example: http://exampleserver.com/220/063/353LGN00/353LGN00_thumb_large.jpg
The result that I would like is something like this:
The result that I've got was that:
My code that adds this image on the map is the following:
import ImageLayer from 'ol/layer/Image'
import Static from 'ol/source/ImageStatic'
...
this.olmap = new Map({
target: 'map',
layers: [
baseLayerGroup, rasterLayerGroup, vectorLayer
],
view: new View({
projection: 'EPSG:4326',
center: [ -45.8392, -3.65286 ],
zoom: 8
})
})
...
this.rasterLayerGroup.getLayers().push(
new ImageLayer({
source: new Static({
url: feature.properties.icon,
projection: 'EPSG:4326',
imageExtent: [
feature.properties.bl_longitude, feature.properties.bl_latitude,
feature.properties.tr_longitude, feature.properties.tr_latitude
]
})
})
)
Would someone know how to pass the image bounding box instead of just the image extent?
Thank you in advance.
EDIT 1: Mike's solution
Through Mike's solution I was able to fix a bug that some images have (near to the equator line). For that reason, his answer solved my problem and it inserted the image in a better position that I was expecting in the moment that I created the question.
However, this solution worked to me with images near to the equator line. Images next to the poles stay distorted (Edit 2).
I send below a picture illustrating the final result:
EDIT 2: New problem?
I was testing some images and I have discovered a new bug. Now I have discovered that the image should fit inside the bounding box. If the image does not fit inside the bbox, it stays distorted, such as the print that I send below illustrating.
The image should fit inside the bbox like in the image below [PS 1]:
I believe that it can be a problem of reprojection, but I don't know, because both view projection and image projection is EPSG:4326.
I tried to follow the explanation about Raster Reprojection[1.] on Openlayers site, however I was not able to reproduce it, because, as I said, both projections (view and image) are the same (or they should be).
I send below the GeoJSON that contains the information related to the image above. The image can be found in "properties.icon" (http://www.dpi.inpe.br/newcatalog/tmp/MOD13Q1/2018/MOD13Q1.A2018017.h13v14.jpg). The bbox coordinates can be found in "geometry.coordinates" or in "properties.bl_latitude", "properties.bl_longitude", "properties.br_latitude" and so on.
"bl" means "bottom left", "br" means "bottom right", "tl" means "top left" and "tr" means "top right". These coordinates inside "properties" are the same inside "geometry.coordinates".
{
"geometry": {
"coordinates": [
[
[
-77.7862,
-50
],
[
-100,
-60
],
[
-80,
-60
],
[
-62.229,
-50
],
[
-77.7862,
-50
]
]
],
"type": "Polygon"
},
"properties": {
"alternate": "http://www.dpi.inpe.br/opensearch/v2/granule.json?uid=MOD13Q1.A2018017.h13v14",
"auxpath": null,
"bitslips": null,
"bl_latitude": -60,
"bl_longitude": -100,
"br_latitude": -60,
"br_longitude": -80,
"centerlatitude": -55,
"centerlongitude": -80.0038,
"centertime": null,
"cloud": 0,
"cloudcovermethod": "M",
"dataset": "MOD13Q1",
"date": "2018-01-17T00:00:00",
"enclosure": [
{
"band": "evi",
"radiometric_processing": "SR",
"type": "MOSAIC",
"url": "http://www.dpi.inpe.br/newcatalog/tmp/MOD13Q1/2018/MOD13Q1.A2018017.h13v14.006.2018033223827.hdf"
},
{
"band": "ndvi",
"radiometric_processing": "SR",
"type": "MOSAIC",
"url": "http://www.dpi.inpe.br/newcatalog/tmp/MOD13Q1/2018/MOD13Q1.A2018017.h13v14.006.2018033223827.hdf"
},
...
],
"icon": "http://www.dpi.inpe.br/newcatalog/tmp/MOD13Q1/2018/MOD13Q1.A2018017.h13v14.jpg",
"id": "http://www.dpi.inpe.br/opensearch/v2/granule.json?uid=MOD13Q1.A2018017.h13v14",
"orbit": 0,
"path": 14,
"provider": "OP_CBERS1",
"row": 13,
"satellite": "T1",
"sensor": "MODIS",
"title": "MOD13Q1.A2018017.h13v14",
"tl_latitude": -50,
"tl_longitude": -77.7862,
"tr_latitude": -50,
"tr_longitude": -62.229,
"type": "IMAGES",
"updated": "2018-03-01T18:51:56",
"via": "http://www.dpi.inpe.br/opensearch/v2/metadata/MOD13Q1.A2018017.h13v14"
},
"type": "Feature"
}
Would someone have a new idea?
[PS 1]: The original code that does the image fits inside the bbox is a Leaflet code [2.] and I send it below:
var map = L.map('map').setView([-15.22, -53.23], 5)
...
var anchor = [
[feature.properties.tl_latitude, feature.properties.tl_longitude],
[feature.properties.tr_latitude, feature.properties.tr_longitude],
[feature.properties.br_latitude, feature.properties.br_longitude],
[feature.properties.bl_latitude, feature.properties.bl_longitude]
]
layer._quicklook = L.imageTransform(feature.properties.icon, anchor).addTo(map)
[1.] https://openlayers.org/en/latest/doc/tutorials/raster-reprojection.html
[2.] https://github.com/ScanEx/Leaflet.imageTransform
If the coordinates are those of the photo and the jpg which contains the rotated photo is in EPSG:4326 (i.e. aligned to meridians and parallels) then you need a bounding box containing all of the corners of the photo
import {boundingExtent} from 'ol/extent';
....
this.rasterLayerGroup.getLayers().push(
new ImageLayer({
source: new Static({
url: feature.properties.icon,
projection: 'EPSG:4326',
imageExtent: boundingExtent([
[feature.properties.bl_longitude, feature.properties.bl_latitude],
[feature.properties.br_longitude, feature.properties.br_latitude],
[feature.properties.tl_longitude, feature.properties.tl_latitude],
[feature.properties.tr_longitude, feature.properties.tr_latitude]
])
})
})
)
However your top screenshot has the jpg itself rotated. If that is desired the projection isn't EPSG:4326 and you would need to define a custom projection to handle the rotation.
I've managed to get something close, but simply stretching the image to fit the polygon doesn't give the exact alignment at the side that the leaflet method does
var properties = {
"bl_latitude": -60,
"bl_longitude": -100,
"br_latitude": -60,
"br_longitude": -80,
"centerlatitude": -55,
"centerlongitude": -80.0038,
"icon": "https://www.mikenunn.net/demo/MOD13Q1.A2018017.h13v14.jpg",
"tl_latitude": -50,
"tl_longitude": -77.7862,
"tr_latitude": -50,
"tr_longitude": -62.229,
};
function overlaySource ( properties ) {
var projection = ol.proj.get('EPSG:3857'); // leaflet projection
var extentSize = [0, 0, 4096, 4096]; // arbitary extent for the projection transforms
var size0 = extentSize[2];
var size1 = extentSize[3];
var url = properties.icon;
var bl = ol.proj.transform([properties.bl_longitude, properties.bl_latitude], 'EPSG:4326', projection);
var tl = ol.proj.transform([properties.tl_longitude, properties.tl_latitude], 'EPSG:4326', projection);
var br = ol.proj.transform([properties.br_longitude, properties.br_latitude], 'EPSG:4326', projection);
var tr = ol.proj.transform([properties.tr_longitude, properties.tr_latitude], 'EPSG:4326', projection);
function normalTransform(coordinates, output, dimensions) {
var dims = dimensions || 2;
for (var i=0; i<coordinates.length; i+=dims) {
var left = bl[0] + (tl[0]-bl[0]) * coordinates[i+1]/size1;
var right = br[0] + (tr[0]-br[0]) * coordinates[i+1]/size1;
var top = tl[1] + (tr[1]-tl[1]) * coordinates[i]/size0;
var bottom = bl[1] + (br[1]-bl[1]) * coordinates[i]/size0;
var newCoordinates0 = left + (right-left) * coordinates[i]/size0;
var newCoordinates1 = bottom + (top-bottom) * coordinates[i+1]/size1;
c = ol.proj.transform([newCoordinates0, newCoordinates1], projection, 'EPSG:3857');
//console.log(coordinates[i] + ' ' + coordinates[i+1] + ' ' + c[0] + ' ' + c[1]);
coordinates[i] = c[0];
coordinates[i+1] = c[1];
}
return coordinates;
}
function rotateTransform(coordinates, output, dimensions) {
var dims = dimensions || 2;
for (var i=0; i<coordinates.length; i+=dims) {
c = ol.proj.transform([coordinates[i], coordinates[i+1]], 'EPSG:3857', projection);
var left = bl[0] + (tl[0]-bl[0]) * (c[1]-bl[1]) /(tl[1]-bl[1]);
var right = br[0] + (tr[0]-br[0]) * (c[1]-br[1]) /(tr[1]-br[1]);
var top = tl[1] + (tr[1]-tl[1]) * (c[0]-tl[0])/(tr[0]-tl[0]);
var bottom = bl[1] + (br[1]-bl[1]) * (c[0]-bl[0])/(br[0]-bl[0]);
var newCoordinates0 = (c[0]-left)*size0/(right-left);
var newCoordinates1 = (c[1]-bottom)*size1/(top-bottom);
//console.log(coordinates[i] + ' ' + coordinates[i+1] + ' ' + newCoordinates0 + ' ' + newCoordinates1);
coordinates[i] = newCoordinates0;
coordinates[i+1] = newCoordinates1;
}
return coordinates;
}
var rotatedProjection = new ol.proj.Projection({
code: 'EPSG:' + url + 'rotated',
units: 'm',
extent: extentSize
});
ol.proj.addProjection(rotatedProjection);
ol.proj.addCoordinateTransforms('EPSG:3857', rotatedProjection,
function(coordinate) {
return rotateTransform(coordinate);
},
function(coordinate) {
return normalTransform(coordinate);
}
);
ol.proj.addCoordinateTransforms('EPSG:4326', rotatedProjection,
function(coordinate) {
return rotateTransform(ol.proj.transform(coordinate, "EPSG:4326", "EPSG:3857"));
},
function(coordinate) {
return ol.proj.transform(normalTransform(coordinate), "EPSG:3857", "EPSG:4326");
}
);
return new ol.source.ImageStatic({
projection: rotatedProjection,
imageExtent: extentSize,
url: url
});
}
var tileLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
attributions: [
'Powered by Esri',
'Source: Esri, DigitalGlobe, GeoEye, Earthstar Geographics, CNES/Airbus DS, USDA, USGS, AeroGRID, IGN, and the GIS User Community'
],
//attributionsCollapsible: false,
url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
maxZoom: 23
})
});
var imageLayer = new ol.layer.Image({
source: overlaySource( properties ),
opacity: 0.7
})
var map = new ol.Map({
layers: [tileLayer, imageLayer],
target: 'map',
logo: false,
view: new ol.View()
});
var imageProj = imageLayer.getSource().getProjection();
map.getView().fit(ol.proj.transformExtent(imageProj.getExtent(), imageProj, map.getView().getProjection()), {constrainResolution: false});
html, body, .map {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
<link href="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/css/ol.css" rel="stylesheet" />
<script src="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/build/ol.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.5.0/proj4.js"></script>
<div id="map" class="map"></div>
This is my KML method, but that is a simple rotation of a rectangle by a specified angle, not warping it into a quadrilateral where only two of the sides are parallel.
function kmlOverlaySource ( kmlExtent, // KMLs specify the extent the unrotated image would occupy
url,
rotation,
imageSize,
) {
// calculate latitude of true scale of equidistant cylindrical projection based on pixels per degree on each axis
proj4.defs('EPSG:' + url, '+proj=eqc +lat_ts=' +
(Math.acos((ol.extent.getHeight(kmlExtent)/imageSize[1])
/(ol.extent.getWidth(kmlExtent)/imageSize[0]))*180/Math.PI) +
' +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs');
if (ol.proj.proj4 && ol.proj.proj4.register) { ol.proj.proj4.register(proj4); } // if OL5 register proj4
// convert the extents to source projection coordinates
var projection = ol.proj.get('EPSG:' + url);
var projExtent = ol.proj.transformExtent(kmlExtent, 'EPSG:4326', projection);
var angle = -rotation * Math.PI/180;
function rotateTransform(coordinates, output, dimensions) {
var dims = dimensions || 2;
for (var i=0; i<coordinates.length; i+=dims) {
var point = new ol.geom.Point([coordinates[i],coordinates[i+1]]);
point.rotate(angle, ol.extent.getCenter(projExtent));
var newCoordinates = point.getCoordinates();
coordinates[i] = newCoordinates[0];
coordinates[i+1] = newCoordinates[1];
}
return coordinates;
}
function normalTransform(coordinates, output, dimensions) {
var dims = dimensions || 2;
for (var i=0; i<coordinates.length; i+=dims) {
var point = new ol.geom.Point([coordinates[i],coordinates[i+1]]);
point.rotate(-angle, ol.extent.getCenter(projExtent));
var newCoordinates = point.getCoordinates();
coordinates[i] = newCoordinates[0];
coordinates[i+1] = newCoordinates[1];
}
return coordinates;
}
var rotatedProjection = new ol.proj.Projection({
code: 'EPSG:' + url + 'rotated',
units: 'm',
extent: projExtent
});
ol.proj.addProjection(rotatedProjection);
ol.proj.addCoordinateTransforms('EPSG:4326', rotatedProjection,
function(coordinate) {
return rotateTransform(ol.proj.transform(coordinate, 'EPSG:4326', projection));
},
function(coordinate) {
return ol.proj.transform(normalTransform(coordinate), projection, 'EPSG:4326');
}
);
ol.proj.addCoordinateTransforms('EPSG:3857', rotatedProjection,
function(coordinate) {
return rotateTransform(ol.proj.transform(coordinate, 'EPSG:3857', projection));
},
function(coordinate) {
return ol.proj.transform(normalTransform(coordinate), projection, 'EPSG:3857');
}
);
return new ol.source.ImageStatic({
projection: rotatedProjection,
url: url,
imageExtent: projExtent
});
}
var tileLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
attributions: [
'Powered by Esri',
'Source: Esri, DigitalGlobe, GeoEye, Earthstar Geographics, CNES/Airbus DS, USDA, USGS, AeroGRID, IGN, and the GIS User Community'
],
//attributionsCollapsible: false,
url: 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
maxZoom: 23
})
});
// these would normally be parsed from a KML file
var kmlExtent = [8.433995415151397, 46.65804355828784, 9.144871415151389, 46.77980155828784];
var url = 'https://raw.githubusercontent.com/ReneNyffenegger/about-GoogleEarth/master/kml/the_png_says.png'
var rotation = 30;
var imageSize = [];
var img = document.createElement('img');
img.onload = imageLoaded;
img.src = url;
function imageLoaded() {
imageSize[0] = img.width;
imageSize[1] = img.height;
var imageLayer = new ol.layer.Image({
source: kmlOverlaySource(kmlExtent, url, rotation, imageSize),
});
var map = new ol.Map({
layers: [tileLayer, imageLayer],
target: 'map',
logo: false,
view: new ol.View()
});
var imageProj = imageLayer.getSource().getProjection();
map.getView().fit(ol.proj.transformExtent(imageProj.getExtent(), imageProj, map.getView().getProjection()), {constrainResolution: false});
}
html, body, .map {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
<link href="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/css/ol.css" rel="stylesheet" />
<script src="https://cdn.rawgit.com/openlayers/openlayers.github.io/master/en/v5.3.0/build/ol.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.5.0/proj4.js"></script>
<div id="map" class="map"></div>

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>

Exporting HTML table to PDF with many columns using jsPDF

I'd like to link this question that doesn't have any answers. Exporting HTML table to PDF with its format with jsPDF. I'm having the same problem with him and the tables looks exactly alike. I have a 20 column html table and I want them to be exported to pdf without any problem. I'm using jsPDF for exporting the table. I have tried the html <colgroup> tag for the column width of my table and it didn't work out. I have the first 8 columns showing and 12 columns hidden. I want all of them to be exported to pdf.
I'd like to try this code but I didn't know how I will execute it using my button in my html.
$(document).on("click", "#btnExportToPDF", function () {
var table1 =
tableToJson($('#table1').get(0)),
cellWidth = 35,
rowCount = 0,
cellContents,
leftMargin = 2,
topMargin = 12,
topMarginTable = 55,
headerRowHeight = 13,
rowHeight = 9,
l = {
orientation: 'l',
unit: 'mm',
format: 'a3',
compress: true,
fontSize: 8,
lineHeight: 1,
autoSize: false,
printHeaders: true
};
var doc = new jsPDF(l, '', '', '');
doc.setProperties({
title: 'Test PDF Document',
subject: 'This is the subject',
author: 'author',
keywords: 'generated, javascript, web 2.0, ajax',
creator: 'author'
});
doc.cellInitialize();
$.each(table1, function (i, row)
{
rowCount++;
$.each(row, function (j, cellContent) {
if (rowCount == 1) {
doc.margins = 1;
doc.setFont("helvetica");
doc.setFontType("bold");
doc.setFontSize(9);
doc.cell(leftMargin, topMargin, cellWidth, headerRowHeight, cellContent, i)
}
else if (rowCount == 2) {
doc.margins = 1;
doc.setFont("times ");
doc.setFontType("italic"); // or for normal font type use ------ doc.setFontType("normal");
doc.setFontSize(8);
doc.cell(leftMargin, topMargin, cellWidth, rowHeight, cellContent, i);
}
else {
doc.margins = 1;
doc.setFont("courier ");
doc.setFontType("bolditalic ");
doc.setFontSize(6.5);
doc.cell(leftMargin, topMargin, cellWidth, rowHeight, cellContent, i); // 1st=left margin 2nd parameter=top margin, 3rd=row cell width 4th=Row height
}
})
})
doc.save('sample Report.pdf'); })
function tableToJson(table) {
var data = [];
// first row needs to be headers
var headers = [];
for (var i=0; i<table.rows[0].cells.length; i++) {
headers[i] = table.rows[0].cells[i].innerHTML.toLowerCase().replace(/ /gi,'');
}
// go through cells
for (var i=1; i<table.rows.length; i++) {
var tableRow = table.rows[i];
var rowData = {};
for (var j=0; j<tableRow.cells.length; j++) {
rowData[ headers[j] ] = tableRow.cells[j].innerHTML;
}
data.push(rowData);
}
return data; }
This is my code btw,
function demoFromHTML() {
$(document).find('tfoot').remove();
$('#table td:nth-child(8)').remove();
var pdf = new jsPDF('p', 'pt', 'letter', true);
// source can be HTML-formatted string, or a reference
// to an actual DOM element from which the text will be scraped.
source = $('#table')[0];
// we support special element handlers. Register them with jQuery-style
// ID selector for either ID or node name. ("#iAmID", "div", "span" etc.)
// There is no support for any other type of selectors
// (class, of compound) at this time.
specialElementHandlers = {
// element with id of "bypass" - jQuery style selector
'#bypassme': function (element, renderer) {
// true = "handled elsewhere, bypass text extraction"
return true
}
};
margins = {
top: 80,
bottom: 60,
left: 55,
width: 522
};
// all coords and widths are in jsPDF instance's declared units
// 'inches' in this case
pdf.fromHTML(
source, // HTML string or DOM elem ref.
margins.left, // x coord
margins.top, { // y coord
'width': margins.width, // max width of content on PDF
'elementHandlers': specialElementHandlers
},
function (dispose) {
// dispose: object with X, Y of the last line add to the PDF
// this allow the insertion of new lines after html
var name = document.getElementById("name").innerHTML;
pdf.save(name);
}, margins);
setTimeout("window.location.reload()",0.0000001);
}
With this code btw, $(document).find('tfoot').remove(); $('#table td:nth-child(8)').remove(); I remove my footer and the 8th column of my table.