How to render a graph as image in node - node.js

I want to render a stacked bar graph in image format on the server.
The intended use is to push to a service like twitter that doesn't support SVG. As well as the code being being deployable to services like Heroku
I've already tried Plotly (their node package is horribly out of date and their API docs poor). I've also looked at Google Graph, Chart.js and AnyChart but they do not support rendering images far as I can see

You can accomplish this with Vega
Vega is a visualization grammar, a declarative format for creating, saving, and sharing interactive visualization designs. With Vega you can describe data visualizations in a JSON format, and generate interactive views using either HTML5 Canvas or SVG.
For example, using the stacked bar chart example spec you can render the chart to PNG file with the following code:
// START vega-demo.js
var vega = require('vega')
var fs = require('fs')
var stackedBarChartSpec = require('./stacked-bar-chart.spec.json');
// create a new view instance for a given Vega JSON spec
var view = new vega
.View(vega.parse(stackedBarChartSpec))
.renderer('none')
.initialize();
// generate static PNG file from chart
view
.toCanvas()
.then(function (canvas) {
// process node-canvas instance for example, generate a PNG stream to write var
// stream = canvas.createPNGStream();
console.log('Writing PNG to file...')
fs.writeFile('stackedBarChart.png', canvas.toBuffer())
})
.catch(function (err) {
console.log("Error writing PNG to file:")
console.error(err)
});
// END vega-demo.js
// START stacked-bar-chart.spec.json
{
"$schema": "https://vega.github.io/schema/vega/v3.0.json",
"width": 500,
"height": 200,
"padding": 5,
"data": [
{
"name": "table",
"values": [
{"x": 0, "y": 28, "c":0}, {"x": 0, "y": 55, "c":1},
{"x": 1, "y": 43, "c":0}, {"x": 1, "y": 91, "c":1},
{"x": 2, "y": 81, "c":0}, {"x": 2, "y": 53, "c":1},
{"x": 3, "y": 19, "c":0}, {"x": 3, "y": 87, "c":1},
{"x": 4, "y": 52, "c":0}, {"x": 4, "y": 48, "c":1},
{"x": 5, "y": 24, "c":0}, {"x": 5, "y": 49, "c":1},
{"x": 6, "y": 87, "c":0}, {"x": 6, "y": 66, "c":1},
{"x": 7, "y": 17, "c":0}, {"x": 7, "y": 27, "c":1},
{"x": 8, "y": 68, "c":0}, {"x": 8, "y": 16, "c":1},
{"x": 9, "y": 49, "c":0}, {"x": 9, "y": 15, "c":1}
],
"transform": [
{
"type": "stack",
"groupby": ["x"],
"sort": {"field": "c"},
"field": "y"
}
]
}
],
"scales": [
{
"name": "x",
"type": "band",
"range": "width",
"domain": {"data": "table", "field": "x"}
},
{
"name": "y",
"type": "linear",
"range": "height",
"nice": true, "zero": true,
"domain": {"data": "table", "field": "y1"}
},
{
"name": "color",
"type": "ordinal",
"range": "category",
"domain": {"data": "table", "field": "c"}
}
],
"axes": [
{"orient": "bottom", "scale": "x", "zindex": 1},
{"orient": "left", "scale": "y", "zindex": 1}
],
"marks": [
{
"type": "rect",
"from": {"data": "table"},
"encode": {
"enter": {
"x": {"scale": "x", "field": "x"},
"width": {"scale": "x", "band": 1, "offset": -1},
"y": {"scale": "y", "field": "y0"},
"y2": {"scale": "y", "field": "y1"},
"fill": {"scale": "color", "field": "c"}
},
"update": {
"fillOpacity": {"value": 1}
},
"hover": {
"fillOpacity": {"value": 0.5}
}
}
}
]
}
// END stacked-bar-chart.spec.json
Will output PNG file:

