Skip to content

Commit 142c041

Browse files
authored
Add Sustainable Web Design rating scale
Adds the Sustainable Web Design Rating Scale for Version 3 of the Sustainable Web Design Model.
2 parents 49b3bc1 + d16724d commit 142c041

File tree

7 files changed

+189
-6
lines changed

7 files changed

+189
-6
lines changed

src/1byte.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const KWH_PER_BYTE_FOR_DEVICES = 1.3e-10;
3939

4040
class OneByte {
4141
constructor(options) {
42+
this.allowRatings = false;
4243
this.options = options;
4344

4445
this.KWH_PER_BYTE_FOR_NETWORK = KWH_PER_BYTE_FOR_NETWORK;

src/co2.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
* @property {number} dataCenterCO2 - The CO2 estimate for data centers in grams
4242
* @property {number} consumerDeviceCO2 - The CO2 estimate for consumer devices in grams
4343
* @property {number} productionCO2 - The CO2 estimate for device production in grams
44+
* @property {string} rating - The rating of the CO2 estimate based on the Sustainable Web Design Model
4445
* @property {number} total - The total CO2 estimate in grams
4546
*/
4647

@@ -54,6 +55,7 @@
5455
* @property {number} 'consumerDeviceCO2 - subsequent' - The CO2 estimate for consumer devices in grams on subsequent visits
5556
* @property {number} 'productionCO2 - first' - The CO2 estimate for device production in grams on first visit
5657
* @property {number} 'productionCO2 - subsequent' - The CO2 estimate for device production in grams on subsequent visits
58+
* @property {string} rating - The rating of the CO2 estimate based on the Sustainable Web Design Model
5759
* @property {number} total - The total CO2 estimate in grams
5860
*/
5961

@@ -81,8 +83,26 @@ class CO2 {
8183
);
8284
}
8385

86+
if (options?.rating && typeof options.rating !== "boolean") {
87+
throw new Error(
88+
`The rating option must be a boolean. Please use true or false.\nSee https://developers.thegreenwebfoundation.org/co2js/options/ to learn more about the options available in CO2.js.`
89+
);
90+
}
91+
92+
// This flag checks to see if the model itself has a rating system.
93+
const allowRatings = !!this.model.allowRatings;
94+
8495
/** @private */
8596
this._segment = options?.results === "segment";
97+
// This flag is set by the user to enable the rating system.
98+
this._rating = options?.rating === true;
99+
100+
// The rating system is only supported in the Sustainable Web Design Model.
101+
if (!allowRatings && this._rating) {
102+
throw new Error(
103+
`The rating system is not supported in the model you are using. Try using the Sustainable Web Design model instead.\nSee https://developers.thegreenwebfoundation.org/co2js/models/ to learn more about the models available in CO2.js.`
104+
);
105+
}
86106
}
87107

88108
/**
@@ -95,7 +115,7 @@ class CO2 {
95115
* @return {number|CO2EstimateComponentsPerByte} the amount of CO2 in grammes or its separate components
96116
*/
97117
perByte(bytes, green = false) {
98-
return this.model.perByte(bytes, green, this._segment);
118+
return this.model.perByte(bytes, green, this._segment, this._rating);
99119
}
100120

