Create a generic function that creates stacked dataset using d3 - javascript

I have this dataset:
const dataset = [
{ date: "2022-01-01", category: "red", value: 10 },
{ date: "2022-01-01", category: "blue", value: 20 },
{ date: "2022-01-01", category: "gold", value: 30 },
{ date: "2022-01-01", category: "green", value: 40 },
{ date: "2022-01-02", category: "red", value: 5 },
{ date: "2022-01-02", category: "blue", value: 15 },
{ date: "2022-01-02", category: "gold", value: 25 },
{ date: "2022-01-02", category: "green", value: 35 }
];
And I need to create a stacked barchart. To do that I used the d3 stack() function.
The result I need is this:
const stackedDataset = [
{ date: "2022-01-01", category: "red", value: 10, start: 0, end: 10 },
{ date: "2022-01-02", category: "red", value: 5, start: 0, end: 5 },
{ date: "2022-01-01", category: "blue", value: 20, start: 10, end: 30 },
{ date: "2022-01-02", category: "blue", value: 15, start: 5, end: 20 },
{ date: "2022-01-01", category: "gold", value: 30, start: 30, end: 60 },
{ date: "2022-01-02", category: "gold", value: 25, start: 20, end: 45 },
{ date: "2022-01-01", category: "green", value: 40, start: 60, end: 100 },
{ date: "2022-01-02", category: "green", value: 35, start: 45, end: 80 }
]
So the same data but with a start and end property computed by d3.
I created a function that takes in input dataset and returns stackedDataset:
export function getStackedSeries(dataset: Datum[]) {
const categories = uniq(dataset.map((d) => d[CATEGORY])) as string[];
const datasetGroupedByDateFlat = flatDataset(dataset);
const stackGenerator = d3.stack().keys(categories);
const seriesRaw = stackGenerator(
datasetGroupedByDateFlat as Array<Dictionary<number>>
);
const series = seriesRaw.flatMap((serie, si) => {
const category = categories[si];
const result = serie.map((s, sj) => {
return {
[DATE]: datasetGroupedByDateFlat[sj][DATE] as string,
[CATEGORY]: category,
[VALUE]: datasetGroupedByDateFlat[sj][category] as number,
start: s[0] || 0,
end: s[1] || 0
};
});
return result;
});
return series;
}
export function flatDataset(
dataset: Datum[]
): Array<Dictionary<string | number>> {
if (dataset.length === 0 || !DATE) {
return (dataset as unknown) as Array<Dictionary<string | number>>;
}
const columnToBeFlatValues = uniqBy(dataset, CATEGORY).map(
(d) => d[CATEGORY]
);
const datasetGroupedByDate = groupBy(dataset, DATE);
const datasetGroupedByMainCategoryFlat = Object.entries(
datasetGroupedByDate
).map(([date, datasetForDate]) => {
const categoriesObject = columnToBeFlatValues.reduce((acc, value) => {
const datum = datasetForDate.find(
(d) => d[DATE] === date && d[CATEGORY] === value
);
acc[value] = datum?.[VALUE];
return acc;
}, {} as Dictionary<string | number | undefined>);
return {
[DATE]: date,
...categoriesObject
};
});
return datasetGroupedByMainCategoryFlat as Array<Dictionary<string | number>>;
}
As you can see, the functions are specific for Datum type. Is there a way to modify them to make them works for a generic type T that has at least the three fields date, category, value?
I mean, I would like to have something like this:
interface StackedStartEnd {
start: number
end: number
}
function getStackedSeries<T>(dataset: T[]): T extends StackedStartEnd
Obviously this piece of code should be refactored to make it more generic:
{
[DATE]: ...,
[CATEGORY]: ...,
[VALUE]: ...,
start: ...,
end: ...,
}
Here the working code.
I'm not a TypeScript expert so I need some help. Honestly what I tried to do was to modify the function signature but I failed and, anyway, I would like to make the functions as generic as possible and I don't know how to start.
Do I need to pass to the functions also the used columns names?
Thank you very much