I capture charts, visualizations and reports under Node.js using the Nightmare headless browser.
Using Nightmare allows you to use any of the wide variety of browser-based visualization frameworks under Node.js, including C3 and D3 which are both awesome.
I've actually created a npm module called c3-chart-maker that wraps up Nightmare and allows you to render a chart under Node.js by feeding it some data and a C3 chart definition.
Install it like this:
npm install --save c3-chart-maker
Use it like this:
const c3ChartMaker = require('c3-chart-maker');
const yourData = ... your data ...
const chartDefinition = { ... c3 chart definition ... }
const outputFilePath = "your-chart-output-file.png";
c3ChartMaker(yourData, chartDefinition, outputFilePath)
.then(() => {
console.log('Done');
})
.catch(err => {
console.error(err);
});
Please check out the C3 example gallery for examples of charts and to see what a C3 chart definition looks like.
You can also use Nightmare manually to be able to capture any web page or browser-based visualization.
To install Nightmare:
npm install --save nightmare
Here's an example that can capture a web page:
const Nightmare = require('nightmare');
// This is the web page to capture.
// It can also be a local web server!
// Or serve from the file system using file://
const urlToCapture = "http://my-visualization.com";
const outputFilePath = "your-chart-output-file.png";
const nightmare = new Nightmare(); // Create Nightmare instance.
nightmare.goto(urlToCapture) // Point the browser at the requested web page.
.wait("svg") // Wait until the specified HTML element appears on the screen.
.screenshot(outputImagePath) // Capture a screenshot to an image file.
.end() // End the Nightmare session. Any queued operations are completed and the headless browser is terminated.
.then(() => {
console.log("Done!");
})
.catch(err => {
console.error(err);
});
I've written more extensively about this on my blog.
I've also dedicated a whole chapter to this in my book Data Wrangling with JavaScript.

Simple Headless NodeJS (not localhost or web-based)
For my purposes, I wanted to just plot a chart without spinning up a localhost server or anything. So I used chartjs-node-canvas and chart.js
Install with:
npm i chartjs-node-canvas chart.js
In this I write it to a file to show it worked but I personally just needed the Base64 string to upload somewhere
// Install libs with: npm i chartjs-node-canvas chart.js
// Docs https://www.npmjs.com/package/chartjs-node-canvas
// Config documentation https://www.chartjs.org/docs/latest/axes/
const fs = require('fs');
const { ChartJSNodeCanvas } = require('chartjs-node-canvas');
const width = 400; //px
const height = 400; //px
const backgroundColour = 'white'; // Uses https://www.w3schools.com/tags/canvas_fillstyle.asp
const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, backgroundColour });
const configuration = {
type: 'line', // for line chart
data: {
labels: [2018, 2019, 2020, 2021],
datasets: [{
label: "Sample 1",
data: [10, 15, -20, 15],
fill: false,
borderColor: ['rgb(51, 204, 204)'],
borderWidth: 1,
xAxisID: 'xAxis1' //define top or bottom axis ,modifies on scale
},
{
label: "Sample 2",
data: [10, 30, 20, 10],
fill: false,
borderColor: ['rgb(255, 102, 255)'],
borderWidth: 1,
xAxisID: 'xAxis1'
},
],
},
options: {
scales: {
y: {
suggestedMin: 0,
}
}
}
}
async function run() {
const dataUrl = await chartJSNodeCanvas.renderToDataURL(configuration);
const base64Image = dataUrl
var base64Data = base64Image.replace(/^data:image\/png;base64,/, "");
fs.writeFile("out.png", base64Data, 'base64', function (err) {
if (err) {
console.log(err);
}
});
return dataUrl
}
run()
Here's the docs https://www.npmjs.com/package/chartjs-node-canvas and the Config documentation is here https://www.chartjs.org/docs/latest/axes/

Related

Can you 'filter' the columns from a second leftJoinAndMapOne?

Can the columns of a second leftJoinAndMapOne() be 'filtered' so that it only returns the columns or values that I need inside the object?
Query Builder:
const favorites = await this.favoritesRepo.createQueryBuilder('favorite')
.where('favorite.user_id = :token', { token })
.leftJoinAndMapOne('favorite.item', 'favorite.item_id', 'items')
.select(['favorite.id'])
.addSelect([
'items.title',
'items.description',
'items.price',
'items.stock',
])
.leftJoinAndMapOne(
'items.photos',
PhotosEntity,
'photos',
'favorite.item_id = photos.subject_id and photos.subject_type = :item',
{item: 'item'}
)
return paginate<Favorite>(favorites, options)
Output (GET):
//...
{
"id": 32,
"item": {
"title": "Foo",
"description": "Buzz",
"price": 250.99,
"stock": 10,
"photos": {
"id": 2,
"createdAt": "2021-12-04T04:55:02.408Z",
"url": "https://images.unsplash.com/photo-1633114128729-0a8dc13406b9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=870&q=80",
"width": 324,
"height": 281,
"size": 42,
"entityId": 18,
"entityType": "item",
"redirectUrl": "https://facebook.com/"
}
}
},
//...
Expected Output:
//...
{
"id": 32,
"item": {
"title": "Foo",
"description": "Buzz",
"price": 250.99,
"stock": 10,
"photos": {
"url": "https://images.unsplash.com/photo-1633114128729-0a8dc13406b9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=870&q=80"
}
}
},
//...
Any suggestions or changes that I should make to make the output as expected? 🤔
.leftJoinAndMapOne(
'items.image',
PhotosEntity,
'photos',
'favorite.item_id = photos.subject_id and photos.subject_type = :item',
{item: 'item'}
)
.addSelect('photos.url')
I tried with .select () or .addSelect () but it doesn't filter columns from photos.
I already resolved it
Query Builder:
const favorites = await this.favoritesRepo.createQueryBuilder('favorite')
.where('favorite.user_id = :token', { token })
.leftJoinAndMapOne('favorite.item', 'favorite.item_id', 'items')
.leftJoinAndMapOne(
'items.photos',
PhotosEntity,
'photos',
'favorite.item_id = photos.subject_id and photos.subject_type = :item',
{item: 'item'}
)
.select(['favorite.id'])
.addSelect([
'items.title',
'items.description',
'items.price',
'items.stock',
'photos.url',
])

