I'm working on a d3-force visualisation, which requires
data in a specific shape. I've got an array of objects, each with an array of tags.
nodes = [
{ name: "post1", tag_list: ["activity", "online"] },
{ name: "post2", tag_list: ["workshop", "online"] },
{ name: "post3", tag_list: ["english", "workshop"] },
...
]
To establish connections between data, I need to explicitly define an array of links:
links = [
{
source: 'post1',
target: 'post2'
},
{
source: 'post2',
target: 'post3'
},
...
]
There is no difference in similarity between links - all relationships are linear and carry the same "agency". Identical data should ideally be filtered to prevent duplicate lines.
How can I generate a link array of the previously mentioned shape from the tag_list arrays?
Here's an example of the required data structure.
--
Some context: I'm trying to visualise thematic overlaps between blog pages. All pages have an array of tags to describe them (tag_list). I wish to connect all tags within the graph. Since d3 requires verbose references to draw links (see link below), I need to compute these from the tag lists that are accessible to me.
You could collect each tag, and for each tag collect the distinct names (in a Set). When such a tag already has names associated to it, iterate those and pair it with the "current" name, putting the lexically smaller name as first pair-member. Store this pair in a map of Sets, so that they are unique.
Here is an implementation:
let nodes = [
{ name: "post1", tag_list: ["activity", "online"] },
{ name: "post2", tag_list: ["workshop", "online"] },
{ name: "post3", tag_list: ["english", "workshop"] },
];
let tags = {};
let pairs = {};
let result = [];
for (let {name, tag_list} of nodes) {
for (let tag of tag_list) {
for (let other of tags[tag] ??= new Set) {
let [source, target] = [name, other].sort();
if (!(pairs[source] ??= new Set).has(target)) {
pairs[source].add(target);
result.push({source, target});
}
}
tags[tag].add(name);
}
}
console.log(result);
You can use hash grouping approach. First make an object where keys are hashes of the links, and then use only the values as the result.
const nodes = [
{ name: "post1", tag_list: ["activity", "online"] },
{ name: "post2", tag_list: ["workshop", "online"] },
{ name: "post3", tag_list: ["online"] },
{ name: "post4", tag_list: ["workshop"] },
{ name: "post5", tag_list: ["lonely"] },
];
const hasIntersection = (arrA, arrB) => arrA.some((el) => arrB.includes(el));
const groupedByHash = nodes.reduce((acc, targetNode) => {
const commonNodes = nodes
.filter(({ tag_list }) => hasIntersection(tag_list, targetNode.tag_list))
.filter(({ name }) => name !== targetNode.name);
if (commonNodes.length < 1) return acc;
const commonLinks = commonNodes.reduce((acc, { name }) => {
const [source, target] = [name, targetNode.name].sort();
const hash = [source, target].join('---');
acc[hash] = { source, target };
return acc;
}, {});
return { ...acc, ...commonLinks };
}, {});
const result = Object.values(groupedByHash);
console.log(result);
.as-console-wrapper{min-height: 100%!important; top: 0}
How to modify my script, so in result there is no key "author" (I want to have only song and place)?
The songs' list looks like that (obviously I do not copy whole list)
const lp3 = [
{
author: 'Queen',
song: 'Bohemian Rhapsody',
place: 1,
change: 0
},
{
author: 'Deep Purple',
song: 'Child in time',
place: 2,
change: 2
},
and my script:
const exercise10 = _.groupBy(lp3, "author")
and the result is e.g.:
Pink Floyd': [
{
author: 'Pink Floyd',
song: 'Shine on you crazy diamond',
place: 12,
change: 4
},
{
author: 'Pink Floyd',
song: 'Comfortably numb',
place: 15,
change: 7
},
{ author: 'Pink Floyd', song: 'Hey you', place: 18, change: 11 },
{
author: 'Pink Floyd',
song: 'Another brick in the wall part II',
place: 21,
change: 10
}
],
Using Array.reduce() or lodash's _.reduce() you can destructure each object, and take out author, and then group the objects:
const lp3 = [{"author":"Queen","song":"Bohemian Rhapsody","place":1,"change":0},{"author":"Deep Purple","song":"Child in time","place":2,"change":2}]
const result = _.reduce(lp3, (acc, { author, ...o }) => {
if(!acc[author]) acc[author] = []
acc[author].push(o)
return acc
}, {})
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous"></script>
If you want to use _.groupBy() you'll need to group the items, and then map the groups with _.mapValues(), and map the items to omit author:
const lp3 = [{"author":"Queen","song":"Bohemian Rhapsody","place":1,"change":0},{"author":"Deep Purple","song":"Child in time","place":2,"change":2}]
const result1 = _.mapValues(
_.groupBy(lp3, 'author'),
values => _.map(values, o => _.omit(o, 'author')),
)
// or using chaining
const result2 = _(lp3)
.groupBy(lp3, 'author') // create the groups
.mapValues(values => // map the values of the groups x
_.map(values, o => _.omit(o, 'author')) // map the items
)
.value()
console.log(result1)
console.log(result2)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous"></script>
Consider, I have the following two arrays of objects:
const existingAndArchivedBookings =
[
{"booking_id":-2},
{"booking_id":-1},
{"booking_id":999}
]
const newAndExistingBookings =
[
{bookingId:-2, name: "name1"},
{bookingId:-3, name: "name1"},
{bookingId:-1, name: "namex"}
]
What I want to do is determine which of the bookings in the second array are new and which are existing. Any bookingId that is in both arrays is existing. Any bookingID that is in the second array but not the first is new. So, the result of the solution should be an array as follows:
[ { bookingId: -2, existing: true, name: 'name1' },
{ bookingId: -3, existing: false, name: 'name1' },
{ bookingId: -1, existing: true, name: 'namex' } ]
I have a solution (which I'll post as an answer), but I think there's probably a more efficient way of doing it. Good luck.
If you want a non-R answer: you can use a simple map to iterate over the data, compare the booking ids in both arrays (with some), and return a new array of objects.
const existingAndArchivedBookings = [{booking_id:-2},{booking_id:-1},{booking_id:999}];
const newAndExistingBookings = [{bookingId:-2, name: "name1"},{bookingId:-3, name: "name1"},{bookingId:-1, name: "namex"}];
function testBookings(arr1, arr2) {
return arr2.map(({ bookingId, name }) => {
const existing = arr1.some(obj => obj.booking_id === bookingId);
return { bookingId, existing, name };
});
}
const out = testBookings(existingAndArchivedBookings, newAndExistingBookings);
console.log(out);
You can greatly simplify it using Array.prototype.reduce to form the result of the comparisons between the 2 arrays and Array.prototype.findIndex to test whether the object in the second array is present in the first array:
const existingAndArchivedBookings =
[
{"booking_id":-2},
{"booking_id":-1},
{"booking_id":999}
]
const newAndExistingBookings =
[
{bookingId:-2, name: "name1"},
{bookingId:-3, name: "name1"},
{bookingId:-1, name: "namex"}
]
const res = newAndExistingBookings.reduce((acc, ele) => {
const idx = existingAndArchivedBookings.findIndex(b => b.booking_id === ele.bookingId);
let existing = false;
if(idx >=0 ){
existing = true;
}
return acc.concat({bookingId : `${ele.bookingId}`, existing: `${existing}`, name: `${ele.name}`});
}, []);
console.log(res);
Here's what I came up with, which seems a bit long winded
const R = require('ramda')
const existingAndArchivedBookings = [{"booking_id":-2},{"booking_id":-1},{"booking_id":999}]
const newAndExistingBookings = [{bookingId:-2, name: "name1"}, {bookingId:-3, name: "name1"}, {bookingId:-1, name: "namex"}]
const existingAndArchivedKeys = existingAndArchivedBookings.map(value => value.booking_id)
const newAndExistingKeys = newAndExistingBookings.map(value => value.bookingId)
const existingKeys = existingAndArchivedKeys.filter(key => newAndExistingKeys.includes(key))
const newKeys = newAndExistingKeys.filter(key => !existingAndArchivedKeys.includes(key))
const existingBookingIds = existingKeys.map(key => {
return {bookingId: key, existing: true}
})
const newBookingIds = newKeys.map(key => {
return {bookingId: key, existing: false}
})
const allArray = R.concat(newAndExistingBookings, R.concat(existingBookingIds, newBookingIds))
console.log(R.values(R.reduceBy(R.mergeLeft, {}, R.prop('bookingId'), allArray)))
I have two arrays
array1 = [{id:"1",title:"Writing"},{id:"2",title:"Singing"},{id:"3",title:"Dance"}];
array2 = [{tags: "1",title: "USA",type: "text"},
{tags: "1,2,3",title: "Japan",type: "image"},
{tags: "2,3",title: "Japan",type: "image"}];
I have to map the id of the array1 to tags of the array2 and display the corresponding title from the array1.
The new array2 should look like,
array2=[{tags:"Writing",title:"USA", type:"text"},
{tags: "Writing,Singing,Dance",title: "Japan",type: "image"},
{tags: "Singing,Dance",title: "Japan",type: "image"}];
I did this to get the array1 mapping and got stuck after that.
var newtags= (array1).map(obj=>{
var rObj={};
rObj[obj.id]=obj.title;
return rObj;
});
You can create a mapping object with each id as key and title as value using reduce. Then map over array2 and split each tags to get the new tags
const array1=[{id:"1",title:"Writing"},{id:"2",title:"Singing"},{id:"3",title:"Dance"}],
array2=[{tags:"1",title:"USA",type:"text"},{tags:"1,2,3",title:"Japan",type:"image"},{tags:"2,3",title:"Japan",type:"image"}]
const map = array1.reduce((r, { id, title }) => ({ ...r, [id]: title }), {});
const output = array2.map(({ tags, ...rest }) => {
const newTags = tags.split(',').map(id => map[id]).join(',')
return { tags: newTags, ...rest }
})
console.log(output)
You could also get the mapping object using Object.fromEntries()
const map = Object.fromEntries(array1.map(({ id, title }) => [id, title]));
Then use the regex /\d+(?=,|$)/ to match the numbers and replace them with their respective titles
const array1=[{id:"1",title:"Writing"},{id:"2",title:"Singing"},{id:"3",title:"Dance"}],
array2=[{tags:"1",title:"USA",type:"text"},{tags:"1,2,3",title:"Japan",type:"image"},{tags:"2,3",title:"Japan",type:"image"}]
const map = Object.fromEntries(array1.map(({ id, title }) => [id, title]));
const output = array2.map(({ tags, ...rest }) => {
const newTags = tags.replace(/\d+(?=,|$)/g, n => map[n])
return { tags: newTags, ...rest }
})
console.log(output)
Here's a solution
I'm using .map, .reduce and .replace to join array1 and array2 together.
const array1 = [
{
id: "1",
title: "Writing"
},
{
id: "2",
title: "Singing"
},
{
id: "3",
title: "Dance"
}
]
const array2 = [
{
tags: "1",
title: "USA",
type: "text"
},
{
tags: "1,2,3",
title: "Japan",
type: "image"
},
{
tags: "2,3",
title: "Japan",
type: "image"
}
]
const array3 =
array2.map(item => ({
...item,
tags: array1.reduce((tags, {id, title}) => tags.replace(id, title), item.tags),
}))
console.log(array3)
You can use filter, map and join method, split tags and filter tags in array1 first.
var newtags= (array2).map(obj=>{
let tags = obj.tags.split(",");
let titles = array1.filter(c=>tags.includes(c.id)).map(c=>c.title);
obj.tags = titles.join();
return obj;
});
array1 = [{id:"1",title:"Writing"},{id:"2",title:"Singing"},{id:"3",title:"Dance"}];
array2 = [{tags: "1",title: "USA",type: "text"},
{tags: "1,2,3",title: "Japan",type: "image"},
{tags: "2,3",title: "Japan",type: "image"}];
var newtags= (array2).map(obj=>{
let tags = obj.tags.split(",");
let titles = array1.filter(c=>tags.includes(c.id)).map(c=>c.title);
obj.tags = titles.join();
return obj;
});
console.log(newtags);
You can try following
Use Array.reduce to convert array1 into an object with id as key and title as value (Step 1)
Iterate over array2 using Array.forEach to update its tags property
To update tags property first split it by , to convert into an array
Map each value in array to its corresponding value in Object created in step 1
Join back the array with , and assign back to tags
let array1 = [{id:"1",title:"Writing"},{id:"2",title:"Singing"},{id:"3",title:"Dance"}];
let array2 = [{tags: "1",title: "USA",type: "text"},{tags: "1,2,3",title: "Japan",type: "image"},{tags: "2,3",title: "Japan",type: "image"}];
let obj = array1.reduce((a,c) => Object.assign(a, {[c.id] : c.title}), {});
array2.forEach(o => o.tags = o.tags.split(",").map(v => obj[v]).join(","));
console.log(array2);
To achieve expected result, use below option of looping array1 and replacing array2 tags with title
Loop Array1 using forEach
Replace array2 tags with each array1 title using array id
array1 = [{id:"1",title:"Writing"},{id:"2",title:"Singing"},{id:"3",title:"Dance"}];
array2 = [{tags: "1",title: "USA",type: "text"},
{tags: "1,2,3",title: "Japan",type: "image"},
{tags: "2,3",title: "Japan",type: "image"}];
array1.forEach(v =>{
const re = new RegExp(v.id, "g");
array2 = JSON.parse(JSON.stringify(array2).replace(re, v.title))
})
console.log(array2);
I would consider breaking this down into several reusable functions. Of course it might be premature abstraction, but I've seen variants of this questions like often enough here that it makes sense to me to look toward the fundamentals.
We want to be able to look up the values in a list stored as an array with what might be arbitrary field names. So we use a function makeDictionary that takes both the field names and the array and returns an object that maps them, such as {'1': 'Writing', '2': 'Singing',...}`.
Then we can use fillField supplying a dictionary, a field name, and an object, and replace that field with the result of looking up the tags in the dictionary. This is a little more specific to the problem, mostly because the comma-separated string format for your tags is a little more cumbersome than it might be if it were an array.
With these, useTags is simple to write, and it is the first function here focused directly on your needs. It combines the above, supplying the field names id and title for the dictionary and tags for your main objects.
This is what it looks like combined:
const makeDictionary = (keyName, valName) => (arr) =>
arr .reduce
( (a, {[keyName]: k, [valName]: v}) => ({...a, [k]: v})
, {}
)
const fillField = (field, dict) => ({[field]: f, ...rest}) => ({
...rest,
[field]: f .split (/,\s*/) .map (t => dict[t]) .join (', ')
})
const useTags = (tags, dict = makeDictionary ('id', 'title') (tags) ) =>
(objs) => objs .map ( fillField ('tags', dict) )
const tags = [{id: "1", title: "Writing"}, {id: "2", title: "Singing"}, {id: "3", title: "Dance"}];
const updateTags = useTags (tags)
const items = [{tags: "1", title: "USA", type: "text"}, {tags: "1, 2, 3", title: "Japan", type: "image"}, {tags: "2, 3", title: "Japan", type: "image"}];
console .log (
updateTags (items)
)
Note that I took a little liberty with the tags: "2,3" and tags: "Singing,Dance" formats, adding a little white space. It's trivial to take this out. But even better, if possible, would be to change this to use arrays for your tags.
You could take a real Map and map the values to the new objects.
var array1 = [{ id: "1", title: "Writing" }, { id: "2", title: "Singing" }, { id: "3", title: "Dance" }],
array2 = [{ tags: "1", title: "USA", type: "text" }, { tags: "1,2,3", title: "Japan", type: "image" }, { tags: "2,3", title: "Japan", type: "image" }],
tags = array1.reduce((m, { id, title }) => m.set(id, title), new Map),
result = array2.map(o => ({ ...o, tags: o.tags.split(',').map(Map.prototype.get, tags).join() }));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Wanted to compare two arrays with objects and remove duplicate by its property name, I have these two:
arr1 = [{
item:"apple",
description: "lorem"
},{
item:"peach",
description: "impsum"
}]
arr2 = [{
item:"apple", description: "dolor"
},{
item:"grape", description: "enum"
}]
and I wanted this result:
arr3 = [{
item:"peach", description: "impsum"
},{
item:"grape", description: "enum"
}]
I've tried this es6 approach but not working arr3 = arr1.filter(val => !arr2.includes(val));
Array.includes won't work because in javascript {} !== {}. You'll need another way like Array.every to check that every object in the other array doesn't have the same value of the property item as the current object. Also, you need to do both arr1.filter(...) and arr2.filter(...) and concat the results:
arr3 = [].concat(
arr1.filter(obj1 => arr2.every(obj2 => obj1.item !== obj2.item)),
arr2.filter(obj2 => arr1.every(obj1 => obj2.item !== obj1.item))
);
Example:
let arr1 = [{
item:"apple",
description: "lorem"
},{
item:"peach",
description: "impsum"
}];
let arr2 = [{
item:"apple", description: "dolor"
},{
item:"grape", description: "enum"
}];
let arr3 = [].concat(
arr1.filter(obj1 => arr2.every(obj2 => obj1.item !== obj2.item)),
arr2.filter(obj2 => arr1.every(obj1 => obj2.item !== obj1.item))
);
console.log(arr3);
See Set, Array.prototype.filter() and Spread Syntax for more info.
// Join Without Dupes.
const joinWithoutDupes = (A, B) => {
const a = new Set(A.map(x => x.item))
const b = new Set(B.map(x => x.item))
return [...A.filter(x => !b.has(x.item)), ...B.filter(x => !a.has(x.item))]
}
// Proof.
const output = joinWithoutDupes([{item:"apple",description: "lorem"},{item:"peach",description: "impsum"}], [{item:"apple", description: "dolor"},{item:"grape", description: "enum"}])
console.log(output)
a simple and faster sample:
arr1 = [{
item:"apple",
description: "lorem"
},{
item:"peach",
description: "impsum"
}]
arr2 = [{
item:"apple", description: "dolor"
},{
item:"grape", description: "enum"
}]
result = (counters = {}, arr1.concat(arr2).map(cur => (counters[cur.item] ? counters[cur.item]++ : (counters[cur.item] = 1), cur)).filter(cur => counters[cur.item] === 1))
console.log(result)