I built an app which consumes data from a redis channel(sellers) with socketio and push the data in realtime to the frontend. The dataset could contain up to a thousand rows so I'm thinking about using a datatable to represent the data in a clean way. The table elements will be updated regularly, but there will be no rows to add/remove, only updates.
The problem I'm facing is that I don't know which would be the proper way to implement it due to my inexperience in the visualization ecosystem. I've been toying with d3js but I think It'll be too difficult to have something ready quickly and also tried using the datatables js library but I failed to see how to make the datatable realtime.
This is the code excerpt from the front end:
socket.on('sellers', function(msg){
var seller = $.parseJSON(msg);
var sales = [];
var visits = [];
var conversion = [];
var items = seller['items'];
var data = [];
for(item in items) {
var item_data = items[item];
//data.push(item_data)
data.push([item_data['title'], item_data['today_visits'], item_data['sold_today'], item_data['conversion-rate']]);
}
//oTable.dataTable(data);
$(".chart").html("");
drawBar(data);
});
Using d3 to solve your problem is simple and elegant. I took a little time this morning to create a fiddle that you can adapt to your own needs:
http://jsfiddle.net/CelloG/47nxxhfu/
To use d3, you need to understand how it works with joining the data to the html elements. Check out http://bost.ocks.org/mike/join/ for a brief description by the author.
The code in the fiddle is:
var table = d3.select('#data')
// set up the table header
table.append('thead')
.append('tr')
.selectAll('th')
.data(['Title', 'Visits', 'Sold', 'Conversion Rate'])
.enter()
.append('th')
.text(function (d) { return d })
table.append('tbody')
// set up the data
// note that both the creation of the table AND the update is
// handled by the same code. The code must be run on each time
// the data is changed.
function setupData(data) {
// first, select the table and join the data to its rows
// just in case we have unsorted data, use the item's title
// as a key for mapping data on update
var rows = d3.select('tbody')
.selectAll('tr')
.data(data, function(d) { return d.title })
// if you do end up having variable-length data,
// uncomment this line to remove the old ones.
// rows.exit().remove()
// For new data, we create rows of <tr> containing
// a <td> for each item.
// d3.map().values() converts an object into an array of
// its values
var entertd = rows.enter()
.append('tr')
.selectAll('td')
.data(function(d) { return d3.map(d).values() })
.enter()
.append('td')
entertd.append('div')
entertd.append('span')
// now that all the placeholder tr/td have been created
// and mapped to their data, we populate the <td> with the data.
// First, we split off the individual data for each td.
// d3.map().entries() returns each key: value as an object
// { key: "key", value: value}
// to get a different color for each column, we set a
// class using the attr() function.
// then, we add a div with a fixed height and width
// proportional to the relative size of the value compared
// to all values in the input set.
// This is accomplished with a linear scale (d3.scale.linear)
// that maps the extremes of values to the width of the td,
// which is 100px
// finally, we display the value. For the title entry, the div
// is 0px wide
var td = rows.selectAll('td')
.data(function(d) { return d3.map(d).entries() })
.attr('class', function (d) { return d.key })
// the simple addition of the transition() makes the
// bars update smoothly when the data changes
td.select('div')
.transition()
.duration(800)
.style('width', function(d) {
switch (d.key) {
case 'conversion_rate' :
// percentage scale is static
scale = d3.scale.linear()
.domain([0, 1])
.range([0, 100])
break;
case 'today_visits':
case 'sold_today' :
scale = d3.scale.linear()
.domain(d3.extent(data, function(d1) { return d1[d.key] }))
.range([0, 100])
break;
default:
return '0px'
}
return scale(d.value) + 'px'
})
td.select('span')
.text(function(d) {
if (d.key == 'conversion_rate') {
return Math.round(100*d.value) + '%'
}
return d.value
})
}
setupData(randomizeData())
d3.select('#update')
.on('click', function() {
setupData(randomizeData())
})
// dummy randomized data: use this function for the socketio data
// instead
//
// socket.on('sellers', function(msg){
// setupData(JSON.parse(msg).items)
// })
function randomizeData() {
var ret = []
for (var i = 0; i < 1000; i++) {
ret.push({
title: "Item " + i,
today_visits: Math.round(Math.random() * 300),
sold_today: Math.round(Math.random() * 200),
conversion_rate: Math.random()
})
}
return ret
}
Related
Im trying to use PDFKit to generate a simple pdf, for the most part the pdf works but albeit in a very non useful way, what i have is a deck building API that takes in a number of cards, each of these objects i want to export to a pdf, its as simple as displaying their name, but as it is, the pdf only renders one card at a time, and only on one line, what id like to happen is to get it to split the text into columns so itd look similar to this.
column 1 | column 2
c1 c8
c2 c9
c3 c10
c4 c(n)
here is my code so far,
module.exports = asyncHandler(async (req, res, next) => {
try {
// find the deck
const deck = await Deck.findById(req.params.deckId);
// need to sort cards by name
await deck.cards.sort((a, b) => {
if (a.name < b.name) {
return -1;
} else if (a.name > b.name) {
return 1;
} else {
return 0;
}
});
// Create a new PDF document
const doc = new PDFDocument();
// Pipe its output somewhere, like to a file or HTTP response
doc.pipe(
fs.createWriteStream(
`${__dirname}/../../public/pdf/${deck.deck_name}.pdf`
)
);
// Embed a font, set the font size, and render some text
doc.fontSize(25).text(`${deck.deck_name} Deck List`, {
align: "center",
underline: true,
underlineColor: "#000000",
underlineThickness: 2,
});
// We need to create two columns for the cards
// The first column will be the card name
// The second column will continue the cards listed
const section = doc.struct("P");
doc.addStructure(section);
for (const card of deck.cards) {
doc.text(`${card.name}`, {
color: "#000000",
fontSize: 10,
columns: 2,
columnGap: 10,
continued: true,
});
}
section.end();
// finalize the PDF and end the response
doc.end();
res.status(200).json({ message: "PDF generated successfully" });
} catch (error) {
console.error(error);
res.status(500).json({
success: false,
message: `Server Error - ${error.message}`,
});
}
});
At Present this does generate a column order like i want, however theres and extreme caveat to this solution and that is, if the card text isnt very long, the next card will start on that same line, it'd be useful if i could find a way to make the text take up the full width of that row, but i havent seen anything to do that with.
I think the problem is that you're relying on PDFKit's text "flow" API/logic, and you're having problems when two cards are not big enough to flow across your columns and you get two cards in one column.
I'd say that what you really want is to create a table—based on your initial text sample.
PDFKit doesn't have a table API (yet), so you'll have to make one up for yourself.
Here's an approach where you figure out the dimensions of things:
the page size
the size of your cells of text (either manually choose for yourself, or use PDFKit to tell you how big some piece of text is)
margins
Then you use those sizes to calculate how many rows and columns of your text can fit on your page.
Finally you iterate of over columns then rows for each page, writing text into those column-by-row "coordinates" (which I track through "offsets" and use to calculate the final "position").
const PDFDocument = require('pdfkit');
const fs = require('fs');
// Create mock-up Cards for OP
const cards = [];
for (let i = 0; i < 100; i++) {
cards.push(`Card ${i + 1}`);
}
// Set a sensible starting point for each page
const originX = 50;
const originY = 50;
const doc = new PDFDocument({ size: 'LETTER' });
// Define row height and column widths, based on font size; either manually,
// or use commented-out heightOf and widthOf methods to dynamically pick sizes
doc.fontSize(24);
const rowH = 50; // doc.heightOfString(cards[cards.length - 1]);
const colW = 150; // doc.widthOfString(cards[cards.length - 1]); // because the last card is the "longest" piece of text
// Margins aren't really discussed in the documentation; I can ignore the top and left margin by
// placing the text at (0,0), but I cannot write below the bottom margin
const pageH = doc.page.height;
const rowsPerPage = parseInt((pageH - originY - doc.page.margins.bottom) / rowH);
const colsPerPage = 2;
var cardIdx = 0;
while (cardIdx < cards.length) {
var colOffset = 0;
while (colOffset < colsPerPage) {
const posX = originX + (colOffset * colW);
var rowOffset = 0;
while (rowOffset < rowsPerPage) {
const posY = originY + (rowOffset * rowH);
doc.text(cards[cardIdx], posX, posY);
cardIdx += 1;
rowOffset += 1;
}
colOffset += 1;
}
// This is hacky, but PDFKit adds a page by default so the loop doesn't 100% control when a page is added;
// this prevents an empty trailing page from being added
if (cardIdx < cards.length) {
doc.addPage();
}
}
// Finalize PDF file
doc.pipe(fs.createWriteStream('output.pdf'));
doc.end();
When I run that I get a PDF with 4 pages that looks like this:
Changing colW = 250 and colsPerPage = 3:
I figured an issue, while i have thousands of pins over the map, i am using drawing tool to draw shapes free hand and then executing the Intersection on "drawingEnded" event, While i could see the intersection should return more than it actually returns,
Am i missing something ? For Example, If there are around 500 pins under the new area drawn, Intersection method only returns 100 or few more,
My Spider Cluster Configuration:
` Microsoft.Maps.loadModule(['SpiderClusterManager'], function () {
spiderManager = new SpiderClusterManager(map, pinssame, {
//clusteredPinCallback: function (cluster) {
// //Customize clustered pushpin.
// cluster.setOptions({
// color: 'red',
// icon:'https://www.bingmapsportal.com/Content/images/poi_custom.png'
// });
//},
pinSelected: function (pin, cluster) {
if (cluster) {
showInfobox(cluster.getLocation(), pin);
} else {
showInfobox(pin.getLocation(), pin);
}
},
pinUnselected: function () {
hideInfobox();
},
gridSize: 80
});
});
`
Intersection Function Code which gets triggered after "drawingEnded" event:
` function findIntersectingData(searchArea) {
//Ensure that the search area is a valid polygon, should have 4 Locations in it's ring as it automatically closes.
if (searchArea && searchArea.getLocations().length >= 4) {
//Get all the pushpins from the pinLayer.
//var pins = spiderManager._data;
//Using spatial math find all pushpins that intersect with the drawn search area.
//The returned data is a copy of the intersecting data and not a reference to the original shapes,
//so making edits to them will not cause any updates on the map.
var intersectingPins = Microsoft.Maps.SpatialMath.Geometry.intersection(pins, searchArea);
//The data returned by the intersection function can be null, a single shape, or an array of shapes.
if (intersectingPins) {
//For ease of usem wrap individudal shapes in an array.
if (intersectingPins && !(intersectingPins instanceof Array)) {
intersectingPins = [intersectingPins];
}
var selectedPins = [];
//Loop through and map the intersecting pushpins back to their original pushpins by comparing their coordinates.
for (var j = 0; j < intersectingPins.length; j++) {
for (var i = 0; i < pins.length; i++) {
if (Microsoft.Maps.Location.areEqual(pins[i].getLocation(), intersectingPins[j].getLocation())) {
selectedPins.push(pins[i]);
break;
}
}
}
//Return the pushpins that were selected.
console.log(selectedPins);
return selectedPins;
}
}
return null;
}
`
The function is not returning accurate pin data,
Am i missing something here ?
Any Help Appreciated,
Thanks & Regards,
Shohil Sethia
UPDATE :
Just figured, It is an assumption ,I have multiple pins with same coordinates over the layer, Is this the reason that it returns only pins which intersects with different coordinates over the map ?,
Thanks & Regards,
Shohil Sethia
The method returns objects that represent the intersection, not the exact copies of input shapes. So yes, if multiple pushpins with the same coordinates are within the area, only one pushpin of that coordinates will be in the result, since that alone is good enough as a representation.
You can try the sample below, only one pushpin is returned:
// Creates a polygon of current map bounds
var polygon = new Microsoft.Maps.SpatialMath.locationRectToPolygon(map.getBounds());
// Creates a bunch of the pushpins of the same coordinates(map center)
var pushpin1 = new Microsoft.Maps.Pushpin(map.getCenter());
var pushpin2 = new Microsoft.Maps.Pushpin(map.getCenter());
var pushpin3 = new Microsoft.Maps.Pushpin(map.getCenter());
var pushpin4 = new Microsoft.Maps.Pushpin(map.getCenter());
var pushpin5 = new Microsoft.Maps.Pushpin(map.getCenter());
// Adds the shapes to map for some visualization
map.entities.push([polygon, pushpin1, pushpin2, pushpin3, pushpin4, pushpin5]);
// Only one pushpin is returned as result
var intersectingPin = Microsoft.Maps.SpatialMath.Geometry.intersection([pushpin1, pushpin2, pushpin3, pushpin4, pushpin5], polygon);
Have you checked if the number of results adds up when taking duplicate pins into account?
I got a solution, Since Intersection API ignore multiple pushPins with same coordinates, Therefore there is another API named as contains which takes two parameters which are the shape and the pushpin, and it returns whether it is contained in that shape or not in a boolean form. So true if pushpin is in that shape, and false in the other way.
function findIntersectingData(searchArea) {
//Ensure that the search area is a valid polygon, should have 4 Locations in it's ring as it automatically closes.
if (searchArea && searchArea.getLocations().length >= 4) {
var selectedPins = [];
for (var i = 0; i < pins.length; i++) {
if (Microsoft.Maps.SpatialMath.Geometry.contains(searchArea, pins[i])) {
selectedPins.push(pins[i]);
}
}
//Return the pushpins that were selected.
console.log(selectedPins);
//return updatePrescriberTerr(selectedPins);
return selectedPins;
}
return null;
}
Therefore in the above function the we can loop it from the pushPins array and form the intersection set accordingly based on the boolean values.
Hope it helps to those with similar scenario !
Regards,
Shohil Sethia
I used to manage object alignments selection on FabricJS with getActiveGroup as below :
canvas.on("selection:created", function(e) {
var activeObj = canvas.getActiveObject();
console.log('e.target.type', e.target.type);
if(activeObj.type === "group") {
console.log("Group created");
var groupWidth = e.target.getWidth();
var groupHeight = e.target.getHeight();
e.target.forEachObject(function(obj) {
var itemWidth = obj.getBoundingRect().width;
var itemHeight = obj.getBoundingRect().height;
$('#objAlignLeft').click(function() {
obj.set({
left: -(groupWidth / 2),
originX: 'left'
});
obj.setCoords();
canvas.renderAll();
});
...
}
});
more detailed here
But now that I use FabricJS 2 and that getActiveObject() has been removed, I don't know what to do. I read on the doc that we could use getActiveObjects(), but it does nothing.
Please how can I reproduce the action of this code with FabricJS 2 (where getActiveGroup isn't supported anymore) ?
Selections of more than one object have the type activeSelection. The group type is only used when you purposefully group multiple objects using new fabric.Group([ obj1, obj2]
When you create a multi-selection using the shift-key as opposed to drawing a selection box, you'll trigger the selection:created event only on the first object selected, while objects added to the selection will trigger the selection:updated event. By calling your alignment code from both the selection:created and selection:updated events you'll make sure that your code is executed every time a multi-selection is created.
Also, you can use getScaledWidth() and getScaledHeight() to get scaled dimensions, or just .width and .height if you just want the unscaled width/height values. Good luck!
canvas.on({
'selection:updated': function() {
manageSelection();
},
'selection:created': function() {
manageSelection();
}
});
function manageSelection() {
var activeObj = canvas.getActiveObject();
console.log('activeObj.type', activeObj.type);
if (activeObj.type === "activeSelection") {
console.log("Group created");
var groupWidth = activeObj.getScaledWidth();
var groupHeight = activeObj.getScaledHeight();
activeObj.forEachObject(function(obj) {
var itemWidth = obj.getBoundingRect().width;
var itemHeight = obj.getBoundingRect().height;
console.log(itemWidth);
$('#objAlignLeft').click(function() {
obj.set({
left: -(groupWidth / 2),
originX: 'left'
});
obj.setCoords();
canvas.renderAll();
});
});
}
}
I use the node module "query-overpass" for a query to get farmshops from openstreetmaps. I would like to convert all polygons to points inside this script. I use turf.js to get the centroids of theese polygons, but I am not able to change the objects in a permanent way. This is my code so far:
const query_overpass = require("query-overpass");
const turf = require ("turf");
const fs = require("fs")
let test
let filename = "data/test.js"
let bbox = "48.91821286473131,8.309097290039062,49.0610446187357,8.520584106445312";
console.log('starting query for ' +filename)
console.log('bbox: ' +bbox)
let query = `
[out:json][timeout:250];
// gather results
(
// query part for: “vending=milk”
node["vending"="milk"](${bbox});
way["vending"="milk"](${bbox});
relation["vending"="milk"](${bbox});
// query part for: “shop=farm”
node["shop"="farm"](${bbox});
way["shop"="farm"](${bbox});
relation["shop"="farm"](${bbox});
// query part for: “vending=food”
node["vending"="food"](${bbox});
way["vending"="food"](${bbox});
relation["vending"="food"](${bbox});
);
// print results
out body;
>;
out skel qt;
`;
// query overpass, write result to file
query_overpass(query, (error, data) => {
data = JSON.stringify(data , null, 1)
console.log(data)
test = JSON.parse(data)
//create centroids for every polyon and save them as a point
for (var i = 0; i < test.features.length; i++) {
console.log("Log: " +test.features[i].geometry.type)
console.log("Log: " +test.features[i].properties.name)
if (test.features[i].geometry.type === "Polygon"){
console.log("polygon detected")
var centroid = turf.centroid(test.features[i]);
var lon = centroid.geometry.coordinates[0];
var lat = centroid.geometry.coordinates[1];
console.log(" lon: " +lon +" lat: " +lat)
test.features[i].geometry.type = 'Point'
//delete Polygon structure and insert centroids as new points here
console.log("polygon deleted and changed to point")
}
}
console.log(test)
fs.writeFile(filename, `var file = ${test};` , ["utf-8"], (error, data) => {if (error) {console.log(error)}})
}, {flatProperties: true}
)
It seems like I can change things inside of the for loop, but they do not appear when the data is saved later. It is basically a question of how to edit json objects properly, but I can't figure out why this doesnt work here at all.
So there are basically two questions:
Why cant I override geometry.type in the example above?
How can I delete the old polygon and add a new point to a feature?
Thanks for any help.
That's quite complicated... Why don't you let Overpass API do this job and use out center; instead of out body;>;out skel qt; to return the center points of all nodes, ways and relations. You can use overpass-turbo.eu to try this out first.
I can't find any code example or docs that answers this:
Achieve almost complete infinite scroll -> unknown # of items, but there is a finite amount that may be infeasible to compute beforehand - e.g. at some point the list needs to stop scrolling
Can I trigger first load of data from within InfiniteScroller/List - it seems you need to pass in a data source that is populated with initial page
I am using this example:
https://github.com/bvaughn/react-virtualized/blob/master/docs/creatingAnInfiniteLoadingList.md
and:
https://github.com/bvaughn/react-virtualized/blob/master/source/InfiniteLoader/InfiniteLoader.example.js
along with CellMeasurer for dynamic height:
https://github.com/bvaughn/react-virtualized/blob/master/source/CellMeasurer/CellMeasurer.DynamicHeightList.example.js
The docs for InfiniteLoader.rowCount say:
"Number of rows in list; can be arbitrary high number if actual number is unknown."
So how do you indicate there are no more rows.
If anyone can post an example using setTimeout() to simulate dynamic loaded data, thanks. I can likely get CellMeasurer working from there.
Edit
This doesn't work the way react-virtualized creator says it should or the infinite loading example implies.
Calls:
render(): rowCount = 1
_rowRenderer(index = 0)
_isRowLoaded(index = 0)
_loadMoreRows(startIndex = 0, stopIndex = 0)
_rowRenderer(index = 0)
end
Do I need to specify a batch size or some other props?
class HistoryBrowser extends React.Component
{
constructor(props,context,updater)
{
super(props,context,updater);
this.eventEmitter = new EventEmitter();
this.eventEmitter.extend(this);
this.state = {
history: []
};
this._cache = new Infinite.CellMeasurerCache({
fixedWidth: true,
minHeight: 50
});
this._timeoutIdMap = {};
_.bindAll(this,'_isRowLoaded','_loadMoreRows','_rowRenderer');
}
render()
{
let rowCount = this.state.history.length ? (this.state.history.length + 1) : 1;
return <Infinite.InfiniteLoader
isRowLoaded={this._isRowLoaded}
loadMoreRows={this._loadMoreRows}
rowCount={rowCount}
>
{({ onRowsRendered, registerChild }) =>
<Infinite.AutoSizer disableHeight>
{({ width }) =>
<Infinite.List
ref={registerChild}
deferredMeasurementCache={this._cache}
height={200}
onRowsRendered={onRowsRendered}
rowCount={rowCount}
rowHeight={this._cache.rowHeight}
rowRenderer={this._rowRenderer}
width={width}
/>}
</Infinite.AutoSizer>}
</Infinite.InfiniteLoader>
}
_isRowLoaded({ index }) {
if (index == 0 && !this.state.history.length)
// No data yet, force load
return false;
}
_loadMoreRows({ startIndex, stopIndex }) {
let self = this;
for (let i = startIndex; i <= stopIndex; i++) {
this.state.history[startIndex] = {loading: true};
}
const timeoutId = setTimeout(() => {
delete this._timeoutIdMap[timeoutId];
for (let i = startIndex; i <= stopIndex; i++) {
self.state.history[i] = {loading: false, text: 'Hi ' + i };
}
promiseResolver();
}, 10000);
this._timeoutIdMap[timeoutId] = true;
let promiseResolver;
return new Promise(resolve => {
promiseResolver = resolve;
});
}
_rowRenderer({ index, key, style }) {
let content;
if (index >= this.state.history.length)
return <div>Placeholder</div>
else if (this.state.history[index].loading) {
content = <div>Loading</div>;
} else {
content = (
<div>Loaded</div>
);
}
return (
<Infinite.CellMeasurer
cache={this._cache}
columnIndex={0}
key={key}
rowIndex={index}
>
<div key={key} style={style}>{content}</div>
</Infinite.CellMeasurer>
);
}
}
The recipe you linked to should be a good starting place. The main thing its missing is an implementation of loadNextPage but that varies from app to app based on how your state/data management code works.
Can I trigger first load of data from within InfiniteScroller/List - it seems you need to pass in a data source that is populated with initial page
This is up to you. IMO it generally makes sense to just fetch the first "page" of records without waiting for InfiniteLoader to ask for them- because you know you'll need them. That being said, if you give InfiniteLoader a rowCount of 1 and then return false from isRowLoaded it should request the first page of records. There are tests confirming this behavior in the react-virtualized GitHub.
The docs for InfiniteLoader.rowCount say: "Number of rows in list; can be arbitrary high number if actual number is unknown."
So how do you indicate there are no more rows.
You stop adding +1 to the rowCount, like the markdown file you linked to mentions:
// If there are more items to be loaded then add an extra row to hold a
loading indicator.
const rowCount = hasNextPage
? list.size + 1
: list.size