MongoDB Array not displaying

I want to query my MongoDB database to only fetch all the values in my array using my user_id
I have tried using the (schema.name), it not working out
// #route GET api/user/package
// #desc Get all package
// #access Private
router.get('/package',
passport.authenticate('jwt', { session: false }),
(req, res) => {
Shipments.find({_id : req.user._id},{paymentStatus: "incomplete"})
.then(shipments => res.status(200).json(shipments.packages))
.catch(err =>res.status(404).json({ nopackages: 'No package found for you now' }));
});
Here is my expected result
"packages": [
{
"date": "2019-09-23T12:52:14.226Z",
"_id": "5d88bffe2a6ed7b8d9873548",
"category": "hazard",
"quantity": "10",
"description": "a valueablegoods",
"length": 10,
"height": 20,
"width": 12,
"weight": 12
},
{
"date": "2019-09-23T12:52:58.129Z",
"_id": "5d88c02a2a6ed7b8d9873549",
"category": "hazard",
"quantity": "10",
"description": "a valueablegoods",
"length": 10,
"height": 20,
"width": 12,
"weight": 12
}
],
but postman didn't return any value but brings a status 200, but returns nobody
Try Shipments.findOne()
This happens because you are getting a single user detail.
All i did was change find to findOne. This solve the problem
router.get('/package',
passport.authenticate('jwt', { session: false }),
(req, res) => {
Shipments.findOne({ $and: [{ _id : req.user._id }, { paymentStatus: "incomplete" }] })
.then(shipments => res.status(200).json(shipments.packages))
.catch(err =>res.status(404).json({ nopackages: 'No package found for you now' }));
});
it may help u

Creating images from text with Cloudinary, does not read style parameters

I followed the example in the cloudinary documentation but I did not detect the styles, what could be happening?
My code
cloudinary.v2.uploader.text("Sample Name",
{ public_id: "dark_name",
font_family: "Arial",
font_size: 18,
font_color: "red",
opacity: 90 },
function(error: any,result: any) { console.log(result)
});
Example Cloudinary
cloudinary.v2.uploader.text("Sample Name",
{ public_id: "dark_name",
font_family: "Arial",
font_size: 12,
font_color: "black",
opacity: 90 },
function(error,result) { console.log(result, error)
});
My result so far.
If you're trying to upload a font you should take a look at the following- https://cloudinary.com/product_updates/custom_fonts

How can we find out battery level info of Beacons

