@@ -38,43 +38,41 @@ export interface PackageQuadrantPoint {
3838 }
3939}
4040
41- interface QuadrantMetricRanges {
42- minimumDownloads : number
43- maximumDownloads : number
44- minimumTotalLikes : number
45- maximumTotalLikes : number
46- minimumPackageSize : number
47- maximumPackageSize : number
48- minimumInstallSize : number
49- maximumInstallSize : number
50- minimumDependencies : number
51- maximumDependencies : number
52- minimumTotalDependencies : number
53- maximumTotalDependencies : number
54- minimumVulnerabilities : number
55- maximumVulnerabilities : number
56- minimumLogarithmicDownloads : number
57- maximumLogarithmicDownloads : number
58- }
59-
6041const WEIGHTS = {
61- // Quadrant X axis
6242 adoption : {
63- downloads : 0.7 , // dominant signal because they best reflect real-world adoption
64- freshness : 0.1 , // small correction so stale packages are slightly penalized
65- likes : 0.01 , // might be pumped up in the future when ./npmx likes are more mainstream
43+ downloads : 0.75 , // dominant signal because they best reflect real-world adoption (in the data we have through facets currently)
44+ freshness : 0.15 , // small correction so stale packages are slightly
45+ likes : 0.1 , // might be pumped up in the future when ./npmx likes are more mainstream
6646 } ,
67- // Quadrant Y axis
6847 efficiency : {
6948 installSize : 0.3 , // weighted highest because it best reflects consumer footprint
70- dependencies : 0.2 , // direct deps capture architectural and supply-chain complexity
71- totalDependencies : 0.15 , // same for total deps
72- packageSize : 0.1 , // publication weight, less important than installed footprint
49+
50+ // dependency weights are already measured in install size in some way, but still useful knobs to find the sweet spot
51+ dependencies : 0.05 , // direct deps capture architectural and supply-chain complexity
52+ totalDependencies : 0.2 , // same for total deps
53+
54+ packageSize : 0.1 ,
7355 vulnerabilities : 0.2 , // penalize security burden
74- types : 0.1 , // TS support
75- deprecation : 0.05 ,
56+ types : 0.2 , // TS support
57+ // Note: the 'deprecated' metric is not weighed because it just forces a -1 evaluation
7658 } ,
7759}
60+
61+ /* Fixed logarithmic ceilings to normalize metrics onto a stable [-1, 1] scale.
62+ * This avoids dataset-relative min/max normalization, which would shift scores depending
63+ * on which packages are being compared. Ceilings act as reference points for what is
64+ * considered 'high' for each metric, ensuring consistent positioning across different
65+ * datasets while preserving meaningful differences via log scaling.
66+ */
67+ const LOG_CEILINGS = {
68+ downloads : 100_000_000 ,
69+ likes : 1000 , // might be pumped up in the future when ./npmx likes are more mainstream
70+ installSize : 25_000_000 ,
71+ dependencies : 100 ,
72+ totalDependencies : 1_000 ,
73+ packageSize : 15_000_000 ,
74+ }
75+
7876const VULNERABILITY_PENALTY_MULTIPLIER = 2
7977
8078function clampInRange ( value : number , min = - 1 , max = 1 ) : number {
@@ -83,16 +81,6 @@ function clampInRange(value: number, min = -1, max = 1): number {
8381 return value
8482}
8583
86- function normalizeNumber ( value : number , min : number , max : number ) : number {
87- if ( max === min ) return 0
88- const normalisedValue = ( value - min ) / ( max - min )
89- return clampInRange ( normalisedValue * 2 - 1 )
90- }
91-
92- function normalizeInverseNumber ( value : number , min : number , max : number ) : number {
93- return - normalizeNumber ( value , min , max )
94- }
95-
9684function normalizeBoolean ( value : boolean ) : number {
9785 return value ? 1 : - 1
9886}
@@ -101,7 +89,7 @@ function toSafeNumber(value: number | null | undefined, fallback = 0): number {
10189 return typeof value === 'number' && Number . isFinite ( value ) ? value : fallback
10290}
10391
104- function getnormalisedFreshness (
92+ function getNormalisedFreshness (
10593 value : string | Date | null | undefined ,
10694 maximumAgeInDays = 365 ,
10795) : number | null {
@@ -121,7 +109,7 @@ function getFreshnessScore(
121109 value : string | Date | null | undefined ,
122110 maximumAgeInDays = 365 ,
123111) : number {
124- const normalisedAge = getnormalisedFreshness ( value , maximumAgeInDays )
112+ const normalisedAge = getNormalisedFreshness ( value , maximumAgeInDays )
125113 if ( normalisedAge === null ) return - 1
126114 return clampInRange ( normalisedAge * 2 - 1 )
127115}
@@ -130,14 +118,27 @@ function getFreshnessPercentage(
130118 value : string | Date | null | undefined ,
131119 maximumAgeInDays = 365 ,
132120) : number {
133- const normalisedAge = getnormalisedFreshness ( value , maximumAgeInDays )
121+ const normalisedAge = getNormalisedFreshness ( value , maximumAgeInDays )
134122 if ( normalisedAge === null ) return 0
135123 return Math . max ( 0 , Math . min ( 1 , normalisedAge ) ) * 100
136124}
137125
138- function getVulnerabilityPenalty ( value : number , minimum : number , maximum : number ) : number {
139- const normalised = normalizeInverseNumber ( value , minimum , maximum )
140- return normalised < 0 ? normalised * VULNERABILITY_PENALTY_MULTIPLIER : normalised
126+ function normalizeLogHigherBetter ( value : number , upperBound : number ) : number {
127+ const safeValue = Math . max ( 0 , value )
128+ const safeUpperBound = Math . max ( 1 , upperBound )
129+ const normalised = Math . log ( safeValue + 1 ) / Math . log ( safeUpperBound + 1 )
130+ return clampInRange ( normalised * 2 - 1 )
131+ }
132+
133+ function normalizeLogLowerBetter ( value : number , upperBound : number ) : number {
134+ return - normalizeLogHigherBetter ( value , upperBound )
135+ }
136+
137+ function getVulnerabilityPenalty ( value : number ) : number {
138+ if ( value <= 0 ) return 1
139+
140+ const penalty = normalizeLogLowerBetter ( value , 10 )
141+ return penalty < 0 ? penalty * VULNERABILITY_PENALTY_MULTIPLIER : penalty
141142}
142143
143144function resolveQuadrant ( x : number , y : number ) : PackageQuadrantPoint [ 'quadrant' ] {
@@ -147,44 +148,7 @@ function resolveQuadrant(x: number, y: number): PackageQuadrantPoint['quadrant']
147148 return 'BOTTOM_LEFT'
148149}
149150
150- function getQuadrantMetricRanges ( packages : PackageQuadrantInput [ ] ) : QuadrantMetricRanges {
151- const downloadsValues = packages . map ( packageItem => toSafeNumber ( packageItem . downloads ) )
152- const totalLikesValues = packages . map ( packageItem => toSafeNumber ( packageItem . totalLikes ) )
153- const packageSizeValues = packages . map ( packageItem => toSafeNumber ( packageItem . packageSize ) )
154- const installSizeValues = packages . map ( packageItem => toSafeNumber ( packageItem . installSize ) )
155- const dependenciesValues = packages . map ( packageItem => toSafeNumber ( packageItem . dependencies ) )
156- const totalDependenciesValues = packages . map ( packageItem =>
157- toSafeNumber ( packageItem . totalDependencies ) ,
158- )
159- const vulnerabilitiesValues = packages . map ( packageItem =>
160- toSafeNumber ( packageItem . vulnerabilities ) ,
161- )
162- const logarithmicDownloadsValues = downloadsValues . map ( value => Math . log ( value + 1 ) )
163-
164- return {
165- minimumDownloads : Math . min ( ...downloadsValues ) ,
166- maximumDownloads : Math . max ( ...downloadsValues ) ,
167- minimumTotalLikes : Math . min ( ...totalLikesValues ) ,
168- maximumTotalLikes : Math . max ( ...totalLikesValues ) ,
169- minimumPackageSize : Math . min ( ...packageSizeValues ) ,
170- maximumPackageSize : Math . max ( ...packageSizeValues ) ,
171- minimumInstallSize : Math . min ( ...installSizeValues ) ,
172- maximumInstallSize : Math . max ( ...installSizeValues ) ,
173- minimumDependencies : Math . min ( ...dependenciesValues ) ,
174- maximumDependencies : Math . max ( ...dependenciesValues ) ,
175- minimumTotalDependencies : Math . min ( ...totalDependenciesValues ) ,
176- maximumTotalDependencies : Math . max ( ...totalDependenciesValues ) ,
177- minimumVulnerabilities : Math . min ( ...vulnerabilitiesValues ) ,
178- maximumVulnerabilities : Math . max ( ...vulnerabilitiesValues ) ,
179- minimumLogarithmicDownloads : Math . min ( ...logarithmicDownloadsValues ) ,
180- maximumLogarithmicDownloads : Math . max ( ...logarithmicDownloadsValues ) ,
181- }
182- }
183-
184- function createQuadrantPoint (
185- packageItem : PackageQuadrantInput ,
186- metricRanges : QuadrantMetricRanges ,
187- ) : PackageQuadrantPoint {
151+ function createQuadrantPoint ( packageItem : PackageQuadrantInput ) : PackageQuadrantPoint {
188152 const downloads = toSafeNumber ( packageItem . downloads )
189153 const totalLikes = toSafeNumber ( packageItem . totalLikes )
190154 const packageSize = toSafeNumber ( packageItem . packageSize )
@@ -197,56 +161,20 @@ function createQuadrantPoint(
197161 const freshnessScore = getFreshnessScore ( packageItem . lastUpdated ) // for weighing
198162 const freshnessPercent = getFreshnessPercentage ( packageItem . lastUpdated ) // for display
199163
200- // Since downloads can span multiple orders of magnitude, log is used to normalise them to produce comparable scores instead of collapsing most values into noise
201- const normalisedDownloads = normalizeNumber (
202- Math . log ( downloads + 1 ) ,
203- metricRanges . minimumLogarithmicDownloads ,
204- metricRanges . maximumLogarithmicDownloads ,
205- )
206-
207- const normalisedTotalLikes = normalizeNumber (
208- totalLikes ,
209- metricRanges . minimumTotalLikes ,
210- metricRanges . maximumTotalLikes ,
211- )
164+ const normalisedDownloads = normalizeLogHigherBetter ( downloads , LOG_CEILINGS . downloads )
165+ const normalisedLikes = normalizeLogHigherBetter ( totalLikes , LOG_CEILINGS . likes )
166+ const normalisedInstallSize = normalizeLogLowerBetter ( installSize , LOG_CEILINGS . installSize )
167+ const normalisedDependencies = normalizeLogLowerBetter ( dependencies , LOG_CEILINGS . dependencies )
168+ const normalisedTotalDependencies = normalizeLogLowerBetter ( totalDependencies , LOG_CEILINGS . totalDependencies )
169+ const normalisedPackageSize = normalizeLogLowerBetter ( packageSize , LOG_CEILINGS . packageSize )
212170
213- const normalisedInstallSize = normalizeInverseNumber (
214- installSize ,
215- metricRanges . minimumInstallSize ,
216- metricRanges . maximumInstallSize ,
217- )
218-
219- const normalisedDependencies = normalizeInverseNumber (
220- dependencies ,
221- metricRanges . minimumDependencies ,
222- metricRanges . maximumDependencies ,
223- )
224-
225- const normalisedTotalDependencies = normalizeInverseNumber (
226- totalDependencies ,
227- metricRanges . minimumTotalDependencies ,
228- metricRanges . maximumTotalDependencies ,
229- )
230-
231- const normalisedPackageSize = normalizeInverseNumber (
232- packageSize ,
233- metricRanges . minimumPackageSize ,
234- metricRanges . maximumPackageSize ,
235- )
236-
237- const normalisedVulnerabilities = getVulnerabilityPenalty (
238- vulnerabilities ,
239- metricRanges . minimumVulnerabilities ,
240- metricRanges . maximumVulnerabilities ,
241- )
242-
243- const deprecationScore = normalizeBoolean ( ! deprecated )
171+ const normalisedVulnerabilities = getVulnerabilityPenalty ( vulnerabilities )
244172 const typesScore = normalizeBoolean ( types )
245173
246174 const adoptionScore = clampInRange (
247175 normalisedDownloads * WEIGHTS . adoption . downloads +
248176 freshnessScore * WEIGHTS . adoption . freshness +
249- normalisedTotalLikes * WEIGHTS . adoption . likes ,
177+ normalisedLikes * WEIGHTS . adoption . likes ,
250178 )
251179
252180 const rawEfficiencyScore =
@@ -255,12 +183,9 @@ function createQuadrantPoint(
255183 normalisedTotalDependencies * WEIGHTS . efficiency . totalDependencies +
256184 normalisedPackageSize * WEIGHTS . efficiency . packageSize +
257185 normalisedVulnerabilities * WEIGHTS . efficiency . vulnerabilities +
258- typesScore * WEIGHTS . efficiency . types +
259- deprecationScore * WEIGHTS . efficiency . deprecation
186+ typesScore * WEIGHTS . efficiency . types
260187
261- // Deprecation considered harmful
262188 const efficiencyScore = deprecated ? - 1 : clampInRange ( rawEfficiencyScore )
263-
264189 const quadrant = resolveQuadrant ( adoptionScore , efficiencyScore )
265190
266191 return {
@@ -290,6 +215,5 @@ function createQuadrantPoint(
290215
291216export function createQuadrantDataset ( packages : PackageQuadrantInput [ ] ) : PackageQuadrantPoint [ ] {
292217 if ( ! packages . length ) return [ ]
293- const metricRanges = getQuadrantMetricRanges ( packages )
294- return packages . map ( packageItem => createQuadrantPoint ( packageItem , metricRanges ) )
295- }
218+ return packages . map ( packageItem => createQuadrantPoint ( packageItem ) )
219+ }
0 commit comments