I tried to make a more generic approach as you suggest mixing the two functions. By default, seems like your getStackedSeries function does not need to know about date and category properties, you can use a Generic Type to ensure just the value property, as we need to know that to calculate start and end values.
The full implementation can be viewed here on codesandbox.
export function getStackedSeries<T extends Datum>(
data: T[],
groupByProperty: PropertyType<T>
) {
const groupedData = groupBy(data, (d) => d[groupByProperty]);
const acumulatedData = Object.entries(groupedData).flatMap(
([_, groupedValue]) => {
let acumulator = 0;
return groupedValue.map(({ value, ...rest }) => {
const obj = {
...rest,
value: value,
start: acumulator,
end: acumulator + value
};
acumulator += value;
return obj;
});
}
);
return acumulatedData;
}
The getStackedSeries() now receives a data property that extends Datum type, which is:
export interface Datum {
value: number;
}
With that and a second property called groupByProperty we can define the groupBy clause and return all flatten by flatMap.
You probably notice that the return type now is defined by typescript dynamically by the use of a generic <T>. For example:
const dataGroupedByDate: (Omit<{
date: string;
category: string;
value: number;
}, "value"> & {
value: number;
start: number;
end: number;
})[]
You can also type this part of the function, but makes sense to let the compiler works for you and generate the types automatically based on input.

You could group by date for start/end and take another grouping by category for the result set.
const
dataset = [{ date: "2022-01-01", category: "red", value: 10 }, { date: "2022-01-01", category: "blue", value: 20 }, { date: "2022-01-01", category: "gold", value: 30 }, { date: "2022-01-01", category: "green", value: 40 }, { date: "2022-01-02", category: "red", value: 5 }, { date: "2022-01-02", category: "blue", value: 15 }, { date: "2022-01-02", category: "gold", value: 25 }, { date: "2022-01-02", category: "green", value: 35 }],
result = Object
.values(dataset
.reduce((r, { date, category, value }) => {
const
start = r.date[date]?.at(-1).end ?? 0,
end = start + value,
object = { date, category, value, start, end };
(r.date[date] ??= []).push(object);
(r.category[category] ??= []).push(object);
return r;
}, { date: {}, category: {} })
.category
)
.flat();
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Related

How to use Typescript to define a generic function signature

I have this dataset:
const dataset = [
{ date: "2022-01-01", category: "red", value: 10 },
{ date: "2022-01-01", category: "blue", value: 20 },
{ date: "2022-01-01", category: "gold", value: 30 },
{ date: "2022-01-01", category: "green", value: 40 },
{ date: "2022-01-02", category: "red", value: 5 },
{ date: "2022-01-02", category: "blue", value: 15 },
{ date: "2022-01-02", category: "gold", value: 25 },
{ date: "2022-01-02", category: "green", value: 35 }
];
And I need to flat the dataset to get this:
const flattenDataset = [
{ "date": "2022-01-01", "red": 10, "blue": 20, "gold": 30, "green": 40 },
{ "date": "2022-01-02", "red": 5, "blue": 15, "gold": 25, "green": 35 }
]
So group dataset by dates and for each category create a key with value as value.
I created this function:
export function flatDataset(
dataset: any,
mainProperty: string,
categoryProperty: string,
valueProperty: string
) {
if (dataset.length === 0 || !mainProperty) {
return (dataset as unknown);
}
const columnToBeFlatValues = uniqBy(dataset, categoryProperty).map(
(d) => d[categoryProperty]
);
const datasetGroupedByMainProperty = groupBy(dataset, mainProperty);
const datasetGroupedByMainCategoryFlat = Object.entries(
datasetGroupedByMainProperty
).map(([date, datasetForDate]) => {
const categoriesObject = columnToBeFlatValues.reduce((acc, value) => {
const datum = datasetForDate.find(
(d) => d[mainProperty] === date && d[categoryProperty] === value
);
acc[value] = datum?.[valueProperty];
return acc;
}, {});
return {
[mainProperty]: date,
...categoriesObject
};
});
return datasetGroupedByMainCategoryFlat;
}
It works but I would like to fix TypeScript.
For example the dataset type should not be any but an array of objects with keys with name mainProperty, categoryProperty, valueProperty.
For example, dataset could also be:
const dataset = [
{ apple: ..., color: ..., something: ... },
...
];
const flattenDataset = flatDataset(dataset, 'apple', 'color', 'something')
How can I do that?
There's a few way to make it
let obj = { apple: 's', sonic: 'a' , etc: 'd'}
function MyFunction<T, K extends keyof T>(obj: T, ...args:K[]): void { }
MyFunction(obj, 'apple', 'sonic', 'etc')
Using Generics with Rest Arguments. I used this post as example to create the rest args. I recommend you create an interfaces for maintein all sorted
I believe this is the right generic types you're looking for.
The main trick is using { [k in Enum]: string }, which allows dynamically restricting a key of an object to be a part of a union of strings.
Please note you're using functions without their implementations, so it's hard to actually test it.
function flatDataset<Main extends string, Category extends string, Value extends string>(
dataset: ({ [k in Main | Category]: string } & { [k in Value]: number })[],
mainProperty: Main,
categoryProperty: Category,
valueProperty: Value,
): ({ [k in (typeof dataset)[number][Main]]: string }
& { [k in (typeof dataset)[number][Category]]: number })[]

