@sjcrh/proteinpaint-shared 2.113.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.113.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,156 +1154,141 @@ export const CNVClasses = Object.values(mclass)
1153
1154
  .filter(m => m.dt == dtcnv)
1154
1155
  .map(m => m.key)
1155
1156
 
1156
- /*
1157
- Term groupsetting used for geneVariant term
1158
- NOTE: for each groupsetting, groups[] is ordered by priority
1159
- for example: in the 'Protein-changing vs. rest' groupsetting, the
1160
- 'Protein-changing' group is listed first in groups[] so that samples
1161
- that have both missense and silent mutations are classified in the
1162
- 'Protein-changing' group
1163
- */
1157
+ // dt terms used for filtering variants for geneVariant term
1158
+ const _dtTerms = [
1159
+ {
1160
+ id: 'snvindel',
1161
+ query: 'snvindel',
1162
+ name: dt2label[dtsnvindel],
1163
+ parent_id: null,
1164
+ isleaf: true,
1165
+ type: 'dtsnvindel',
1166
+ dt: dtsnvindel,
1167
+ values: Object.fromEntries(
1168
+ mutationClasses.filter(key => key != 'Blank').map(key => [key, { label: mclass[key].label }])
1169
+ )
1170
+ },
1171
+ {
1172
+ id: 'cnv',
1173
+ query: 'cnv',
1174
+ name: dt2label[dtcnv],
1175
+ parent_id: null,
1176
+ isleaf: true,
1177
+ type: 'dtcnv',
1178
+ dt: dtcnv,
1179
+ values: Object.fromEntries([...CNVClasses, 'WT'].map(key => [key, { label: mclass[key].label }]))
1180
+ },
1181
+ {
1182
+ id: 'fusion',
1183
+ query: 'svfusion',
1184
+ name: dt2label[dtfusionrna],
1185
+ parent_id: null,
1186
+ isleaf: true,
1187
+ type: 'dtfusion',
1188
+ dt: dtfusionrna,
1189
+ values: Object.fromEntries([mclassfusionrna, 'WT'].map(key => [key, { label: mclass[key].label }]))
1190
+ },
1191
+ {
1192
+ id: 'sv',
1193
+ query: 'svfusion',
1194
+ name: dt2label[dtsv],
1195
+ parent_id: null,
1196
+ isleaf: true,
1197
+ type: 'dtsv',
1198
+ dt: dtsv,
1199
+ values: Object.fromEntries([mclasssv, 'WT'].map(key => [key, { label: mclass[key].label }]))
1200
+ }
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
+ }
1164
1287
 