I am detecting beacons and getting JSON data from which I can get Mac Address of Beacon but I need information about battery level.
Currently I am using node js and using Noble module for searching beacons.
var https = require('https'),
myip = require('quick-local-ip'),
noble = require('noble'),
os = require('os'),
amqp = require('amqp');
noble.on('stateChange', function (state) {
if (state === 'poweredOn') {
console.log("start scanning");
noble.startScanning([], true);
} else {
noble.removeAllListener();
noble.stopScanning();
console.log("stop scanning, is Bluetooth on?");
}
noble.on('discover', function (peripheral) {console.log("peripheral - "+peripheral);}
Output -
{
"id": "ac233f244b34",
"address": "ac:23:3f:24:4b:34",
"addressType": "public",
"connectable": true,
"advertisement": {
"manufacturerData": {
"type": "Buffer",
"data": [76, 0, 2, 21, 226, 197, 109, 181, 223, 251, 72, 210, 176, 96, 208, 245, 167, 16, 150, 224, 0, 1, 0, 1, 129]
},
"serviceData": [{
"uuid": "feaa",
"data": {
"type": "Buffer",
"data": [32, 0, 12, 120, 24, 0, 0, 0, 251, 140, 1, 223, 146, 120]
}
}],
"serviceUuids": ["feaa"],
"solicitationServiceUuids": [],
"serviceSolicitationUuids": []
},
"rssi": -48,
"state": "disconnected"
}
How can I Get to know about Battery level from this data.

d3.js: zoom and drag failure

I'm using d3.js but I'm having a problem. When I zoom in SVG, is not commensurate. Also, after dragging, the handling of SVG I made; a further drifting. I'm giving the necessary code. What can I do to fix this code?
Code:
/* Tünelin bütün elemanları için bir dizi oluşturuldu
Daha sonra her ayrı eleman için dizi oluşturulup,
datalar bu doğrultuda çekilecektir.
*/
var tunnelElements = new Array();
tunnelElements = [{ "x": 0, "y": 0, "radius": 10, "color" : "green" },
{ "x": 25, "y": 25, "radius": 10, "color" : "green"},
{ "x": 50, "y": 50, "radius": 10, "color" : "green" },
{ "x": 75, "y": 75, "radius": 10, "color" : "green"},
{ "x": 100, "y": 100, "radius": 10, "color" : "green" },
{ "x": 125, "y": 125, "radius": 10, "color" : "green" },
{ "x": 62.5, "y": 62.5, "radius": 10, "color" : "red" },
{ "x": 0, "y": 125, "radius": 10, "color" : "purple" },
{ "x": 25, "y": 100, "radius": 10, "color" : "purple" },
{ "x": 50, "y": 75, "radius": 10, "color" : "purple" },
{ "x": 75, "y": 50, "radius": 10, "color" : "purple" },
{ "x": 100, "y": 25, "radius": 10, "color" : "purple" },
{ "x": 125, "y": 0, "radius": 10, "color" : "purple" }];
/* Tünel elemanları datadan çekilip circles dizisine
kopyalanmaktadır.
*/
var circles = []
for (var i = 0; i < tunnelElements.length; i++) {
circles[i] = tunnelElements[i];
};
console.log(circles);
var width = 719, height = 262;
var X = d3.scale.linear()
var Y = d3.scale.linear()
/* Semantic zoom için zoom değişkeni oluşturuldu */
var zoom = d3.behavior.zoom()
.x(X).y(Y)
.scaleExtent([1, 10])
.on("zoom", zoomed);
/* Alternatif zoom
.on("zoom", function () {
circle.attr("transform", transform)
});
*/
/* Elementler svg olarak oluşturuldu */
var svg = d3.select("#d3")
.append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
/* Road background çağırdık */
var road = svg.selectAll("image").data([0]);
road.enter()
.append("svg:image")
.attr("xlink:href", "http://ahmetates.com.tr/road.svg")
.attr("width", width)
.attr("height", height);
var circle;
/* Bütün elemenlar seçilip transform fonksiyonu
sayesinde drag and drop özelliği kazandı */
circle = svg.selectAll("circle")
.data(circles)
.enter().append("circle")
.attr("transform", transform);
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
/* Fonksiyonlar */
function zoomed() {
road.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
circle.attr("transform", transform)
}
function transform(d) {
return "translate("+X(d.x)+", "+Y(d.y)+")";
}
function generateit(){
var circles = svg.selectAll("circle");
var json_circles = JSON.stringify(circles.data());
d3.select("#console").html('tunnelElements = '+json_circles+';');
}
d3.select("#exportit").on("click", generateit);
var circleAttributes = circle
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", function (d) { return d.radius; })
.style("fill", function(d) { return d.color; })
.call(drag);
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
#d3 svg{
margin:30px;
padding:10px;
border:1px solid #333;
width: 100%;
height: auto;
}
.line{
stroke-width: 3px;
}
#exportit {
background:#000;
cursor:pointer;
color:#fff;
width:45px;
padding:2px 4px;
}
<script src="http://d3js.org/d3.v3.min.js"></script>
<div id="exportit">Export</div>
<div id="d3"></div>
<div id="console"></div>
EDIT:
It seems that problem is not explained very well. The first problem can be traced like this: drag any circle, then drag background and you will see that dragged circle moved to random position. The second problem is scaling and you can trace that zooming in and out, then you can see that positions of circles are changed according to background.
You should make a main group and in that add all the components to which you want to give zoom and drag.
Something like this:
var maingroup = svg.append("g").call(zoom);
//add road and circles in this maingroup
//drag events on circle as you had done no change in that.
Here is a working fiddle this should give more clarity:
http://jsfiddle.net/cyril123/ow8r5n6f/3/

Resources