Related
i'm attempting to create a Tree Diagram with react-d3-js. It needs to be in a specific format. So i need to convert the initial data that i have to the format.
This is a diagram for a shop to see the distribution chain and who is allowed to make a purchase from specific nodes.
Initial Data:
store.name = 'Absolut Chocolat' //Main Parent
store.shopconditions: [
{
"role": "agent",
"condition": ["owner", "stokist"]
},
{
"role": "stokist",
"condition": ["owner", "master stokist"]
},
{
"role": "master stokist",
"condition": ["owner"]
}
]
// If role is agent, then they are allowed to buy from 'owner' and 'stokist'
Here's the hardcoded ideal output:
orgChart = {
name: 'Absolut Chocolat',
children: [
{ name: 'Agent' },
{
name: 'Stokist',
children: [
{
name: 'Agent',
},
],
},
{
name: 'Master Stokist',
children: [
{
name: 'Stokist',
children: [
{
name: 'Agent',
},
],
},
],
},
],
};
With a few for each loops, i've gotten to the first 2 layers of the intended output but i cannot find a way to get more than that.
Here is what i got so far:
Agent node is not under Master Stokist
Current code:
let chartData = { name: store.name, children: [] };
store.shopconditions.forEach((i) => {
i.condition.forEach((c) => {
if (c === 'owner') {
chartData.children.push({ name: i.role });
}
});
});
const chartDataParser = (data) => {
data.children.map((i) => {
for (const [k, v] of Object.entries(i)) {
store.shopconditions.forEach((c) => {
c.condition.forEach((o) => {
if (o === v) {
if (!i.children) {
i.children = [{ name: c.role }];
} else {
i.children.push({ name: c.role });
}
}
});
});
}
});
};
chartDataParser(chartData);
Current output:
{
name: 'Absolut Chocolat',
children: [
{ name: 'Agent' },
{
name: 'Stokist',
children: [
{
name: 'Agent',
},
],
},
{
name: 'Master Stokist',
children: [
{
name: 'Stokist',
// Missing children: Agent Node
},
],
},
],
};
What the tree diagram should look like:
As you can see under Master Stokist node, Agent is under Stokist
The Agent node is not reached under the stokist node in the right most chain. I need a fix to my current code so it can go to that extra layer. Thanks in advance. Looking forward to learn from your answers.
You can build an object that lists children by role and then use that to recursively build the nodes of the object. Possibly something like the following:
const store = {
name: 'Absolut Chocolat',
shopconditions: [
{ "role": "agent", "condition": ["owner", "stokist"], name: 'Agent' },
{ "role": "stokist", "condition": ["owner", "master stokist"], name: 'Stockist' },
{ "role": "master stokist", "condition": ["owner"], name: 'Master Stockist' },
]
};
const build_role_map = (store) => {
let role_map = Object.fromEntries(
store.shopconditions.map((v) => [v.role, { ...v, children: [] }])
);
role_map.owner = { "role": "owner", "condition": [], children: [], name: store.name };
store.shopconditions.forEach(
({ role, condition }) => {
condition.forEach((parent) => { role_map[parent].children.push(role) })
}
);
return role_map;
};
const build_node = (role_map, { name, children }) => {
let node = { name };
if(children.length > 0)
node.children = children.map((child) => build_node(role_map, role_map[child]));
return node;
};
const build_tree = (store) => {
const role_map = build_role_map(store);
return build_node(role_map, role_map.owner);
};
console.log(build_tree(store));
I have below mock data based on object property need to update their respective value using spread operator. I am not able to do that because it is nested.
I am able to achieve using below approach but as you can see code line is getting increased. But we can definitely reduce use spread operator. Because in "title" property i am only updating 0 index and in left column only updating the label value rest of the thing is same.
const mockData = {
title: ["Javascript", "Selected Topic"],
leftColumn: [
{
key: "name",
label: "Javascript Topic Name",
type: "string",
},
{
key: "id",
label: "Javasctipt Topic Id",
type: "string",
},
],
};
const handleType = (val) => {
switch (val.type) {
case "Javascript":
return {
...mockData,
};
case "Angular":
return {
...mockData,
title: ["Angular", "Selected Topic"],
leftColumn: [
{
key: "name",
label: "Angular Topic Name",
type: "string",
},
{
key: "id",
label: "Angular Topic Id",
type: "string",
},
],
};
case "React":
return {
...mockData,
title: ["React", "Selected Topic"],
leftColumn: [
{
key: "name",
label: "React Topic Name",
type: "string",
},
{
key: "id",
label: "Reacr Topic Id",
type: "string",
},
],
};
}
};
i am trying something below approach but not getting desired result
const handleType = (val) => {
const newArray = [mockData];
console.log("newArray", newArray);
// const index = mockData.findIndex((ele) => ele);
// console.log("index", index);
// newArray["title"][0] = "React";
// console.log("newArray ==>", newArray);
switch (val.type) {
case "Javascript":
return {
...mockData,
};
case "Angular":
mockData.title[0] = "Angular";
return mockData.leftColumn.map((val, index) => {
let value = { ...val, label: "Angular",};
return Object.assign({}, value);
});
case "React":
return {
...mockData,
title: ["React", "Selected Topic"],
leftColumn: [
{
key: "name",
label: "React Topic Name",
type: "string",
},
{
key: "id",
label: "Reacr Topic Id",
type: "string",
},
],
};
}
};
const type = {
type: "Angular",
};
console.log("Angular", handleType(type));
EDIT: Your solution worked but now my dropdown doesn't show what is selected unless its selected twice, like this:
Here I selected Health Care, but the dropdown still says "Select Industry"
The here I selected Health Care for a second time and it now shows that it is selected, this is happening with all of the dropdown options
I have a dropdown menu that I can select an option from, which saves the option to a variable. I want to use this variable to change the fetch route used in my useEffect hook so that the data found is filtered to a specific industry. How would I do this? Is there an easier way to filter the mapped data rather than changing the fetch address?
This is my code:
import React, {useState, useEffect} from "react";
import { AgGridReact } from "ag-grid-react";
import { Dropdown } from "semantic-ui-react";
import "ag-grid-community/dist/styles/ag-grid.css";
import "ag-grid-community/dist/styles/ag-theme-balham.css";
// Data Table
export default function Table() {
const[rowData, setRowData] = useState([]);
const columns = [
{ headerName: "Name", field: "name", sortable: true},
{ headerName: "Symbol", field: "symbol", sortable: true},
{ headerName: "Industry", field: "industry", sortable: true},
];
const searchIndustry = (event, {value}) => {
let industryChoice = value;
// I want to use this variable
console.log(industryChoice);
}
const DropdownSearchSelection = () => (
<Dropdown
placeholder="Select Industry"
search
selection
options={industryOptions}
onChange={searchIndustry}
/>
);
useEffect(() => {
// To change this address
fetch(`http://131.181.190.87:3000/stocks/symbols${industryChoice}`)
.then(res => res.json())
.then(data =>
data.map(stock => {
return {
name: stock.name,
symbol: stock.symbol,
industry: stock.industry,
};
})
)
.then(stock => setRowData(stock));
}, []);
const industryOptions = [
{ key: "as", value: "", text: "View All Industries" },
{ key: "dc", value: "?industry=consumer%20discretionary", text: "Consumer Discretionary" },
{ key: "ds", value: "?industry=consumer%20staples", text: "Consumer Staples" },
{ key: "en", value: "?industry=energy", text: "Energy" },
{ key: "fi", value: "?industry=einancials", text: "Financials" },
{ key: "hc", value: "?industry=health%20care", text: "Health Care" },
{ key: "in", value: "?industry=industrials", text: "Industrials" },
{ key: "it", value: "?industry=information%20technology", text: "Information Technology" },
{ key: "ma", value: "?industry=materials", text: "Materials" },
{ key: "re", value: "?industry=real%20estate", text: "Real Estate" },
{ key: "ts", value: "?industry=telecommunication%20services", text: "Telecommunication Services" },
{ key: "ut", value: "?industry=utilities", text: "Utilities" },
];
return (
<div className="filter">
<div>
<input type="text" id="searchBox" placeholder="Filter..."/>
<DropdownSearchSelection />
<br/>
</div>
<div className="ag-theme-balham" >
<AgGridReact
columnDefs={columns}
rowData={rowData}
pagination={true}
paginationPageSize={11}
/>
</div>
</div>
);
}
Thanks for the help! :)
First, you will need to store the selected option from Dropdown into your component's state.
const[industryChoice, setIndustryChoice] = useState();
And on your searchIndustry method,
const searchIndustry = (event, {value}) => {
setIndustryChoice(value);
}
And then, you add searchIndustry as part of the dependency array, which will fetch the data based on the industryChoice state, and this will be triggered whenever the value of industryChoice is updated.
useEffect(() => {
// To change this address
fetch(`http://131.181.190.87:3000/stocks/symbols${industryChoice}`)
.then(res => res.json())
.then(data =>
data.map(stock => {
return {
name: stock.name,
symbol: stock.symbol,
industry: stock.industry,
};
})
)
.then(stock => setRowData(stock));
}, [industryChoice]);
As for the additional issue you have raised, you should bind the value of the industryChoice state to the Dropdown.
<Dropdown
placeholder="Select Industry"
search
selection
options={industryOptions}
onChange={searchIndustry}
value={industryChoice}
/>
I am trying to refactor a codebase and I got stuck somewhere. I am basically trying to update the state based on the onChange event of a select box.
In this case, my searchCriteria parameter in my handleFilterChange functions is ingredients.
But let's assume I have another select box that contains the options of anotherIngredients. I couldn't spread the prevState based on a variable.
I had to type ...prevState.ingredients
Is there a proper way to do that?
So my component state is:
state = {
ingredients: {
name: "",
operation: "",
value: ""
},
anotherIngredients: {
name: "",
operation: "",
value: ""
}
};
My handler is:
handleFilterChange = (event, searchCriteria, searchCriteriaKey) => {
let newValue = event.target.value;
this.setState(prevState => ({
[searchCriteria]: {
...prevState.ingredients,
[searchCriteriaKey]: newValue
}
}));
};
My Select box component is :
<Select
value={ingredients.name}
options={[
{ key: "", value: "Please choose an ingredient" },
{ key: "ingredient1", value: "INGREDIENT 1" },
{ key: "ingredient2", value: "INGREDIENT 2" },
{ key: "ingredient3", value: "INGREDIENT 3" },
{ key: "ingredient4", value: "INGREDIENT 4" }
]}
changeHandler={event =>
this.handleFilterChange(event, "ingredients", "name")
}
/>
Hope I could explain myself. Thank you!
You should reuse your variable searchCriteria to extract the previous values from the state.
handleFilterChange = (event, searchCriteria, searchCriteriaKey) => {
let newValue = event.target.value;
this.setState(prevState => ({
[searchCriteria]: {
...prevState[searchCriteria],
[searchCriteriaKey]: newValue
}
}));
};
I've have a complex data structure with multiple nested arrays in place.
Below is the current structure
var contentData = {
data: {
content: [
{
type: "column",
sections: [
{
sub: [
{
type: "heading-1",
text: "Heading Text"
}
]
}
]
},
{
type: "acc-item",
sections: [
{
sub: [
{
type: "heading-1",
text: "Heading Text"
},
{
type: "ordered-item",
text: "Item 1"
},
{
type: "unordered-item",
text: "Item 2"
}
]
}
]
},
{
type: "acc-item",
sections: [
{
sub: [
{
type: "heading-1",
text: "Heading Text 2"
}
]
}
]
}
]
}
}
So What I wanted is,
I wanted to group all the ordered-item & unordered-item into a new object like {type: 'list', items:[all list items]}.
I need to extract all items which are inside sub and push it to new object embedded and it should placed in the root level like below,
{type:"acc-item",embedded:[{type:"heading-1",text:"Heading Text 2"}]};
So What I've done so far,
I can able to group acc-item, but not the ordered-item & unordered-item.
So my final expected result should like this,
[{
"type": "column",
"embedded": [
{
"type": "heading-1",
"text": "Heading Text"
}
]
},
{
"type": "acc-group",
"items": [
{
"type": "acc-item",
"embedded": [
{
"type": "heading-1",
"text": "Heading Text"
},
{
"type": "list",
"items": [
{
"type": "ordered-item",
"text": "Item 1"
},
{
"type": "unordered-item",
"text": "Item 2"
}
]
}
]
},
{
"type": "acc-item",
"embedded": [
{
"type": "heading-1",
"text": "Heading Text 2"
}
]
}
]
}]
Below is my code,
var group,contentData={data:{content:[{type:"column",sections:[{sub:[{type:"heading-1",text:"Heading Text"}]}]},{type:"acc-item",sections:[{sub:[{type:"heading-1",text:"Heading Text"},{type:"ordered-item",text:"Item 1"},{type:"unordered-item",text:"Item 2"}]}]},{type:"acc-item",sections:[{sub:[{type:"heading-1",text:"Heading Text 2"}]}]}]}},types=[["list",["ordered-item","unordered-item"]],["accordion",["acc-item"]]];
var result = contentData.data.content.reduce((r, o) => {
var type = (types.find(({ 1: values }) => values.indexOf(o.type) > -1)|| {})[0];
if (!type) {
r.push(o);
group = undefined;
return r;
}
if (!group || group.type !== type) {
group = { type, items: [] };
r.push(group);
}
group.items.push(o);
return r;
}, []);
document.body.innerHTML = '<pre>' + JSON.stringify(result, null, ' ') + '</pre>';
You could store the last items array as well as the last embedded array and use them until a column type is found.
var contentData = { data: { content: [{ type: "column", sections: [{ sub: [{ type: "heading-1", text: "Heading Text" }] }] }, { type: "acc-item", sections: [{ sub: [{ type: "heading-1", text: "Heading Text" }, { type: "ordered-item", text: "Item 1" }, { type: "unordered-item", text: "Item 2" }] }] }, { type: "acc-item", sections: [{ sub: [{ type: "heading-1", text: "Heading Text 2" }] }] }] } },
list = ["ordered-item", "unordered-item"],
lastItems, lastEmbedded,
result = contentData.data.content.reduce((r, { type, sections }) => {
if (type === 'column') {
r.push({ type, embedded: sections.reduce((q, { sub }) => q.concat(sub), []) });
lastItems = undefined;
lastEmbedded = undefined;
return r;
}
if (!lastItems) r.push({ type: "acc-group", items: lastItems = [] });
lastItems.push(...sections.map(({ sub }) => ({
type,
embedded: sub.reduce((q, o) => {
if (list.includes(o.type)) {
if (!lastEmbedded) q.push({ type: 'list', items: lastEmbedded = [] });
lastEmbedded.push(o);
} else {
q.push(o);
lastEmbedded = undefined;
}
return q;
}, [])
})));
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
The Array.prototype and Object.prototype methods are perfect for this kind of thing.
And you're right that this is some complicated kind of logic.
I would suggest that you definitely need some unit tests for this, and try break in to separate pieces.
Here's how I'm thinking I'd do it.
1. Group By the type to create your groups..
I'm actually creating a more generic solution that you've asked for here. That is, I'm not just grouping the 'acc-item', but everything.
I did a quick search for 'array group by javascript' and it gives us this answer which suggests using Array.reduce, so let's do that.
const groupedData = contentData.data.content.reduce((acc, cur) => {
//Check if this indexed array already exists, if not create it.
const currentArray = (acc[`${cur.type}-group`] && acc[`${cur.type}-group`].items) || [];
return {
...acc,
[`${cur.type}-group`]: {
type: `${cur.type}-group`,
items: [...currentArray, cur]
}
}
}, {});
2. Now for each of those items, we need to look at their subs, and group just the list items.
To do this, we basically want to find all the `item -> sections -> sub -> types and filter them into two arrays. A quick google on how to create two arrays using a filter gives me this answer.
First though, we need to flatten that sections-> subs thing, so lets just do that.
function flattenSectionsAndSubs(item) {
return {
type: item.type,
subs: item.sections.reduce((acc, cur) => ([...acc, ...cur.sub]), [])
};
}
And I'll just copy paste that partition function in:
function partition(array, isValid) {
return array.reduce(([pass, fail], elem) => {
return isValid(elem) ? [[...pass, elem], fail] : [pass, [...fail, elem]];
}, [[], []]);
}
const listTypes = ['ordered-item', 'unordered-item'];
function createEmbeddedFromItem(item) {
const [lists, nonLists] = partition(item.subs, (v) => listTypes.includes(v.type);
return {
type: item.type,
embedded: [
...nonLists,
{
type: "list",
items: lists
}
]
}
}
Putting this all together and we get.
const contentData = {
data: {
content: [{
type: "column",
sections: [{
sub: [{
type: "heading-1",
text: "Heading Text"
}]
}]
},
{
type: "acc-item",
sections: [{
sub: [{
type: "heading-1",
text: "Heading Text"
},
{
type: "ordered-item",
text: "Item 1"
},
{
type: "unordered-item",
text: "Item 2"
}
]
}]
},
{
type: "acc-item",
sections: [{
sub: [{
type: "heading-1",
text: "Heading Text 2"
}]
}]
}
]
}
}
function partition(array, isValid) {
return array.reduce(([pass, fail], elem) => {
return isValid(elem) ? [
[...pass, elem], fail
] : [pass, [...fail, elem]];
}, [
[],
[]
]);
}
function flattenSectionsAndSubs(item) {
return {
type: item.type,
subs: item.sections.reduce((acc, cur) => ([...acc, ...cur.sub]), [])
};
}
const listTypes = ['ordered-item', 'unordered-item'];
function createEmbeddedFromItem(item) {
const [lists, nonLists] = partition(item.subs, (v) => listTypes.includes(v.type));
return {
type: item.type,
embedded: [
...nonLists,
{
type: "list",
items: lists
}
]
}
}
const groupedData = contentData.data.content.reduce((acc, cur) => {
//Check if this indexed array already exists, if not create it.
const currentArray = (acc[`${cur.type}-group`] && acc[`${cur.type}-group`].items) || [];
const flattenedItem = flattenSectionsAndSubs(cur);
const embeddedItem = createEmbeddedFromItem(flattenedItem);
return {
...acc,
[`${cur.type}-group`]: {
type: `${cur.type}-group`,
items: [...currentArray, embeddedItem]
}
}
}, {});
console.log(groupedData);
Now this doesn't exactly match what you've asked for - but it should probably work.
You can add your own bits into only add a list item, if the array isn't empty, and to stop the column from being in its own group.
The thing is - tbh it seems like a little bit of a red flag that you would create an array of items that don't having matching structures, which is why I've done it this way.