How to filter one array with another array values and return a key value of the first array

I have 2 arrays with current week dates and investments with value and date. I want to return an array with the values that have corresponding dates between the 2 arrays.
My non-working solution is:
const daysOfWeek = [
"20-06-2022",
"21-06-2022",
"22-06-2022",
"23-06-2022",
"24-06-2022",
"25-06-2022",
"26-06-2022",
]
const investmentsData = [{
value: 0.77,
date: "21-06-2022"
},
{
value: 1.50,
date: "22-06-2022"
},
{
value: 0.80,
date: "20-06-2022"
},
{
value: 1.00,
date: "21-06-2022"
},
{
value: 0.77,
date: "20-06-2022"
},
{
value: 0.79,
date: "22-06-2022"
},
{
value: 0.73,
date: "18-06-2022"
},
{
value: 1.29,
date: "19-06-2022"
}
]
const result = investmentsData.flatMap((dayValue) => {
const getDayValue = daysOfWeek.filter((day) => {
return dayValue.date === day;
});
return getDayValue;
});
const filteredResult = result.filter((val) => !!val);
console.log(filteredResult)
// ["21-06-2022", "22-06-2022", "20-06-2022", "21-06-2022", "20-06-2022", "22-06-2022"]
When what I need is:
[0.77, 1.50, 0.80, 1.00, 0.77, 0.79]
Probably the filter inside the map is not the best option as it´s going to return the value of the first array (which is a date).
I also have the problem that result returns also the undefined. I then run filteredResult to remove all the undefined in the result. I guess this is a job that can be done with one function all together.
Take it step by step:
Filter investmentsData on whether or not daysOfWeek contains the date
From the filtered values, return the value.
const daysOfWeek = ["20-06-2022", "21-06-2022", "22-06-2022", "23-06-2022", "24-06-2022", "25-06-2022", "26-06-2022"];
const investmentsData = [
{ value: 0.77, date: "21-06-2022" },
{ value: 1.50, date: "22-06-2022" },
{ value: 0.80, date: "20-06-2022" },
{ value: 1.00, date: "21-06-2022" },
{ value: 0.77, date: "20-06-2022" },
{ value: 0.79, date: "22-06-2022" },
{ value: 0.73, date: "18-06-2022" },
{ value: 1.29, date: "19-06-2022" }
]
const result = investmentsData
.filter(d => daysOfWeek.includes(d.date))
.map(d => d.value);
console.log(result);

How to aggregate nested values in array of objects