101121
/**
@@ -109,7 +129,7 @@ class CO2 {
109129
*/
110130
perVisit(bytes, green = false) {
111131
if (this.model?.perVisit) {
112-
return this.model.perVisit(bytes, green, this._segment);
132+
return this.model.perVisit(bytes, green, this._segment, this._rating);
113133
} else {
114134
throw new Error(
115135
`The perVisit() method is not supported in the model you are using. Try using perByte() instead.\nSee https://developers.thegreenwebfoundation.org/co2js/methods/ to learn more about the methods available in CO2.js.`
@@ -134,7 +154,13 @@ class CO2 {
134154
adjustments = parseOptions(options);
135155
}
136156
return {
137-
co2: this.model.perByte(bytes, green, this._segment, adjustments),
157+
co2: this.model.perByte(
158+
bytes,
159+
green,
160+
this._segment,
161+
this._rating,
162+
adjustments
163+
),
138164
green,
139165
variables: {
140166
description:
@@ -176,7 +202,13 @@ class CO2 {
176202
}
177203

178204
return {
179-
co2: this.model.perVisit(bytes, green, this._segment, adjustments),
205+
co2: this.model.perVisit(
206+
bytes,
207+
green,
208+
this._segment,
209+
this._rating,
210+
adjustments
211+
),
180212
green,
181213
variables: {
182214
description:

src/co2.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,22 @@ describe("co2", () => {
211211
`The perVisit() method is not supported in the model you are using. Try using perByte() instead.\nSee https://developers.thegreenwebfoundation.org/co2js/methods/ to learn more about the methods available in CO2.js.`
212212
);
213213
});
214+
215+
it("throws an error if using the rating system with OneByte", () => {
216+
expect(() => {
217+
co2 = new CO2({ model: "1byte", rating: true });
218+
}).toThrowError(
219+
`The rating system is not supported in the model you are using. Try using the Sustainable Web Design model instead.\nSee https://developers.thegreenwebfoundation.org/co2js/models/ to learn more about the models available in CO2.js.`
220+
);
221+
});
222+
223+
it("throws an error if the rating parameter is not a boolean", () => {
224+
expect(() => {
225+
co2 = new CO2({ rating: "false" });
226+
}).toThrowError(
227+
`The rating option must be a boolean. Please use true or false.\nSee https://developers.thegreenwebfoundation.org/co2js/options/ to learn more about the options available in CO2.js.`
228+
);
229+
});
214230
});
215231

216232
// Test that grid intensity data can be imported and used
@@ -832,4 +848,23 @@ describe("co2", () => {
832848
expect(co2Result["consumerDeviceCO2 - subsequent"]).toBe(0);
833849
});
834850
});
851+
852+
describe("Returning SWD results with rating", () => {
853+
const co2NoRating = new CO2();
854+
const co2Rating = new CO2({ rating: true });
855+
const co2RatingSegmented = new CO2({ rating: true, results: "segment" });
856+
857+
it("does not return a rating when rating is false", () => {
858+
expect(co2NoRating.perVisit(MILLION)).not.toHaveProperty("rating");
859+
});
860+
861+
it("returns a rating when rating is true", () => {
862+
expect(co2Rating.perVisit(MILLION)).toHaveProperty("rating");
863+
});
864+
865+
it("returns a rating when rating is true and results are segmented", () => {
866+
expect(co2RatingSegmented.perByte(MILLION)).toHaveProperty("rating");
867+
expect(co2RatingSegmented.perByte(MILLION)).toHaveProperty("networkCO2");
868+
});
869+
});
835870
});

src/constants/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ const FIRST_TIME_VIEWING_PERCENTAGE = 0.75;
2424
const RETURNING_VISITOR_PERCENTAGE = 0.25;
2525
const PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD = 0.02;
2626

