@sjcrh/proteinpaint-shared 2.114.0 → 2.115.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-shared",
3
- "version": "2.114.0",
3
+ "version": "2.115.0",
4
4
  "description": "ProteinPaint code that is shared between server and client-side workspaces",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -11,7 +11,9 @@
11
11
  "scripts": {
12
12
  "build": "esbuild src/*.ts --platform=node --outdir=src/ --format=esm && prettier --no-semi --use-tabs --write src/urljson.js src/joinUrl.js src/doc.js",
13
13
  "prepack": "npm run build",
14
- "test": "ls src/test/*.spec* | xargs node"
14
+ "pretest": "mkdir -p test && node emitImports > test/internals-test.ts",
15
+ "test": "tsx test/internals-test.ts",
16
+ "test-x": "ls src/test/*.spec* | xargs -I % bash -c '{ tsx %; sleep 0.001; }'"
15
17
  },
16
18
  "author": "",
17
19
  "license": "ISC",
package/src/common.js CHANGED
@@ -11,6 +11,7 @@ exported functions
11
11
  import { rgb } from 'd3-color'
12
12
  import * as d3scale from 'd3-scale'
13
13
  import * as d3 from 'd3'
14
+ import { getWrappedTvslst } from './filter.js'
14
15
 
15
16
  // moved from `#shared/terms` to here, so that this can be passed as
16
17
  // part of 'common' argument to exported dataset js function, at server runtime
@@ -1153,10 +1154,11 @@ export const CNVClasses = Object.values(mclass)
1153
1154
  .filter(m => m.dt == dtcnv)
1154
1155
  .map(m => m.key)
1155
1156
 