I have an existing function that creates a summary object from two arrays (one array is transaction data, the other is master data).
Currently, the returned object contains a sum of the transaction quantity by year and month - example below.
I'm looking for help to add a sum of sums at the root level for each item - example also below.
I've reviewed answers for similar questions but wasn't able to find a solution close enough to my use case.
Here's the existing code:
const
array1 = [{ material: "ABC123", cost: 100 },
{ material: "DEF456", cost: 150 }],
array2 = [{ material: "ABC123", date: "1/1/20", quantity: 4 },
{ material: "ABC123", date: "1/15/20", quantity: 1 },
{ material: "ABC123", date: "2/15/20", quantity: 3 },
{ material: "ABC123", date: "4/15/21", quantity: 1 },
{ material: "DEF456", date: "3/05/20", quantity: 6 },
{ material: "DEF456", date: "3/18/20", quantity: 1 },
{ material: "DEF456", date: "5/15/21", quantity: 2 }],
groups = [
({ material }) => material,
_ => 'byYear',
({ date }) => '20' + date.split('/')[2],
_ => 'byMonth'
],
sum = {
key: ({ date }) => date.split('/')[0],
value: ({ quantity }) => quantity
},
result = array2.reduce(
(r, o) => {
const temp = groups.reduce((t, fn) => t[fn(o)] ??= {}, r);
temp[sum.key(o)] ??= 0;
temp[sum.key(o)] += sum.value(o);
return r;
},
Object.fromEntries(array1.map(({ material, cost }) => [material, { cost }]))
);
console.log(result);
As mentioned above, I would like it to add a "totalSum" key/value at the root level. The result would look like:
{
"ABC123": {
"totalSum": 8,
"cost": 100,
"byYear": {
"2020": {
"byMonth": {
"1": {
"sum": 5,
"max": 4
},
"2": {
"sum": 3,
"max": 3
}
}
}
}
}
}
Any help is greatly appreciated!
You can actually achieve the entire refactoring with a single Array#reduce() call, combined with a Map of the master array to retrieve cost.
Adding a total sum is then trivial since you have the all the properties to hand and don't have to remap your aggregated data.
const
array1 = [{ material: "ABC123", cost: 100, critical: true }, { material: "DEF456", cost: 150, critical: false }],
array2 = [{ material: "ABC123", date: "1/1/20", quantity: 4 }, { material: "ABC123", date: "1/15/20", quantity: 1 }, { material: "ABC123", date: "2/15/20", quantity: 3 }, { material: "ABC123", date: "4/15/21", quantity: 1 }, { material: "DEF456", date: "3/05/20", quantity: 6 }, { material: "DEF456", date: "3/18/20", quantity: 1 }, { material: "DEF456", date: "5/15/21", quantity: 2 }],
master = new Map(array1.map(({ material, ...item }) => [material, item])),
result = array2.reduce((acc, { material, date, quantity }) => {
let [month, , year] = date.split('/');
year = '20' + year;
const
_material = (acc[material] ??= { ...master.get(material), totalSum: 0, byYear: {} }),
_byYear = (_material.byYear[year] ??= { byMonth: {} }),
_byMonth = (_byYear.byMonth[month] ??= { sum: 0, max: 0 });
_material.totalSum += quantity;
_byMonth.sum += quantity;
_byMonth.max = Math.max(_byMonth.max, quantity);
return acc;
}, {});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Edit
In a comment you mentioned that you might have more properties in array1 that should be included in the final result. I've edited to accommodate this by changing the Map to reference the entire object as found in array1. We can then include all the properties available using spread syntax when we first declare the relevant object in the accumulator.
// split out 'material' from the rest of the object in the Map
const master = new Map(array1.map(({ material, ...item }) => [material, item]));
// later access the object by material and spread its props into the accumulator object.
_material = (acc[material] ??= { ...master.get(material), totalSum: 0, byYear: {} }),

Taking an array of transactions and summing values to respective dates. Conditionally