1165
1288
  export const geneVariantTermGroupsetting = {
1166
1289
  disabled: false, // as const, // TODO: may need to add is when converting common.js to .ts
1167
1290
  type: 'custom',
1168
- lst: [
1169
- {
1170
- // SNV/indel groupsetting
1171
- name: 'Mutated vs. wildtype',
1172
- groups: [
1173
- {
1174
- type: 'values', // as const, // TODO: may need to add is when converting common.js to .ts
1175
- name: 'Mutated',
1176
- values: mutationClasses
1177
- .filter(key => key != 'WT' && key != 'Blank')
1178
- .map(key => {
1179
- return { key, label: mclass[key].label }
1180
- })
1181
- },
1182
- {
1183
- type: 'values',
1184
- name: 'Wildtype',
1185
- values: [{ key: 'WT', label: 'Wildtype' }]
1186
- },
1187
- {
1188
- type: 'values',
1189
- name: 'Not tested',
1190
- values: [{ key: 'Blank', label: 'Not tested' }],
1191
- uncomputable: true
1192
- }
1193
- ]
1194
- },
1195
- // SNV/indel groupsetting
1196
- {
1197
- name: 'Protein-changing vs. rest',
1198
- groups: [
1199
- {
1200
- type: 'values',
1201
- name: 'Protein-changing',
1202
- values: proteinChangingMutations.map(key => {
1203
- return { key, label: mclass[key].label }
1204
- })
1205
- },
1206
- {
1207
- type: 'values',
1208
- name: 'Rest',
1209
- values: Object.keys(mclass)
1210
- .filter(key => !proteinChangingMutations.includes(key) && key != 'Blank')
1211
- .map(key => {
1212
- return { key, label: mclass[key].label }
1213
- })
1214
- },
1215
- {
1216
- type: 'values',
1217
- name: 'Not tested',
1218
- values: [{ key: 'Blank', label: 'Not tested' }],
1219
- uncomputable: true
1220
- }
1221
- ]
1222
- },
1223
- // SNV/indel groupsetting
1224
- {
1225
- name: 'Truncating vs. rest',
1226
- groups: [
1227
- {
1228
- type: 'values',
1229
- name: 'Truncating',
1230
- values: truncatingMutations.map(key => {
1231
- return { key, label: mclass[key].label }
1232
- })
1233
- },
1234
- {
1235
- type: 'values',
1236
- name: 'Rest',
1237
- values: Object.keys(mclass)
1238
- .filter(key => !truncatingMutations.includes(key) && key != 'Blank')
1239
- .map(key => {
1240
- return { key, label: mclass[key].label }
1241
- })
1242
- },
1243
- {
1244
- type: 'values',
1245
- name: 'Not tested',
1246
- values: [{ key: 'Blank', label: 'Not tested' }],
1247
- uncomputable: true
1248
- }
1249
- ]
1250
- },
1251
- // CNV groupsetting
1252
- {
1253
- name: 'Gain vs. Loss vs. LOH vs. Wildtype',
1254
- groups: [
1255
- {
1256
- type: 'values',
1257
- name: 'Copy number gain',
1258
- values: [{ key: 'CNV_amp', label: mclass['CNV_amp'].label }]
1259
- },
1260
- {
1261
- type: 'values',
1262
- name: 'Copy number loss',
1263
- values: [{ key: 'CNV_loss', label: mclass['CNV_loss'].label }]
1264
- },
1265
- {
1266
- type: 'values',
1267
- name: 'LOH',
1268
- values: [{ key: 'CNV_loh', label: mclass['CNV_loh'].label }]
1269
- },
1270
- {
1271
- type: 'values',
1272
- name: 'Wildtype',
1273
- values: [{ key: 'WT', label: 'Wildtype' }]
1274
- },
1275
- {
1276
- type: 'values',
1277
- name: 'Not tested',
1278
- values: [{ key: 'Blank', label: 'Not tested' }],
1279
- uncomputable: true
1280
- }
1281
- ]
1282
- },
1283
- // SV fusion groupsetting
1284
- {
1285
- name: 'Fusion vs. Wildtype',
1286
- groups: [
1287
- {
1288
- type: 'values',
1289
- name: 'Fusion transcript',
1290
- values: [{ key: 'Fuserna', label: mclass['Fuserna'].label }]
1291
- },
1292
- {
1293
- type: 'values',
1294
- name: 'Wildtype',
1295
- values: [{ key: 'WT', label: 'Wildtype' }]
1296
- },
1297
- {
1298
- type: 'values',
1299
- name: 'Not tested',
1300
- values: [{ key: 'Blank', label: 'Not tested' }],
1301
- uncomputable: true
1302
- }
1303
- ]
1304
- }
1305
- ]
1291
+ lst: groupsetLst
1306
1292
  }
1307
1293
 
1308
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
+ }
@@ -11,11 +11,17 @@ export const graphableTypes = new Set([
11
11
  'geneVariant',
12
12
  'samplelst',
13
13
  'geneExpression',
14
+ 'dtcnv',
15
+ 'dtsnvindel',
16
+ 'dtfusion',
17
+ 'dtsv',
18
+ 'date',
14
19
  TermTypes.METABOLITE_INTENSITY,
15
20
  TermTypes.SINGLECELL_GENE_EXPRESSION,
16
21
  TermTypes.SINGLECELL_CELLTYPE,
17
22
  TermTypes.SNP
18
23
  ])
24
+
19
25
  /*
20
26
  isUsableTerm() will
21
27
 
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
+ };