qiprofile
Version:
Quantitative Imaging Profile ( QiPr) web application
152 lines (134 loc) • 5.77 kB
text/coffeescript
define ['angular', 'lodash', 'helpers'], (ng, _) ->
breast = ng.module 'qiprofile.breast', ['qiprofile.helpers']
breast.factory 'Breast', ['ObjectHelper', (ObjectHelper) ->
# The cancer stages correspond to TNM scores, assuming no
# metastasis (M0).
#
# TODO - extend this table to account for the T and N suffixes per
# the reference document mentioned below.
STAGES = [
['1A', '2A', '3A', '3C']
['1A', '2A', '3A', '3C']
['2A', '2B', '3A', '3C']
['2B', '3A','3A', '3C']
['3B', '3B','3B', '3C']
]
Grade:
RANGES: [[3..5], [6..7], [8..9]]
SCORES: ['tubularFormation', 'mitoticCount', 'nuclearPleomorphism']
# @returns the sorted stage values
stageExtent: ->
_.chain(STAGES).flatten().union(['4']).uniq().sort().value()
# Returns the cancer stage.
#
# 1f metastasis exists (M1), then the stage is 4.
# Otherwise, the stage is determined by T and N scores as
# defined in the tumor type factory STAGES associative
# lookup table.
#
# @param tnm the TNM object
# @returns the cancer stage object, as described in tnm.coffee
# stage
stage: (tnm) ->
# M1 => stage IV.
# TODO - shouldn't this be '4'? Is there a test case for this?
# Since the value is romanized prior to display, this bug might
# be hidden. Same with the Sarcoma stage.
if tnm.metastasis
return 'IV'
# The T and N scores.
# TODO - factor in the size suffix.
t = tnm.size.tumorSize
n = tnm.lymphStatus
# Lookup (T, N) in the stage table.
# If not found, then throw an error.
find = (table, value) ->
result = table[value] or
throw new ReferenceError("Unsupported #{ tnm.tumorType }" +
" TNM: #{ ObjectHelper.prettyPrint(tnm) }")
_.reduce([t, n], find, STAGES)
# Returns the cancer recurrence score. This score is calculated from
# a genetic expression assay according to algorithm in Figure 1 of
# the following paper:
#
# Paik, et al., 'A Multigene Assay to Predict Recurrence of
# Tamoxifen-Treated, Node-Negative Breast Cancer',
# N Engl J Med 2004; 351:2817-2826
# (http://www.nejm.org/doi/full/10.1056/NEJMoa041588)
#
# 1f metastasis exists (M1), then the stage is 4.
# Otherwise, the stage is determined by T and N scores as
# defined in the tumor type factory STAGES associative
# lookup table.
#
# @param tnm the TNM object
# @returns the cancer stage object, as described in tnm.coffee
# stage
recurrenceScore: (assay) ->
her2Unscaled = (0.9 * assay.her2.grb7) + (0.1 * assay.her2.her2)
her2 = Math.max(8, her2Unscaled)
erUnscaled = (0.8 * assay.estrogen.er) + (1.2 * assay.estrogen.pgr) +
assay.estrogen.bcl2 + assay.estrogen.scube2
er = erUnscaled / 4
proliferationUnscaled = (assay.proliferation.survivin + assay.proliferation.ki67 +
assay.proliferation.mybl2 + assay.proliferation.ccnb1 +
assay.proliferation.stk15) / 5
proliferation = Math.max(6.5, proliferationUnscaled)
invasion = (assay.invasion.ctsl2 + assay.invasion.mmp11) / 2
# The unscaled score.
recurrenceUnscaled = (0.47 * her2) - (0.34 * er) + (1.04 * proliferation) +
(0.10 * invasion) + (0.05 * assay.cd68) -
(0.08 * assay.gstm1) - (0.07 * assay.bag1)
# Guard against missing values.
if isNaN(recurrenceUnscaled)
return null
recurrenceScaled = Math.round(20 * (recurrenceUnscaled - 6.7))
# Return the score fit to the range [0, 100].
Math.max(0, Math.min(recurrenceScaled, 100))
# Calculates the Residual Cancer Burden index and class as described in:
# JCO 25:28 4414-4422 <http://jco.ascopubs.org/content/25/28/4414.full>
#
# @param tumor the tumor object
# @returns the RCB object extended with index and class properties
residualCancerBurden: (tumor) ->
# @param extent the tumor extent {length, width, depth} REST object
# @param rcb the RCB REST object
# @returns the RCB index
rcbIndex = (extent, rcb) ->
# The bidimensional tumor size metric.
size = Math.sqrt(extent.length * extent.width)
# The overall tumor cellularity.
overall = rcb.tumorCellDensity / 100
# The in situ cellularity.
inSitu = rcb.dcisCellDensity / 100
# The invasive carcinoma proportion.
invasion = (1 - inSitu) * overall
# The RCB index invasion component.
invasionFactor = 1.4 * Math.pow(invasion * size, 0.17)
# The RCB index positive node component.
posNodeFactor = 1 - Math.pow(0.75, rcb.positiveNodeCount)
# The base of the RCB index node component.
nodeBase = 4 * posNodeFactor * rcb.largestNodalMetastasisLength
# The RCB index node component.
nodeFactor = Math.pow(nodeBase, 0.17)
# The RCB index is the sum of the invasion and node components.
invasionFactor + nodeFactor
# @param index the calculated RCB index value
# @returns the RCB class based on RCB index cut-offs
rcbClass = (index) ->
if index == 0
return 0
else if index < 1.36
return 1
else if index < 3.28
return 2
else
return 3
# If the RCB object exists, then return it extended with the
# index and class properties.
rcb = tumor.rcb
if rcb?
rcb.index = rcbIndex(tumor.extent, tumor.rcb)
rcb.class = rcbClass(rcb.index)
rcb
]