@aclymatepackages/modules 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/babel.config.json +18 -0
- package/dist/components/CompaniesAutocomplete.js +126 -0
- package/dist/components/CompanyOnboardingInput.js +116 -0
- package/dist/components/ComparisonChart.js +200 -0
- package/dist/components/CustomTooltipDisplayRow.js +59 -0
- package/dist/components/EmissionsChart.js +467 -0
- package/dist/components/EmissionsCustomTooltip.js +181 -0
- package/dist/components/EmissionsPieChart.js +130 -0
- package/dist/components/EmissionsReductionGraph.js +118 -0
- package/dist/components/FootprintEquivalencies.js +181 -0
- package/dist/components/FootprintVideo.js +262 -0
- package/dist/components/FuelTypesSelect.js +36 -0
- package/dist/components/IndustryAutocomplete.js +58 -0
- package/dist/components/PlacesAutocomplete.js +173 -0
- package/dist/components/StripeElements.js +95 -0
- package/dist/components/YesNoQuestion.js +88 -0
- package/dist/components/stripeInput.js +293 -0
- package/dist/components/useChartWarningLabels.js +143 -0
- package/dist/index.js +118 -0
- package/package.json +74 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +43 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +3 -0
- package/src/components/CompaniesAutocomplete.js +125 -0
- package/src/components/CompanyOnboardingInput.js +113 -0
- package/src/components/ComparisonChart.js +236 -0
- package/src/components/CustomTooltipDisplayRow.js +41 -0
- package/src/components/EmissionsChart.js +579 -0
- package/src/components/EmissionsCustomTooltip.js +146 -0
- package/src/components/EmissionsPieChart.js +120 -0
- package/src/components/EmissionsReductionGraph.js +107 -0
- package/src/components/FootprintEquivalencies.js +203 -0
- package/src/components/FootprintVideo.js +328 -0
- package/src/components/FuelTypesSelect.js +29 -0
- package/src/components/IndustryAutocomplete.js +56 -0
- package/src/components/PlacesAutocomplete.js +174 -0
- package/src/components/StripeElements.js +95 -0
- package/src/components/YesNoQuestion.js +68 -0
- package/src/components/stripeInput.js +288 -0
- package/src/components/useChartWarningLabels.js +139 -0
- package/src/index.js +35 -0
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import dayjs from "dayjs";
|
|
3
|
+
import dayOfYear from "dayjs/plugin/dayOfYear";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
XAxis,
|
|
7
|
+
YAxis,
|
|
8
|
+
CartesianGrid,
|
|
9
|
+
ResponsiveContainer,
|
|
10
|
+
Area,
|
|
11
|
+
Bar,
|
|
12
|
+
ComposedChart,
|
|
13
|
+
Line,
|
|
14
|
+
Tooltip as ChartTooltip,
|
|
15
|
+
} from "recharts";
|
|
16
|
+
|
|
17
|
+
import { useTheme } from "@mui/styles";
|
|
18
|
+
import { Grid, Typography, Box } from "@mui/material";
|
|
19
|
+
|
|
20
|
+
import { formatDecimal } from "@aclymatepackages/formatters";
|
|
21
|
+
import {
|
|
22
|
+
findInclusiveDateDifference,
|
|
23
|
+
findYearFirstDate,
|
|
24
|
+
findQuarterDate,
|
|
25
|
+
} from "@aclymatepackages/date-helpers";
|
|
26
|
+
import { DAYS_PER_YEAR } from "@aclymatepackages/constants";
|
|
27
|
+
import { sumTonsCo2e } from "@aclymatepackages/other-helpers";
|
|
28
|
+
|
|
29
|
+
import EmissionsCustomTooltip from "./EmissionsCustomTooltip";
|
|
30
|
+
import useChartWarningLabels from "./useChartWarningLabels";
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
buildScopesRealDataObj,
|
|
34
|
+
buildSubcategoriesDataObj,
|
|
35
|
+
sumEmissionsTonsBySubcategory,
|
|
36
|
+
buildEmissionGroupData,
|
|
37
|
+
} from "@aclymatepackages/chart-helpers";
|
|
38
|
+
import {
|
|
39
|
+
subcategories,
|
|
40
|
+
allSubcategoriesWithOther,
|
|
41
|
+
} from "@aclymatepackages/subcategories";
|
|
42
|
+
|
|
43
|
+
dayjs.extend(dayOfYear);
|
|
44
|
+
|
|
45
|
+
const DAYS_IN_QUARTER = DAYS_PER_YEAR / 4;
|
|
46
|
+
|
|
47
|
+
const findObjectValuesSum = (object) =>
|
|
48
|
+
Object.values(object).reduce((sum, value) => sum + value, 0);
|
|
49
|
+
|
|
50
|
+
const findLabelDate = (period, label) => {
|
|
51
|
+
if (period === "quarter") {
|
|
52
|
+
return findQuarterDate(label);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (period === "year") {
|
|
56
|
+
return findYearFirstDate(label);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return dayjs(label, "MM-YY");
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isDateFirstDateOfPeriod = (date, period) => {
|
|
63
|
+
if (period === "quarter") {
|
|
64
|
+
const quarterFirstDate = findQuarterDate(date);
|
|
65
|
+
return dayjs(date).isSame(quarterFirstDate, "day");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (period === "year") {
|
|
69
|
+
const yearFirstDate = findYearFirstDate(date);
|
|
70
|
+
return dayjs(date).isSame(yearFirstDate, "day");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return dayjs(date).date() === 1;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const findIsEstimatedFirstDateLabel = ({
|
|
77
|
+
startDate,
|
|
78
|
+
earliestEmissionDate,
|
|
79
|
+
label,
|
|
80
|
+
period,
|
|
81
|
+
}) => {
|
|
82
|
+
const labelDate = findLabelDate(period, label);
|
|
83
|
+
const labelStartDateDifference = findInclusiveDateDifference(
|
|
84
|
+
labelDate,
|
|
85
|
+
startDate,
|
|
86
|
+
period
|
|
87
|
+
);
|
|
88
|
+
const labelDateIsFirstOfPeriod = isDateFirstDateOfPeriod(
|
|
89
|
+
earliestEmissionDate,
|
|
90
|
+
period
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return labelStartDateDifference === 1 && !labelDateIsFirstOfPeriod;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const findIsFirstPeriodEstimated = (chartLabelsArray, period) =>
|
|
97
|
+
!chartLabelsArray.reduce(
|
|
98
|
+
(acc, label) => acc && !findIsEstimatedFirstDateLabel({ label, period }),
|
|
99
|
+
true
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const findIsDataProjected = (latestEmissionDate) => {
|
|
103
|
+
if (!dayjs(latestEmissionDate).isSame(dayjs(), "month")) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return dayjs(latestEmissionDate).date() !== dayjs().daysInMonth();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const findProjectedSubcategoryValueFromEmissions =
|
|
110
|
+
({ emissions, projectionMultiplier }) =>
|
|
111
|
+
(subcategory) => {
|
|
112
|
+
const recurringSubcategoryEmissionsSum = sumEmissionsTonsBySubcategory(
|
|
113
|
+
emissions,
|
|
114
|
+
subcategory
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return projectionMultiplier * recurringSubcategoryEmissionsSum;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const buildSubcategoriesProjectionObj = (emissions, projectionMultiplier) => {
|
|
121
|
+
const findProjectedSubcategoryValue =
|
|
122
|
+
findProjectedSubcategoryValueFromEmissions({
|
|
123
|
+
emissions,
|
|
124
|
+
projectionMultiplier,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"commuting-auto_projected": findProjectedSubcategoryValue("commuting-auto"),
|
|
129
|
+
"home-office_projected": findProjectedSubcategoryValue("home-office"),
|
|
130
|
+
utilities_projected: findProjectedSubcategoryValue("utilities"),
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const buildScopesProjectionObj = (emissions, projectionMultiplier) => {
|
|
135
|
+
const findProjectedSubcategoryValue =
|
|
136
|
+
findProjectedSubcategoryValueFromEmissions({
|
|
137
|
+
emissions,
|
|
138
|
+
projectionMultiplier,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const projectedCommutingEmissionsTons =
|
|
142
|
+
findProjectedSubcategoryValue("commuting-auto");
|
|
143
|
+
const projectedHomeOfficeEmissionsTons =
|
|
144
|
+
findProjectedSubcategoryValue("homeOffice");
|
|
145
|
+
const projectedElectricityEmissionsTons =
|
|
146
|
+
findProjectedSubcategoryValue("electricity");
|
|
147
|
+
const projectedGasEmissionsTons = findProjectedSubcategoryValue("gas");
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
scope1_projected: projectedGasEmissionsTons,
|
|
151
|
+
scope2_projected: projectedElectricityEmissionsTons,
|
|
152
|
+
scope3_projected:
|
|
153
|
+
projectedHomeOfficeEmissionsTons + projectedCommutingEmissionsTons,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const buildGroupedChartData = ({
|
|
158
|
+
startDate = "",
|
|
159
|
+
period,
|
|
160
|
+
isDataProjected,
|
|
161
|
+
isFirstPeriodEstimated,
|
|
162
|
+
groupedEmissions,
|
|
163
|
+
buildProjectionObj,
|
|
164
|
+
buildRealDataObj,
|
|
165
|
+
showProjection,
|
|
166
|
+
}) => {
|
|
167
|
+
const findPeriodPosition = (idx) => {
|
|
168
|
+
if (!idx) {
|
|
169
|
+
return "first";
|
|
170
|
+
}
|
|
171
|
+
if (idx === groupedEmissions.length - 1) {
|
|
172
|
+
return "last";
|
|
173
|
+
}
|
|
174
|
+
return "middle";
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const forwardProjectionPeriodFraction = (period) => {
|
|
178
|
+
if (period === "month") {
|
|
179
|
+
const daysInMonth = dayjs().daysInMonth();
|
|
180
|
+
return dayjs().date() / daysInMonth;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (period === "quarter") {
|
|
184
|
+
const firstDayInQuarter = findQuarterDate(dayjs());
|
|
185
|
+
const currentDaysIntoQuarter = dayjs().diff(firstDayInQuarter, "day");
|
|
186
|
+
return currentDaysIntoQuarter / DAYS_IN_QUARTER;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const currentDaysIntoYear = dayjs().dayOfYear();
|
|
190
|
+
return currentDaysIntoYear / DAYS_PER_YEAR;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const backwardsProjectionPeriodFraction = (period) => {
|
|
194
|
+
const formattedStartDate = dayjs(startDate);
|
|
195
|
+
if (period === "month") {
|
|
196
|
+
const daysInMonth = formattedStartDate.daysInMonth();
|
|
197
|
+
const daysFromEndOfMonth = daysInMonth - formattedStartDate.date();
|
|
198
|
+
return daysFromEndOfMonth / daysInMonth;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (period === "quarter") {
|
|
202
|
+
const lastDateInQuarter = findQuarterDate(startDate, false);
|
|
203
|
+
const daysFromEndOfQuarter =
|
|
204
|
+
Math.abs(formattedStartDate.diff(lastDateInQuarter, "day")) + 1;
|
|
205
|
+
return daysFromEndOfQuarter / DAYS_IN_QUARTER;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const daysFromEndOfYear = Math.abs(
|
|
209
|
+
formattedStartDate.diff(
|
|
210
|
+
dayjs(new Date(formattedStartDate.year(), 12, 31)),
|
|
211
|
+
"day"
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return daysFromEndOfYear / DAYS_PER_YEAR;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const findPeriodProjectionMultiplier = ({ period, position }) => {
|
|
219
|
+
if (
|
|
220
|
+
!showProjection ||
|
|
221
|
+
position === "middle" ||
|
|
222
|
+
(position === "first" && !isFirstPeriodEstimated) ||
|
|
223
|
+
(position === "last" && !isDataProjected)
|
|
224
|
+
) {
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
//TODO: -UPDATE: this projection function doesn't take into account closed offices or terminated employee, so we probably need to rethink it
|
|
229
|
+
const projectionFraction =
|
|
230
|
+
position === "first"
|
|
231
|
+
? backwardsProjectionPeriodFraction(period)
|
|
232
|
+
: forwardProjectionPeriodFraction(period);
|
|
233
|
+
|
|
234
|
+
return 1 / projectionFraction - 1;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return groupedEmissions.map((emissionsArray, idx) => {
|
|
238
|
+
const position = findPeriodPosition(idx, groupedEmissions);
|
|
239
|
+
const projectionMultiplier = findPeriodProjectionMultiplier({
|
|
240
|
+
startDate,
|
|
241
|
+
position,
|
|
242
|
+
period,
|
|
243
|
+
isDataProjected,
|
|
244
|
+
isFirstPeriodEstimated,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const projectedCategoriesObj = buildProjectionObj(
|
|
248
|
+
emissionsArray,
|
|
249
|
+
projectionMultiplier
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const subcategoriesObj = buildRealDataObj(
|
|
253
|
+
emissionsArray,
|
|
254
|
+
allSubcategoriesWithOther
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const chartCategoriesObj = {
|
|
258
|
+
...projectedCategoriesObj,
|
|
259
|
+
...subcategoriesObj,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const buildWarningObj = () => {
|
|
263
|
+
const singleEmissionWarning = emissionsArray.find(
|
|
264
|
+
({ warning }) => !!warning
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (!singleEmissionWarning) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const { warning } = singleEmissionWarning;
|
|
272
|
+
return warning;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
...chartCategoriesObj,
|
|
277
|
+
warning: buildWarningObj(),
|
|
278
|
+
id: `emissions-chart-group-${idx}`,
|
|
279
|
+
emissionsSumTons: sumTonsCo2e(emissionsArray),
|
|
280
|
+
totalEmissionsSumTons: findObjectValuesSum(chartCategoriesObj),
|
|
281
|
+
projectedEmissionsSumTons: findObjectValuesSum(projectedCategoriesObj),
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const addTrendlineToChartData = (chartData) => {
|
|
287
|
+
const averageX = (chartData.length - 1) / 2;
|
|
288
|
+
const averageY =
|
|
289
|
+
chartData.reduce(
|
|
290
|
+
(sum, { totalEmissionsSumTons }) =>
|
|
291
|
+
Number(sum) + Number(totalEmissionsSumTons),
|
|
292
|
+
0
|
|
293
|
+
) / chartData.length;
|
|
294
|
+
|
|
295
|
+
const trendLineCalcData = chartData.map(({ totalEmissionsSumTons }, idx) => {
|
|
296
|
+
const xDifferenceFromMean = idx - averageX;
|
|
297
|
+
const yDifferenceFromMean = totalEmissionsSumTons - averageY;
|
|
298
|
+
const xDifferenceSquared = xDifferenceFromMean * xDifferenceFromMean;
|
|
299
|
+
const xyDifferenceFromMean = xDifferenceFromMean * yDifferenceFromMean;
|
|
300
|
+
return { xDifferenceSquared, xyDifferenceFromMean };
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const xDifferenceSquaredSum = trendLineCalcData.reduce(
|
|
304
|
+
(sum, { xDifferenceSquared }) => sum + xDifferenceSquared,
|
|
305
|
+
0
|
|
306
|
+
);
|
|
307
|
+
const xyDifferenceFromMeanSum = trendLineCalcData.reduce(
|
|
308
|
+
(sum, { xyDifferenceFromMean }) => sum + xyDifferenceFromMean,
|
|
309
|
+
0
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const trendlineSlope = xyDifferenceFromMeanSum / xDifferenceSquaredSum;
|
|
313
|
+
|
|
314
|
+
const trendlineIntercept = averageY - trendlineSlope * averageX;
|
|
315
|
+
|
|
316
|
+
const findTrendlineValue = (x) => trendlineSlope * x + trendlineIntercept;
|
|
317
|
+
|
|
318
|
+
return chartData.map(({ totalEmissionsSumTons, ...otherProps }, idx) => ({
|
|
319
|
+
...otherProps,
|
|
320
|
+
trendline: findTrendlineValue(idx),
|
|
321
|
+
}));
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const LabeledEmissionsChart = ({
|
|
325
|
+
data,
|
|
326
|
+
chartRef,
|
|
327
|
+
type,
|
|
328
|
+
displayUnitLabel,
|
|
329
|
+
chartArray,
|
|
330
|
+
aspect = 3,
|
|
331
|
+
showTooltip = true,
|
|
332
|
+
}) => {
|
|
333
|
+
const { palette } = useTheme();
|
|
334
|
+
|
|
335
|
+
const { warningLabels, labelSetter } = useChartWarningLabels({
|
|
336
|
+
data,
|
|
337
|
+
warningField: "warning",
|
|
338
|
+
barSumField: "totalEmissionsSumTons",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const isTrendLineGood = data[0]?.trendLine > data[data.length - 1]?.trendLine;
|
|
342
|
+
|
|
343
|
+
const ChartElement = type === "bar" ? Bar : Area;
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<Box position="relative">
|
|
347
|
+
<ResponsiveContainer aspect={aspect}>
|
|
348
|
+
<ComposedChart width={500} height={300} data={data} ref={chartRef}>
|
|
349
|
+
<XAxis dataKey="label" interval="preserveStartEnd" height={20} />
|
|
350
|
+
<YAxis
|
|
351
|
+
tickFormatter={(tick) =>
|
|
352
|
+
`${formatDecimal(tick)} ${displayUnitLabel}`
|
|
353
|
+
}
|
|
354
|
+
width={100}
|
|
355
|
+
/>
|
|
356
|
+
<CartesianGrid strokeDasharray="3 3" />
|
|
357
|
+
{showTooltip && (
|
|
358
|
+
<ChartTooltip
|
|
359
|
+
wrapperStyle={{ zIndex: 99 }}
|
|
360
|
+
content={
|
|
361
|
+
<EmissionsCustomTooltip
|
|
362
|
+
categoriesArray={chartArray}
|
|
363
|
+
displayUnitLabel={displayUnitLabel}
|
|
364
|
+
/>
|
|
365
|
+
}
|
|
366
|
+
/>
|
|
367
|
+
)}
|
|
368
|
+
{chartArray.map(({ subcategory, color, ...otherProps }) => (
|
|
369
|
+
<ChartElement
|
|
370
|
+
key={`emissions-chart-element-${subcategory}`}
|
|
371
|
+
type="monotone"
|
|
372
|
+
stroke="none"
|
|
373
|
+
fillOpacity={1}
|
|
374
|
+
stackId="a"
|
|
375
|
+
fill={color}
|
|
376
|
+
dataKey={subcategory}
|
|
377
|
+
{...otherProps}
|
|
378
|
+
>
|
|
379
|
+
{labelSetter}
|
|
380
|
+
</ChartElement>
|
|
381
|
+
))}
|
|
382
|
+
<Line
|
|
383
|
+
name="6 Month Trend"
|
|
384
|
+
connectNulls
|
|
385
|
+
dataKey="trendline"
|
|
386
|
+
stroke={
|
|
387
|
+
isTrendLineGood ? palette.secondary.main : palette.error.main
|
|
388
|
+
}
|
|
389
|
+
strokeWidth={4}
|
|
390
|
+
dot={false}
|
|
391
|
+
strokeDasharray="5 5"
|
|
392
|
+
/>
|
|
393
|
+
</ComposedChart>
|
|
394
|
+
</ResponsiveContainer>
|
|
395
|
+
{warningLabels}
|
|
396
|
+
</Box>
|
|
397
|
+
);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const EmissionsChart = ({
|
|
401
|
+
dataArray: emissions,
|
|
402
|
+
type,
|
|
403
|
+
viewMode = "subcategories",
|
|
404
|
+
graphPeriod,
|
|
405
|
+
chartRef,
|
|
406
|
+
showTrendline,
|
|
407
|
+
showProjection,
|
|
408
|
+
displayUnit,
|
|
409
|
+
unitConverter,
|
|
410
|
+
aspect,
|
|
411
|
+
isPercentageChart,
|
|
412
|
+
showTooltip,
|
|
413
|
+
startDate,
|
|
414
|
+
convertCarbonUnits,
|
|
415
|
+
displayUnitLabel,
|
|
416
|
+
}) => {
|
|
417
|
+
const {
|
|
418
|
+
chartLabelsArray,
|
|
419
|
+
latestEmissionDate,
|
|
420
|
+
scopesArray,
|
|
421
|
+
subcategoriesArray,
|
|
422
|
+
period,
|
|
423
|
+
groupedEmissions,
|
|
424
|
+
} = buildEmissionGroupData(emissions, graphPeriod, startDate);
|
|
425
|
+
|
|
426
|
+
const isDataProjected = findIsDataProjected(latestEmissionDate);
|
|
427
|
+
const isFirstPeriodEstimated = findIsFirstPeriodEstimated(
|
|
428
|
+
chartLabelsArray,
|
|
429
|
+
graphPeriod
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
const buildProjectionObj =
|
|
433
|
+
viewMode === "subcategories"
|
|
434
|
+
? buildSubcategoriesProjectionObj
|
|
435
|
+
: buildScopesProjectionObj;
|
|
436
|
+
|
|
437
|
+
const buildRealDataObj =
|
|
438
|
+
viewMode === "subcategories"
|
|
439
|
+
? buildSubcategoriesDataObj
|
|
440
|
+
: buildScopesRealDataObj;
|
|
441
|
+
|
|
442
|
+
const buildChartData = () => {
|
|
443
|
+
const preliminaryChartData = buildGroupedChartData({
|
|
444
|
+
startDate,
|
|
445
|
+
period,
|
|
446
|
+
isDataProjected,
|
|
447
|
+
isFirstPeriodEstimated,
|
|
448
|
+
groupedEmissions,
|
|
449
|
+
buildProjectionObj,
|
|
450
|
+
buildRealDataObj,
|
|
451
|
+
showProjection,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const formattedLabels = chartLabelsArray.map((label, idx) => {
|
|
455
|
+
if (idx === chartLabelsArray.length - 1 && isDataProjected) {
|
|
456
|
+
return `${label}*`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (idx || !isFirstPeriodEstimated) {
|
|
460
|
+
return label;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return `${label}**`;
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const convertChartDataObject = (chartDataObj, converter) =>
|
|
467
|
+
Object.fromEntries(
|
|
468
|
+
Object.entries(chartDataObj).map(([key, value]) => {
|
|
469
|
+
if (typeof value !== "number") {
|
|
470
|
+
return [key, value];
|
|
471
|
+
}
|
|
472
|
+
return [key, converter(value)];
|
|
473
|
+
})
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const labelChartData = (chartData) =>
|
|
477
|
+
chartData.map((data, idx) => ({
|
|
478
|
+
...data,
|
|
479
|
+
label: formattedLabels[idx],
|
|
480
|
+
}));
|
|
481
|
+
|
|
482
|
+
if (isPercentageChart) {
|
|
483
|
+
const percentageConvertedChartData = preliminaryChartData.map(
|
|
484
|
+
(chartDataObj) => {
|
|
485
|
+
const objectSubcategoryProperties = Object.keys(chartDataObj).filter(
|
|
486
|
+
(key) =>
|
|
487
|
+
subcategories.find(({ subcategory }) => subcategory === key)
|
|
488
|
+
);
|
|
489
|
+
const objectSubcategoryValues = objectSubcategoryProperties.map(
|
|
490
|
+
(subcategory) => ({
|
|
491
|
+
key: subcategory,
|
|
492
|
+
value: chartDataObj[subcategory],
|
|
493
|
+
})
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const periodEmissionsSum = objectSubcategoryValues.reduce(
|
|
497
|
+
(sum, { value }) => value + sum,
|
|
498
|
+
0
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const percentageConverter = (value) =>
|
|
502
|
+
(value / periodEmissionsSum) * 100;
|
|
503
|
+
|
|
504
|
+
const newObject = Object.fromEntries(
|
|
505
|
+
objectSubcategoryValues.map(({ key, value }) => [key, value])
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
return convertChartDataObject(newObject, percentageConverter);
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
return labelChartData(percentageConvertedChartData);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const unitConvertedChartData = preliminaryChartData.map((chartDataObj) =>
|
|
515
|
+
convertChartDataObject(chartDataObj, (value) =>
|
|
516
|
+
unitConverter ? unitConverter(value) : convertCarbonUnits(value)
|
|
517
|
+
)
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
const labeledChartData = labelChartData(unitConvertedChartData);
|
|
521
|
+
|
|
522
|
+
if (showTrendline) {
|
|
523
|
+
return addTrendlineToChartData(labeledChartData);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return labeledChartData;
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const chartData = buildChartData();
|
|
530
|
+
|
|
531
|
+
const chartArray = viewMode === "scopes" ? scopesArray : subcategoriesArray;
|
|
532
|
+
|
|
533
|
+
return (
|
|
534
|
+
<Grid container direction="column">
|
|
535
|
+
<Grid item>
|
|
536
|
+
<LabeledEmissionsChart
|
|
537
|
+
data={chartData}
|
|
538
|
+
chartRef={chartRef}
|
|
539
|
+
type={type}
|
|
540
|
+
displayUnitLabel={
|
|
541
|
+
isPercentageChart ? "%" : displayUnit || displayUnitLabel
|
|
542
|
+
}
|
|
543
|
+
chartArray={chartArray}
|
|
544
|
+
aspect={aspect}
|
|
545
|
+
showTooltip={showTooltip}
|
|
546
|
+
/>
|
|
547
|
+
</Grid>
|
|
548
|
+
{(isDataProjected || isFirstPeriodEstimated) && (
|
|
549
|
+
<Grid item container justifyContent="flex-end">
|
|
550
|
+
<Grid item>
|
|
551
|
+
{isDataProjected && showProjection && (
|
|
552
|
+
<Typography
|
|
553
|
+
variant="caption"
|
|
554
|
+
color="textSecondary"
|
|
555
|
+
display="block"
|
|
556
|
+
align="right"
|
|
557
|
+
>
|
|
558
|
+
*Data in the most recent period is projected since this period
|
|
559
|
+
isn't complete yet.
|
|
560
|
+
</Typography>
|
|
561
|
+
)}
|
|
562
|
+
{isFirstPeriodEstimated && showProjection && (
|
|
563
|
+
<Typography
|
|
564
|
+
variant="caption"
|
|
565
|
+
color="textSecondary"
|
|
566
|
+
display="block"
|
|
567
|
+
align="right"
|
|
568
|
+
>
|
|
569
|
+
**Data in the first period is estimated since your start date is
|
|
570
|
+
in the middle of this period.
|
|
571
|
+
</Typography>
|
|
572
|
+
)}
|
|
573
|
+
</Grid>
|
|
574
|
+
</Grid>
|
|
575
|
+
)}
|
|
576
|
+
</Grid>
|
|
577
|
+
);
|
|
578
|
+
};
|
|
579
|
+
export default EmissionsChart;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { Grid, Typography, Paper, Box, ThemeProvider } from "@mui/material";
|
|
4
|
+
|
|
5
|
+
import { DefaultPaper } from "@aclymatepackages/atoms";
|
|
6
|
+
import { formatDecimal } from "@aclymatepackages/formatters";
|
|
7
|
+
import { mergeDarkTheme } from "@aclymatepackages/themes";
|
|
8
|
+
|
|
9
|
+
import CustomTooltipDisplayRow from "./CustomTooltipDisplayRow";
|
|
10
|
+
|
|
11
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
12
|
+
|
|
13
|
+
const StatusDisplay = ({ dataArray, id }) => {
|
|
14
|
+
const { status } = dataArray.find((dataObj) => dataObj?.id === id) || {};
|
|
15
|
+
if (!status) {
|
|
16
|
+
return <></>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { color, icon, tooltip } = status;
|
|
20
|
+
return (
|
|
21
|
+
<Grid item>
|
|
22
|
+
<Paper style={{ backgroundColor: color }}>
|
|
23
|
+
<Box p={1}>
|
|
24
|
+
<Grid container spacing={2} alignItems="center">
|
|
25
|
+
<Grid item>
|
|
26
|
+
<FontAwesomeIcon
|
|
27
|
+
icon={icon}
|
|
28
|
+
style={{ color: "white" }}
|
|
29
|
+
size="2x"
|
|
30
|
+
/>
|
|
31
|
+
</Grid>
|
|
32
|
+
<Grid item>
|
|
33
|
+
<ThemeProvider theme={mergeDarkTheme}>
|
|
34
|
+
<Typography variant="subtitle2" color="textPrimary">
|
|
35
|
+
{tooltip}
|
|
36
|
+
</Typography>
|
|
37
|
+
</ThemeProvider>
|
|
38
|
+
</Grid>
|
|
39
|
+
</Grid>
|
|
40
|
+
</Box>
|
|
41
|
+
</Paper>
|
|
42
|
+
</Grid>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const EmissionsCustomTooltip = ({
|
|
47
|
+
payload,
|
|
48
|
+
categoriesArray,
|
|
49
|
+
labelAnnotation = "",
|
|
50
|
+
dataArray = [],
|
|
51
|
+
displayUnitLabel,
|
|
52
|
+
}) => {
|
|
53
|
+
const buildSubtitle = (realCarbonVolume, projectedCarbonVolume) => {
|
|
54
|
+
const baseSubtitle = `${formatDecimal(
|
|
55
|
+
realCarbonVolume
|
|
56
|
+
)} ${displayUnitLabel} CO2`;
|
|
57
|
+
|
|
58
|
+
if (!projectedCarbonVolume) {
|
|
59
|
+
return baseSubtitle;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return `${baseSubtitle} (${formatDecimal(
|
|
63
|
+
projectedCarbonVolume
|
|
64
|
+
)} projected)`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const columnDetails = payload?.[0]?.payload;
|
|
68
|
+
const {
|
|
69
|
+
label = "",
|
|
70
|
+
trendline,
|
|
71
|
+
emissionsSumTons,
|
|
72
|
+
projectedEmissionsSumTons,
|
|
73
|
+
id,
|
|
74
|
+
...graphValues
|
|
75
|
+
} = columnDetails || {};
|
|
76
|
+
const cleanLabel = label.replaceAll(/[*]/gi, "");
|
|
77
|
+
|
|
78
|
+
const filteredFormattedGraphValuesArray = Object.entries(graphValues)
|
|
79
|
+
.filter(([_, value]) => value)
|
|
80
|
+
.map(([key, value]) => {
|
|
81
|
+
const [subcategory, projected] = key.split("_");
|
|
82
|
+
return { subcategory, projected: !!projected, value };
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const uniqueSubcategories = [
|
|
86
|
+
...new Set(
|
|
87
|
+
filteredFormattedGraphValuesArray.map(({ subcategory }) => subcategory)
|
|
88
|
+
),
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const subcategoryDisplayArray = uniqueSubcategories.map((subcategory) => {
|
|
92
|
+
const realValue = filteredFormattedGraphValuesArray.find(
|
|
93
|
+
(graphValue) =>
|
|
94
|
+
graphValue.subcategory === subcategory && !graphValue.projected
|
|
95
|
+
)?.value;
|
|
96
|
+
|
|
97
|
+
const projectedValue = filteredFormattedGraphValuesArray.find(
|
|
98
|
+
(graphValue) =>
|
|
99
|
+
graphValue.subcategory === subcategory && !!graphValue.projected
|
|
100
|
+
)?.value;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
subcategory,
|
|
104
|
+
realValue,
|
|
105
|
+
projectedValue,
|
|
106
|
+
subtitle: buildSubtitle(realValue, projectedValue),
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const displayArrayWithIcons = subcategoryDisplayArray
|
|
111
|
+
.map(({ subcategory, ...otherProps }) => {
|
|
112
|
+
const { Icon, color, icon, name } =
|
|
113
|
+
categoriesArray.find(
|
|
114
|
+
(categoryObj) => categoryObj.subcategory === subcategory
|
|
115
|
+
) || {};
|
|
116
|
+
return { subcategory, Icon, icon, color, name, ...otherProps };
|
|
117
|
+
})
|
|
118
|
+
.filter(({ color }) => color);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<DefaultPaper style={{ zIndex: 99999, borderColor: "transparent" }}>
|
|
122
|
+
<Grid container direction="column" spacing={2}>
|
|
123
|
+
<Grid item>
|
|
124
|
+
<Typography
|
|
125
|
+
variant="h6"
|
|
126
|
+
color="textPrimary"
|
|
127
|
+
>{`${cleanLabel} Emissions`}</Typography>
|
|
128
|
+
<Typography variant="subtitle2" color="textSecondary">
|
|
129
|
+
{`${buildSubtitle(
|
|
130
|
+
emissionsSumTons,
|
|
131
|
+
projectedEmissionsSumTons
|
|
132
|
+
)} ${labelAnnotation}`}
|
|
133
|
+
</Typography>
|
|
134
|
+
</Grid>
|
|
135
|
+
{displayArrayWithIcons.map((displayData, idx) => (
|
|
136
|
+
<CustomTooltipDisplayRow
|
|
137
|
+
key={`tooltip-display-row-${idx}`}
|
|
138
|
+
{...displayData}
|
|
139
|
+
/>
|
|
140
|
+
))}
|
|
141
|
+
<StatusDisplay id={id} dataArray={dataArray} />
|
|
142
|
+
</Grid>
|
|
143
|
+
</DefaultPaper>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
export default EmissionsCustomTooltip;
|