So I have an array of transactions that looks like this:
[
{
date: '22-03-2021,
amount: 2,
value: 10,
fees: 0.1,
type: "BUY"
transactionId: '21412412',
}
]
And what I want to do with the array is two things...
One is get an array of objects per date. Summing the value property of objects that are of type 'BUY' and deducting the value property from objects of type 'SELL'. Eventually I will need to deal with other transaction types, but not now...
Example: (you can see the value rise and fall, due to SELL and BUY both taking place)
[
{
date: "04-06-2021",
value: 20.0,
},
{
date: "05-06-2021",
value: 28.99,
},
{
date: "06-06-2021",
value: 47.99,
},
{
date: "07-06-2021",
value: 37.99,
},
{
date: "08-06-2021",
value: 42.29,
},
]
Two is pretty similar. I want to generate an array of objects per date but associated to a particular ticker.
Example: (value rises due to only BUY taking place)
[
{
TSLA: [
{
date: "04-06-2021",
value: 20.0,
},
{
date: "05-06-2021",
value: 23.99,
},
{
date: "06-06-2021",
value: 42.99,
},
{
date: "07-06-2021",
value: 47.29,
},
]
}
]
Here is what ive come up with so far:
const valueByDate = useMemo(
() =>
holdings
.map((data: any) => data)
.reduce((r: any, a: any) => {
// Find objects where ticker matches.
r[a.date] = r[a.date] || [];
// Push found object into array.
r[a.date].push({ type: a.type, value: a.value });
return r;
}, Object.create(null)),
[holdings],
);
// valueByDate creates an object like so...
let arr = {
'22-05-2021': [{ type: 'SELL', value: '9' }],
'22-06-2021': [{ type: 'SELL', value: '9' }],
'22-07-2021': [{ type: 'SELL', value: '9' }],
};
// Sum or Deduct VALUE per date depending on TYPE
const reduce = Object.values(valueByDate).forEach((element: any) =>
Object.values(element)
.flat()
.reduce((accumulator: any, currentValue: any) => {
if (currentValue.type == 'BUY') {
return (
parseFloat(accumulator) + parseFloat(currentValue.value)
);
} else if (currentValue.type == 'SELL') {
return (
parseFloat(accumulator) - parseFloat(currentValue.value)
);
}
}),
);
But I just have not been able to wrap my head around getting into the nested arrays and conditionally computing those values. Any insight would be a help.
Thank you
Those are the functions that I created:
type Transaction = {
date: string,
amount: number,
value: number,
fees: number,
type: "BUY" | "SELL",
transactionId: string
}
const transactions: Transaction[] = [
{
date: '22-03-2021',
amount: 2,
value: 10,
fees: 0.1,
type: "BUY",
transactionId: '21412412',
},
{
date: '22-03-2021',
amount: 2,
value: 10,
fees: 0.1,
type: "SELL",
transactionId: '21412412',
},
{
date: '22-03-2021',
amount: 2,
value: 10,
fees: 0.1,
type: "BUY",
transactionId: '21412412',
},
{
date: '23-03-2021',
amount: 2,
value: 10,
fees: 0.1,
type: "BUY",
transactionId: '21412412',
},
{
date: '23-03-2021',
amount: 2,
value: 10,
fees: 0.1,
type: "BUY",
transactionId: '21412412',
},
{
date: '23-03-2021',
amount: 2,
value: 10,
fees: 0.1,
type: "SELL",
transactionId: '21412412',
},
{
date: '23-03-2021',
amount: 2,
value: 10,
fees: 0.1,
type: "BUY",
transactionId: '21412412',
},
{
date: '23-03-2021',
amount: 2,
value: 10,
fees: 0.1,
type: "BUY",
transactionId: '21412412',
},
]
type DatedTransactions = { [key: string]: Transaction[] }
function getValuesByDate(transactions: Transaction[]): DatedTransactions {
let result: { [key: string]: Transaction[] } = {}
transactions.forEach(transaction => {
result[transaction.date] ? result[transaction.date].push(transaction) : result[transaction.date] = [transaction]
})
return result
}
function getTotalValue(transactions: Transaction[]): number {
return transactions.reduce((prev, curr) => {
if (curr.type === "BUY") {
return prev + curr.value
}
return prev - curr.value
}, 0)
}
function getTotalValueForEveryDate(datedTransactions: DatedTransactions): { date: string, value: number }[] {
const result: { date: string, value: number }[] = []
Object.keys(datedTransactions).forEach(date => {
const datedValue = { date, value: getTotalValue(datedTransactions[date]) }
result.push(datedValue)
})
return result
}
console.log(getTotalValueForEveryDate(getValuesByDate(transactions)))
in order to accomplish also the last part just iterate for every ticker :)
let me know if there is something not clear