1156
- // custom dt terms used by variant filter of geneVariant term
1157
- export const dtTerms = [
1157
+ // dt terms used for filtering variants for geneVariant term
1158
+ const _dtTerms = [
1158
1159
  {
1159
1160
  id: 'snvindel',
1161
+ query: 'snvindel',
1160
1162
  name: dt2label[dtsnvindel],
1161
1163
  parent_id: null,
1162
1164
  isleaf: true,
@@ -1168,6 +1170,7 @@ export const dtTerms = [
1168
1170
  },
1169
1171
  {
1170
1172
  id: 'cnv',
1173
+ query: 'cnv',
1171
1174
  name: dt2label[dtcnv],
1172
1175
  parent_id: null,
1173
1176
  isleaf: true,
@@ -1177,6 +1180,7 @@ export const dtTerms = [
1177
1180
  },
1178
1181
  {
1179
1182
  id: 'fusion',
1183
+ query: 'svfusion',
1180
1184
  name: dt2label[dtfusionrna],
1181
1185
  parent_id: null,
1182
1186
  isleaf: true,
@@ -1186,6 +1190,7 @@ export const dtTerms = [
1186
1190
  },
1187
1191
  {
1188
1192
  id: 'sv',
1193
+ query: 'svfusion',
1189
1194
  name: dt2label[dtsv],
1190
1195
  parent_id: null,
1191
1196
  isleaf: true,
@@ -1194,156 +1199,96 @@ export const dtTerms = [
1194
1199
  values: Object.fromEntries([mclasssv, 'WT'].map(key => [key, { label: mclass[key].label }]))
1195
1200
  }
1196
1201
  ]
1202
+ // add origin annotations to dt terms
1203
+ export const dtTerms = []
1204
+ for (const _dtTerm of _dtTerms) {
1205
+ const dtTerm = structuredClone(_dtTerm)
1206
+ dtTerm.name_noOrigin = dtTerm.name // for labeling groups in groupsetting
1207
+ dtTerms.push(dtTerm) // no origin
1208
+ for (const origin of ['somatic', 'germline']) {
1209
+ // add origins
1210
+ const addOrigin = {
1211
+ id: `${dtTerm.id}_${origin}`,
1212
+ name: `${dtTerm.name} (${origin})`,
1213
+ origin
1214
+ }
1215
+ dtTerms.push(Object.assign({}, dtTerm, addOrigin))
1216
+ }
1217
+ }
1218
+
1219
+ const dtFilter = {
1220
+ opts: { joinWith: ['and', 'or'] },
1221
+ terms: dtTerms
1222
+ }
1223
+
1224
+ const groupsetLst = []
1225
+ for (const term of dtTerms) {
1226
+ // wildtype filter
1227
+ const WTfilter = structuredClone(dtFilter)
1228
+ WTfilter.group = 2
1229
+ const WT = 'WT'
1230
+ const WTvalue = { key: WT, label: term.values[WT].label, value: WT }
1231
+ const WTtvs = { type: 'tvs', tvs: { term, values: [WTvalue] } }
1232
+ WTfilter.active = getWrappedTvslst([WTtvs])
1233
+ let WTname = 'Wildtype'
1234
+ // mutated filter
1235
+ const MUTfilter = structuredClone(dtFilter)
1236
+ MUTfilter.group = 1
1237
+ const classes = Object.keys(term.values)
1238
+ if (classes.length < 2) throw 'should have at least 2 classes'
1239
+ let MUTtvs, MUTname
1240
+ if (classes.length == 2) {
1241
+ // only 2 classes
1242
+ // mutant filter will filter for the mutant class
1243
+ const MUT = classes.find(c => c != WT)
1244
+ const MUTvalue = { key: MUT, label: term.values[MUT].label, value: MUT }
1245
+ MUTtvs = { type: 'tvs', tvs: { term, values: [MUTvalue] } }
1246
+ MUTname = term.values[MUT].label
1247
+ } else {
1248
+ // more than 2 classes
1249
+ // mutant filter will filter for all non-wildtype classes
1250
+ MUTtvs = { type: 'tvs', tvs: { term, values: [WTvalue], isnot: true } }
1251
+ MUTname = term.name_noOrigin
1252
+ }
1253
+ MUTfilter.active = getWrappedTvslst([MUTtvs])
1254
+ // excluded filter
1255
+ const EXCLUDEfilter = structuredClone(dtFilter)
1256
+ EXCLUDEfilter.group = 0
1257
+ EXCLUDEfilter.active = getWrappedTvslst()
1258
+ // assign filters to groups
1259
+ let GRPSETname = `${MUTname} vs. Wildtype`
1260
+ if (term.origin) GRPSETname += ` (${term.origin})`
1261
+ const WTgroup = {
1262
+ name: WTname,
1263
+ type: 'filter',
1264
+ uncomputable: false,
1265
+ filter: WTfilter
1266
+ }
1267
+ const MUTgroup = {
1268
+ name: MUTname,
1269
+ type: 'filter',
1270
+ uncomputable: false,
1271
+ filter: MUTfilter
1272
+ }
1273
+ const EXCLUDEgroup = {
1274
+ name: 'Excluded categories',
1275
+ type: 'filter',
1276
+ uncomputable: true,
1277
+ filter: EXCLUDEfilter
1278
+ }
1279
+ // assign groups to groupset
1280
+ const groupset = {
1281
+ name: GRPSETname,
1282
+ groups: [EXCLUDEgroup, MUTgroup, WTgroup],
1283
+ id: term.id
1284
+ }
1285
+ groupsetLst.push(groupset)
1286
+ }
1197
1287
 
1198
- /*
1199
- Term groupsetting used for geneVariant term
1200
- NOTE: for each groupsetting, groups[] is ordered by priority
1201
- for example: in the 'Protein-changing vs. rest' groupsetting, the
1202
- 'Protein-changing' group is listed first in groups[] so that samples
1203
- that have both missense and silent mutations are classified in the
1204
- 'Protein-changing' group
1205
- */
1206
1288
  export const geneVariantTermGroupsetting = {
1207
1289
  disabled: false, // as const, // TODO: may need to add is when converting common.js to .ts
1208
1290
  type: 'custom',
1209
- lst: [
1210
- {
1211
- // SNV/indel groupsetting
1212
- name: 'Mutated vs. wildtype',
1213
- groups: [
1214
- {
1215
- type: 'values', // as const, // TODO: may need to add is when converting common.js to .ts
1216
- name: 'Mutated',
1217
- values: mutationClasses
1218
- .filter(key => key != 'WT' && key != 'Blank')
1219
- .map(key => {
1220
- return { key, label: mclass[key].label }
1221
- })
1222
- },
1223
- {
1224
- type: 'values',
1225
- name: 'Wildtype',
1226
- values: [{ key: 'WT', label: 'Wildtype' }]
1227
- },
1228
- {
1229
- type: 'values',
1230
- name: 'Not tested',
1231
- values: [{ key: 'Blank', label: 'Not tested' }],
1232
- uncomputable: true
1233
- }
1234
- ]
1235
- },
1236
- // SNV/indel groupsetting
1237
- {
1238
- name: 'Protein-changing vs. rest',
1239
- groups: [
1240
- {
1241
- type: 'values',
1242
- name: 'Protein-changing',
1243
- values: proteinChangingMutations.map(key => {
1244
- return { key, label: mclass[key].label }
1245
- })
1246
- },
1247
- {
1248
- type: 'values',
1249
- name: 'Rest',
1250
- values: Object.keys(mclass)
1251
- .filter(key => !proteinChangingMutations.includes(key) && key != 'Blank')
1252
- .map(key => {
1253
- return { key, label: mclass[key].label }
1254
- })
1255
- },
1256
- {
1257
- type: 'values',
1258
- name: 'Not tested',
1259
- values: [{ key: 'Blank', label: 'Not tested' }],
1260
- uncomputable: true
1261
- }
1262
- ]
1263
- },
1264
- // SNV/indel groupsetting
1265
- {
1266
- name: 'Truncating vs. rest',
1267
- groups: [
1268
- {
1269
- type: 'values',
1270
- name: 'Truncating',
1271
- values: truncatingMutations.map(key => {
1272
- return { key, label: mclass[key].label }
1273
- })
1274
- },
1275
- {
1276
- type: 'values',
1277
- name: 'Rest',
1278
- values: Object.keys(mclass)
1279
- .filter(key => !truncatingMutations.includes(key) && key != 'Blank')
1280
- .map(key => {
1281
- return { key, label: mclass[key].label }
1282
- })
1283
- },
1284
- {
1285
- type: 'values',
1286
- name: 'Not tested',
1287
- values: [{ key: 'Blank', label: 'Not tested' }],
1288
- uncomputable: true
1289
- }
1290
- ]
1291
- },
1292
- // CNV groupsetting
1293
- {
1294
- name: 'Gain vs. Loss vs. LOH vs. Wildtype',
1295
- groups: [
1296
- {
1297
- type: 'values',
1298
- name: 'Copy number gain',
1299
- values: [{ key: 'CNV_amp', label: mclass['CNV_amp'].label }]
1300
- },
1301
- {
1302
- type: 'values',
1303
- name: 'Copy number loss',
1304
- values: [{ key: 'CNV_loss', label: mclass['CNV_loss'].label }]
1305
- },
1306
- {
1307
- type: 'values',
1308
- name: 'LOH',
1309
- values: [{ key: 'CNV_loh', label: mclass['CNV_loh'].label }]
1310
- },
1311
- {
1312
- type: 'values',
1313
- name: 'Wildtype',
1314
- values: [{ key: 'WT', label: 'Wildtype' }]
1315
- },
1316
- {
1317
- type: 'values',
1318
- name: 'Not tested',
1319
- values: [{ key: 'Blank', label: 'Not tested' }],
1320
- uncomputable: true
1321
- }
1322
- ]
1323
- },
1324
- // SV fusion groupsetting
1325
- {
1326
- name: 'Fusion vs. Wildtype',
1327
- groups: [
1328
- {
1329
- type: 'values',
1330
- name: 'Fusion transcript',
1331
- values: [{ key: 'Fuserna', label: mclass['Fuserna'].label }]
1332
- },
1333
- {
1334
- type: 'values',
1335
- name: 'Wildtype',
1336
- values: [{ key: 'WT', label: 'Wildtype' }]
1337
- },
1338
- {
1339
- type: 'values',
1340
- name: 'Not tested',
1341
- values: [{ key: 'Blank', label: 'Not tested' }],
1342
- uncomputable: true
1343
- }
1344
- ]
1345
- }
1346
- ]
1291
+ lst: groupsetLst
1347
1292
  }
1348
1293
 
1349
1294
  export const colorScaleMap = {
package/src/filter.js CHANGED
@@ -242,3 +242,14 @@ export function filterJoin(lst) {
242
242
  }
243
243
  return f
244
244
  }
245
+
246
+ export function getWrappedTvslst(lst = [], join = '', $id = null) {
247
+ const filter = {
248
+ type: 'tvslst',
249
+ in: true,
250
+ join,
251
+ lst
252
+ }
253
+ if ($id !== null && filter.$id !== undefined) filter.$id = $id
254
+ return filter
255
+ }
@@ -15,6 +15,7 @@ export const graphableTypes = new Set([
15
15
  'dtsnvindel',
16
16
  'dtfusion',
17
17
  'dtsv',
18
+ 'date',
18
19
  TermTypes.METABOLITE_INTENSITY,
19
20
  TermTypes.SINGLECELL_GENE_EXPRESSION,
20
21
  TermTypes.SINGLECELL_CELLTYPE,
package/src/terms.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { dtgeneexpression, dtmetaboliteintensity, TermTypeGroups } from './common.js'
2
+ import { roundValueAuto } from './roundValue.js'
2
3
 
3
4
  // moved TermTypeGroups to `server/src/common.js`, so now has to re-export
4
5
  export { TermTypeGroups } from './common.js'
@@ -37,7 +38,8 @@ export const TermTypes = {
37
38
  METABOLITE_INTENSITY: 'metaboliteIntensity',
38
39
  SINGLECELL_GENE_EXPRESSION: 'singleCellGeneExpression',
39
40
  SINGLECELL_CELLTYPE: 'singleCellCellType',
40
- MULTIVALUE: 'multivalue'
41
+ MULTIVALUE: 'multivalue',
42
+ DATE: 'date'
41
43
  }
42
44
 
43
45
  export const NUMERIC_DICTIONARY_TERM = 'numericDictTerm'
@@ -80,7 +82,8 @@ export const numericTypes = new Set([
80
82
  TermTypes.FLOAT,
81
83
  TermTypes.GENE_EXPRESSION,
82
84
  TermTypes.METABOLITE_INTENSITY,
83
- TermTypes.SINGLECELL_GENE_EXPRESSION
85
+ TermTypes.SINGLECELL_GENE_EXPRESSION,
86
+ TermTypes.DATE
84
87
  ])
85
88
 
86
89
  const categoricalTypes = new Set([TermTypes.CATEGORICAL, TermTypes.SNP])
@@ -206,3 +209,37 @@ const typeMap = {
206
209
  export function termType2label(type) {
207
210
  return typeMap[type] || 'Unknown term type'
208
211
  }
212
+
213
+ /*
214
+ Value is a decimal year.
215
+ A decimal year is a way of expressing a date or time period as a year with a decimal part, where the decimal portion
216
+ represents the fraction of the year that has elapsed.
217
+ Example:
218
+ 2025.0 represents the beginning of the year 2025.
219
+ 2025.5 represents the middle of the year 2025.
220
+ */
221
+ export function getDateStrFromNumber(value) {
222
+ const year = Math.floor(value)
223
+ const time = (value - year) * 365 * 24 * 60 * 60 * 1000 // convert to milliseconds
224
+ const january1st = new Date(year, 0, 0)
225
+ const date = new Date(january1st.getTime() + time)
226
+
227
+ //Omit day to deidentify the patients
228
+ return date.toLocaleDateString('en-US', {
229
+ year: 'numeric',
230
+ month: 'long'
231
+ })
232
+ }
233
+
234
+ //The value returned is a decimal year
235
+ //A decimal year is a way of expressing a date or time period as a year with a decimal part, where the decimal portion
236
+ //represents the fraction of the year that has elapsed.
237
+ export function getNumberFromDateStr(str) {
238
+ const date = new Date(str)
239
+ const year = date.getFullYear()
240
+ const january1st = new Date(year, 0, 0)
241
+ const diffTime = date - january1st
242
+ const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
243
+ const decimal = roundValueAuto(diffDays / 365)
244
+ return year + decimal
245
+ }
package/src/time.js ADDED
@@ -0,0 +1,26 @@
1
+ function formatElapsedTime(ms) {
2
+ if (typeof ms !== "number") {
3
+ return "Invalid time: not a number";
4
+ }
5
+ if (isNaN(ms)) {
6
+ return "Invalid time: NaN";
7
+ }
8
+ if (!isFinite(ms)) {
9
+ return ms > 0 ? "Infinite time" : "-Infinite time";
10
+ }
11
+ const absMs = Math.abs(ms);
12
+ const sign = ms < 0 ? "-" : "";
13
+ if (absMs < 1e3) {
14
+ return `${sign}${absMs}ms`;
15
+ } else if (absMs < 6e4) {
16
+ const seconds = (absMs / 1e3).toFixed(2);
17
+ return `${sign}${seconds}s`;
18
+ } else {
19
+ const minutes = Math.floor(absMs / 6e4);
20
+ const seconds = (absMs % 6e4 / 1e3).toFixed(2);
21
+ return `${sign}${minutes}m ${seconds}s`;
22
+ }
23
+ }
24
+ export {
25
+ formatElapsedTime
26
+ };