27+
const SWDMv3Ratings = {
28+
fifthPercentile: 0.095,
29+
tenthPercentile: 0.186,
30+
twentiethPercentile: 0.341,
31+
thirtiethPercentile: 0.493,
32+
fortiethPercentile: 0.656,
33+
fiftiethPercentile: 0.846,
34+
};
35+
2736
export {
2837
fileSize,
2938
KWH_PER_GB,
@@ -36,4 +45,5 @@ export {
3645
FIRST_TIME_VIEWING_PERCENTAGE,
3746
RETURNING_VISITOR_PERCENTAGE,
3847
PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD,
48+
SWDMv3Ratings,
3949
};

src/helpers/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717

1818
const formatNumber = (num) => parseFloat(num.toFixed(2));
1919

20+
const lessThanEqualTo = (num, limit) => num <= limit;
21+
2022
function parseOptions(options) {
2123
// CHeck that it is an object
2224
if (typeof options !== "object") {
@@ -192,4 +194,4 @@ function getApiRequestHeaders(comment = "") {
192194
return { "User-Agent": `co2js/${process.env.CO2JS_VERSION} ${comment}` };
193195
}
194196

195-
export { formatNumber, parseOptions, getApiRequestHeaders };
197+
export { formatNumber, parseOptions, getApiRequestHeaders, lessThanEqualTo };

src/sustainable-web-design.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,22 @@ import {
2020
FIRST_TIME_VIEWING_PERCENTAGE,
2121
RETURNING_VISITOR_PERCENTAGE,
2222
PERCENTAGE_OF_DATA_LOADED_ON_SUBSEQUENT_LOAD,
23+
SWDMv3Ratings,
2324
} from "./constants/index.js";
24-
import { formatNumber } from "./helpers/index.js";
25+
import { formatNumber, lessThanEqualTo } from "./helpers/index.js";
26+
27+
const {
28+
fifthPercentile,
29+
tenthPercentile,
30+
twentiethPercentile,
31+
thirtiethPercentile,
32+
fortiethPercentile,
33+
fiftiethPercentile,
34+
} = SWDMv3Ratings;
2535

2636
class SustainableWebDesign {
2737
constructor(options) {
38+
this.allowRatings = true;
2839
this.options = options;
2940
}
3041

@@ -119,13 +130,15 @@ class SustainableWebDesign {
119130
* @param {number} bytes - the data transferred in bytes
120131
* @param {boolean} carbonIntensity - a boolean indicating whether the data center is green or not
121132
* @param {boolean} segmentResults - a boolean indicating whether to return the results broken down by component
133+
* @param {boolean} ratingResults - a boolean indicating whether to return the rating based on the Sustainable Web Design Model
122134
* @param {object} options - an object containing the grid intensity and first/return visitor values
123135
* @return {number|object} the total number in grams of CO2 equivalent emissions, or an object containing the breakdown by component
124136
*/
125137
perByte(
126138
bytes,
127139
carbonIntensity = false,
128140
segmentResults = false,
141+
ratingResults = false,
129142
options = {}
130143
) {
131144
if (bytes < 1) {
@@ -153,10 +166,27 @@ class SustainableWebDesign {
153166
(prevValue, currentValue) => prevValue + currentValue
154167
);
155168

169+
let rating = null;
170+
if (ratingResults) {
171+
rating = this.ratingScale(co2ValuesSum);
172+
}
173+
156174
if (segmentResults) {
175+
if (ratingResults) {
176+
return {
177+
...co2ValuesbyComponent,
178+
total: co2ValuesSum,
179+
rating: rating,
180+
};
181+
}
182+
157183
return { ...co2ValuesbyComponent, total: co2ValuesSum };
158184
}
159185

186+
if (ratingResults) {
187+
return { total: co2ValuesSum, rating: rating };
188+
}
189+
160190
return co2ValuesSum;
161191
}
162192

@@ -167,13 +197,15 @@ class SustainableWebDesign {
167197
* @param {number} bytes - the data transferred in bytes
168198
* @param {boolean} carbonIntensity - a boolean indicating whether the data center is green or not
169199
* @param {boolean} segmentResults - a boolean indicating whether to return the results broken down by component
200+
* @param {boolean} ratingResults - a boolean indicating whether to return the rating based on the Sustainable Web Design Model
170201
* @param {object} options - an object containing the grid intensity and first/return visitor values
171202
* @return {number|object} the total number in grams of CO2 equivalent emissions, or an object containing the breakdown by component
172203
*/
173204
perVisit(
174205
bytes,
175206
carbonIntensity = false,
176207
segmentResults = false,
208+
ratingResults = false,
177209
options = {}
178210
) {
179211
const energyBycomponent = this.energyPerVisitByComponent(bytes, options);
@@ -197,10 +229,26 @@ class SustainableWebDesign {
197229
(prevValue, currentValue) => prevValue + currentValue
198230
);
199231

232+
let rating = null;
233+
if (ratingResults) {
234+
rating = this.ratingScale(co2ValuesSum);
235+
}
236+
200237
if (segmentResults) {
238+
if (ratingResults) {
239+
return {
240+
...co2ValuesbyComponent,
241+
total: co2ValuesSum,
242+
rating: rating,
243+
};
244+
}
201245
return { ...co2ValuesbyComponent, total: co2ValuesSum };
202246
}
203247

248+
if (ratingResults) {
249+
return { total: co2ValuesSum, rating: rating };
250+
}
251+
204252
// so we can return their sum
205253
return co2ValuesSum;
206254
}
@@ -331,6 +379,30 @@ class SustainableWebDesign {
331379
productionEnergy: formatNumber(annualEnergy * PRODUCTION_ENERGY),
332380
};
333381
}
382+
383+
/**
384+
* Determines the rating of a website's sustainability based on its CO2 emissions.
385+
*
386+
* @param {number} co2e - The CO2 emissions of the website in grams.
387+
* @returns {string} The sustainability rating, ranging from "A+" (best) to "F" (worst).
388+
*/
389+
ratingScale(co2e) {
390+
if (lessThanEqualTo(co2e, fifthPercentile)) {
391+
return "A+";
392+
} else if (lessThanEqualTo(co2e, tenthPercentile)) {
393+
return "A";
394+
} else if (lessThanEqualTo(co2e, twentiethPercentile)) {
395+
return "B";
396+
} else if (lessThanEqualTo(co2e, thirtiethPercentile)) {
397+
return "C";
398+
} else if (lessThanEqualTo(co2e, fortiethPercentile)) {
399+
return "D";
400+
} else if (lessThanEqualTo(co2e, fiftiethPercentile)) {
401+
return "E";
402+
} else {
403+
return "F";
404+
}
405+
}
334406
}
335407

336408
export { SustainableWebDesign };

src/sustainable-web-design.test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import SustainableWebDesign from "./sustainable-web-design.js";
22
import { MILLION, SWD } from "./constants/test-constants.js";
3+
import { SWDMv3Ratings } from "./constants/index.js";
4+
5+
const {
6+
fifthPercentile,
7+
tenthPercentile,
8+
twentiethPercentile,
9+
thirtiethPercentile,
10+
fortiethPercentile,
11+
fiftiethPercentile,
12+
} = SWDMv3Ratings;
313

414
describe("sustainable web design model", () => {
515
const swd = new SustainableWebDesign();
@@ -113,4 +123,25 @@ describe("sustainable web design model", () => {
113123
});
114124
});
115125
});
126+
127+
describe("SWD Rating Scale", () => {
128+
it("should return a string", () => {
129+
expect(typeof swd.ratingScale(averageWebsiteInBytes)).toBe("string");
130+
});
131+
132+
it("should return a rating", () => {
133+
// Check a 3MB file size
134+
expect(swd.ratingScale(3000000)).toBe("F");
135+
});
136+
137+
it("returns ratings as expected", () => {
138+
expect(swd.ratingScale(fifthPercentile)).toBe("A+");
139+
expect(swd.ratingScale(tenthPercentile)).toBe("A");
140+
expect(swd.ratingScale(twentiethPercentile)).toBe("B");
141+
expect(swd.ratingScale(thirtiethPercentile)).toBe("C");
142+
expect(swd.ratingScale(fortiethPercentile)).toBe("D");
143+
expect(swd.ratingScale(fiftiethPercentile)).toBe("E");
144+
expect(swd.ratingScale(0.9)).toBe("F");
145+
});
146+
});
116147
});

0 commit comments

Comments
 (0)