Array of objects value with some weight age value not comes in proper sequence

I have an array of object like this:
let messageScoreData = {
messagescore: [
{
userid: "5bacc8c6563a882a1ca7756a",
score: 2605.4
},
{
userid: "5bacc98431481e0520856df8",
score: 1013.2
},
{
userid: "5bc6d0bb26f1bb1b44a790c6",
score: 41
},
{
userid: "5bc6d0bb26f1bb1b44a790c9",
score: 29
}
],
messagescorebefore: [
{
userid: "5bacc8c6563a882a1ca7756a",
score: 3754
},
{
userid: "5bacc98431481e0520856df8",
score: 1259.8
},
{
userid: "5bc6d0bb26f1bb1b44a790c6",
score: 98
},
{
userid: "5bced078d62b321d08f012af",
score: 22
},
{
userid: "5bcec1ad11302529f452b31e",
score: 6
},
{
userid: "5c10afec8c587d2fac8c356e",
score: 6
},
{
userid: "5c07b7f199848528e86e9359",
score: 3
},
{
userid: "5bed1373f94b611de4425259",
score: 2
},
{
userid: "5c21ccff833a5006fc5a98af",
score: 2
},
{
userid: "5c21ccff82e32c05c4043410",
score: 1
}
]
};
Now we will provide the weight-age value i.e messagescorebefore array have 0.4 value and messagescore have 0.6 value;
For that I have the algorithm which sequentialize the value with weight-age value. i.e
var result = messageScoreData;
var columns = [
{
name: "messagescorebefore",
value: 0.4
},
{
name: "messagescore",
value: 0.6
}
];
var total = {};
for (let column of columns) {
for (let userid of result[column.name]) {
var alphabet = userid.userid;
if (total[alphabet]) {
total[alphabet] += column.value;
} else {
total[alphabet] = column.value;
}
}
}
const valueholder = Object.keys(total)
.map(key => ({ name: key, value: total[key] }))
.sort((f, s) => s.value - f.value);
console.log(valueholder);
By this Algo output is :
[ { name: '5bacc8c6563a882a1ca7756a', value: 1 },
{ name: '5bc6d0bb26f1bb1b44a790c6', value: 1 },
{ name: '5bacc98431481e0520856df8', value: 1 },
{ name: '5bc6d0bb26f1bb1b44a790c9', value: 0.6 },
{ name: '5bcec1ad11302529f452b31e', value: 0.4 },
{ name: '5bced078d62b321d08f012af', value: 0.4 },
{ name: '5c07b7f199848528e86e9359', value: 0.4 },
{ name: '5bed1373f94b611de4425259', value: 0.4 },
{ name: '5c21ccff833a5006fc5a98af', value: 0.4 },
{ name: '5c21ccff82e32c05c4043410', value: 0.4 },
{ name: '5c10afec8c587d2fac8c356e', value: 0.4 } ]
Problem is userid: "5bacc98431481e0520856df8" will come on second position on both array but after final calculation this will come under 3rd position which is wrong.
expected output will be like this:
[ { name: '5bacc8c6563a882a1ca7756a', value: 1 },
{ name: '5bacc98431481e0520856df8', value: 1 },
{ name: '5bc6d0bb26f1bb1b44a790c6', value: 1 },
{ name: '5bc6d0bb26f1bb1b44a790c9', value: 0.6 },
{ name: '5bced078d62b321d08f012af', value: 0.4 },
{ name: '5bcec1ad11302529f452b31e', value: 0.4 },
{ name: '5c10afec8c587d2fac8c356e', value: 0.4 },
{ name: '5c07b7f199848528e86e9359', value: 0.4 },
{ name: '5bed1373f94b611de4425259', value: 0.4 },
{ name: '5c21ccff833a5006fc5a98af', value: 0.4 },
]
Any help is really appreciated for this. Thanks in advance
Actually, you want to preserve relative order of elements. normal sort function is not guaranteed to preserve relative order. so we need some tricks to keep relative order like below.
let messageScoreData = {
messagescore: [
{
userid: "5bacc8c6563a882a1ca7756a",
score: 2605.4
},
{
userid: "5bacc98431481e0520856df8",
score: 1013.2
},
{
userid: "5bc6d0bb26f1bb1b44a790c6",
score: 41
},
{
userid: "5bc6d0bb26f1bb1b44a790c9",
score: 29
}
],
messagescorebefore: [
{
userid: "5bacc8c6563a882a1ca7756a",
score: 3754
},
{
userid: "5bacc98431481e0520856df8",
score: 1259.8
},
{
userid: "5bc6d0bb26f1bb1b44a790c6",
score: 98
},
{
userid: "5bced078d62b321d08f012af",
score: 22
},
{
userid: "5bcec1ad11302529f452b31e",
score: 6
},
{
userid: "5c10afec8c587d2fac8c356e",
score: 6
},
{
userid: "5c07b7f199848528e86e9359",
score: 3
},
{
userid: "5bed1373f94b611de4425259",
score: 2
},
{
userid: "5c21ccff833a5006fc5a98af",
score: 2
},
{
userid: "5c21ccff82e32c05c4043410",
score: 1
}
]
};
var result = messageScoreData;
var columns = [
{
name: "messagescorebefore",
value: 0.4
},
{
name: "messagescore",
value: 0.6
}
];
var total = [];
for (let column of columns) {
for (let userid of result[column.name]) {
var alphabet = userid.userid;
if (total[alphabet]) {
total[alphabet] += column.value;
} else {
total[alphabet] = column.value;
}
}
}
let res = Object.keys(total).map((k, idx) => {
return {
name: k,
value: total[k],
index: idx
}
})
var output = res.sort((f, s) => {
if (s.value < f.value) return -1;
if (s.value > f.value) return 1;
return f.index - s.index
})
console.log("output : ", output)
The observed behaviour is expected since you are sorting the values in a descending way: .sort((f, s) => s.value - f.value);. From your example it seems that you want to sort the entries lexicographically on the names. In that case you should sort according to the names:
const valueholder = Object.keys(total)
.map(key => ({ name: key, value: total[key] }))
.sort((f, s) => f.name.localeCompare(s.name));
If you want to sort them primarily on the values (descending) and secondarily on the names (ascending) then do:
const valueholder = Object.keys(total)
.map(key => ({ name: key, value: total[key] }))
.sort((f, s) => s.value - f.value || f.name.localeCompare(s.name));
In this case, if two entries have the same value the difference s.value - f.value will be 0. Since this is a falsy value, f.name.localeCompare(s.name) will be evaluated, effectively sorting the values lexicographically on their name.
If you want to sort the entries based on their values but retain the original order for entries with the same value you can do the following:
const entries = Object.keys(total)
.map(key => ({ name: key, value: total[key] }))
const valueholder = entries.sort((f, s) => s.value - f.value || arr.indexOf(f) - arr.indexOf(s));
The reason we need to explicitly sort on their original order is because the built-in sorting algorithm is not (guaranteed to be) stable. Note that the above sorting is not very efficient since we use indexOf. I leave it as an exercise to first loop through the array and accumulate all indexes in a map that maps names to indexes. As such, when sorting you can look up the indexes rather than computing them.
If you're looking for stable sort, i.e. preserving the original order of elements of the array with equal value, you have to add a comparison of the indexes of the key array (assuming that this has the proper ordering):
const keys = Object.keys(total);
const valueholder = keys
.map(key => ({ name: key, value: total[key] }))
.sort((f, s) => s.value - f.value || keys.indexOf(f.name) < keys.indexOf(s.name));

Categories