diff --git a/Changelog.md b/Changelog.md index 7e1968a..488bcf2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ Small and insignificant changes may not be included in this log. ## Unversioned Changes +* Improved the creation process of genetic analyses in various ways. A sample of offspring polygenic disease risk scores are now created and viewable by users. - *Simon Sarasova* * Improved the genetic analysis creation process in various ways. Recombination breakpoints are more accurately predicted now. - *Simon Sarasova* * Improved the identity hash generation tool. The fastest quantity of goroutines is now identified and used. - *Simon Sarasova* * Improved the creation procedures, encoding format, and graphical presentation of genetic analyses. Map lists have been replaced by custom objects. - *Simon Sarasova* diff --git a/Contributors.md b/Contributors.md index 5a9d5b8..9a7b304 100644 --- a/Contributors.md +++ b/Contributors.md @@ -9,4 +9,4 @@ Many other people have written code for modules which are imported by Seekia. Th Name | Date Of First Commit | Number Of Commits --- | --- | --- -Simon Sarasova | June 13, 2023 | 250 \ No newline at end of file +Simon Sarasova | June 13, 2023 | 251 \ No newline at end of file diff --git a/gui/statisticsGui.go b/gui/statisticsGui.go index 50a3e9c..5deee89 100644 --- a/gui/statisticsGui.go +++ b/gui/statisticsGui.go @@ -18,6 +18,7 @@ import "seekia/internal/helpers" import "seekia/internal/network/appNetworkType/getAppNetworkType" import "seekia/internal/profiles/attributeDisplay" import "seekia/internal/profiles/userStatistics" +import "seekia/internal/statisticsDatum" import "errors" import "image" @@ -146,13 +147,13 @@ func setViewUserAttributeStatisticsPage_BarChart( showYAxisPercentage bool, statisticsReady bool, anyUsersExist bool, - statisticsItemsList []userStatistics.StatisticsItem, + statisticsDatumsList []statisticsDatum.StatisticsDatum, groupingPerformed bool, - groupedStatisticsItemsList []userStatistics.StatisticsItem, + groupedStatisticsDatumsList []statisticsDatum.StatisticsDatum, chartImage image.Image, previousPage func()){ - currentPage := func(){setViewUserAttributeStatisticsPage_BarChart(window, identityType, xAxisAttributeName, yAxisAttribute, yAxisUnits, showYAxisPercentage, statisticsReady, anyUsersExist, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, chartImage, previousPage)} + currentPage := func(){setViewUserAttributeStatisticsPage_BarChart(window, identityType, xAxisAttributeName, yAxisAttribute, yAxisUnits, showYAxisPercentage, statisticsReady, anyUsersExist, statisticsDatumsList, groupingPerformed, groupedStatisticsDatumsList, chartImage, previousPage)} pageIdentifier, err := helpers.GetNewRandomHexString(16) if (err != nil) { @@ -264,7 +265,7 @@ func setViewUserAttributeStatisticsPage_BarChart( } viewDataButton := widget.NewButtonWithIcon("View Data", theme.ListIcon(), func(){ - setViewUserStatisticsDataPage(window, xAxisAttributeTitle, yAxisAttribute, showYAxisPercentage, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, true, xAxisUnits, yAxisUnits, currentPage) + setViewUserStatisticsDataPage(window, xAxisAttributeTitle, yAxisAttribute, showYAxisPercentage, statisticsDatumsList, groupingPerformed, groupedStatisticsDatumsList, true, xAxisUnits, yAxisUnits, currentPage) }) viewFullscreenButton := widget.NewButtonWithIcon("View Fullscreen", theme.ZoomInIcon(), func(){ @@ -336,7 +337,7 @@ func setViewUserAttributeStatisticsPage_BarChart( go updateLoadingBindingFunction() - totalAnalyzedUsers, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, formatYAxisValuesFunction, err := userStatistics.GetUserStatisticsItemsLists_BarChart(identityType, appNetworkType, xAxisAttributeName, xAxisIsNumerical, formatXAxisValuesFunction, unknownXAxisValuesTextTranslated, yAxisAttribute) + totalAnalyzedUsers, statisticsDatumsList, groupingPerformed, groupedStatisticsDatumsList, formatYAxisValuesFunction, err := userStatistics.GetUserStatisticsDatumsLists_BarChart(identityType, appNetworkType, xAxisAttributeName, xAxisIsNumerical, formatXAxisValuesFunction, unknownXAxisValuesTextTranslated, yAxisAttribute) if (err != nil) { functionCompleteBoolMutex.Lock() @@ -350,7 +351,7 @@ func setViewUserAttributeStatisticsPage_BarChart( return } - if (len(statisticsItemsList) == 0){ + if (len(statisticsDatumsList) == 0){ functionCompleteBoolMutex.Lock() functionCompleteBool = true @@ -363,14 +364,14 @@ func setViewUserAttributeStatisticsPage_BarChart( return } - getChartStatisticsItemsList := func()[]userStatistics.StatisticsItem{ + getChartStatisticsDatumsList := func()[]statisticsDatum.StatisticsDatum{ if (groupingPerformed == false){ - return statisticsItemsList + return statisticsDatumsList } - return groupedStatisticsItemsList + return groupedStatisticsDatumsList } - chartStatisticsItemsList := getChartStatisticsItemsList() + chartStatisticsDatumsList := getChartStatisticsDatumsList() getChartTitle := func()string{ @@ -401,7 +402,7 @@ func setViewUserAttributeStatisticsPage_BarChart( chartTitle := getChartTitle() - newChartImage, err := createCharts.CreateBarChart(chartTitle, chartStatisticsItemsList, formatYAxisValuesFunction, true, yAxisUnits) + newChartImage, err := createCharts.CreateBarChart(chartTitle, chartStatisticsDatumsList, formatYAxisValuesFunction, true, yAxisUnits) if (err != nil) { functionCompleteBoolMutex.Lock() @@ -421,7 +422,7 @@ func setViewUserAttributeStatisticsPage_BarChart( pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == false){ - setViewUserAttributeStatisticsPage_BarChart(window, identityType, xAxisAttributeName, yAxisAttribute, yAxisUnits, showYAxisPercentage, true, true, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, newChartImage, previousPage) + setViewUserAttributeStatisticsPage_BarChart(window, identityType, xAxisAttributeName, yAxisAttribute, yAxisUnits, showYAxisPercentage, true, true, statisticsDatumsList, groupingPerformed, groupedStatisticsDatumsList, newChartImage, previousPage) } } @@ -441,13 +442,13 @@ func setViewUserAttributeStatisticsPage_DonutChart( attributeName string, statisticsReady bool, anyUsersExist bool, - statisticsItemsList []userStatistics.StatisticsItem, + statisticsDatumsList []statisticsDatum.StatisticsDatum, groupingPerformed bool, - groupedStatisticsItemsList []userStatistics.StatisticsItem, + groupedStatisticsDatumsList []statisticsDatum.StatisticsDatum, chartImage image.Image, previousPage func()){ - currentPage := func(){setViewUserAttributeStatisticsPage_DonutChart(window, identityType, attributeName, statisticsReady, anyUsersExist, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, chartImage, previousPage)} + currentPage := func(){setViewUserAttributeStatisticsPage_DonutChart(window, identityType, attributeName, statisticsReady, anyUsersExist, statisticsDatumsList, groupingPerformed, groupedStatisticsDatumsList, chartImage, previousPage)} pageIdentifier, err := helpers.GetNewRandomHexString(16) if (err != nil) { @@ -545,7 +546,7 @@ func setViewUserAttributeStatisticsPage_DonutChart( viewDataButton := widget.NewButtonWithIcon("View Data", theme.ListIcon(), func(){ - setViewUserStatisticsDataPage(window, attributeTitle, "Number Of Users", true, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, true, attributeUnits, "Users", currentPage) + setViewUserStatisticsDataPage(window, attributeTitle, "Number Of Users", true, statisticsDatumsList, groupingPerformed, groupedStatisticsDatumsList, true, attributeUnits, "Users", currentPage) }) viewFullscreenButton := widget.NewButtonWithIcon("View Fullscreen", theme.ZoomInIcon(), func(){ @@ -616,7 +617,7 @@ func setViewUserAttributeStatisticsPage_DonutChart( go updateLoadingBindingFunction() - totalAnalyzedUsers, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, err := userStatistics.GetUserStatisticsItemsLists_DonutChart(identityType, appNetworkType, attributeName, attributeIsNumerical, formatAttributeValuesFunction, unknownValuesTextTranslated) + totalAnalyzedUsers, statisticsDatumsList, groupingPerformed, groupedStatisticsDatumsList, err := userStatistics.GetUserStatisticsDatumsLists_DonutChart(identityType, appNetworkType, attributeName, attributeIsNumerical, formatAttributeValuesFunction, unknownValuesTextTranslated) if (err != nil) { functionCompleteBoolMutex.Lock() @@ -630,7 +631,7 @@ func setViewUserAttributeStatisticsPage_DonutChart( return } - if (len(statisticsItemsList) == 0){ + if (len(statisticsDatumsList) == 0){ functionCompleteBoolMutex.Lock() functionCompleteBool = true @@ -643,14 +644,14 @@ func setViewUserAttributeStatisticsPage_DonutChart( return } - getChartStatisticsItemsList := func()[]userStatistics.StatisticsItem{ + getChartStatisticsDatumsList := func()[]statisticsDatum.StatisticsDatum{ if (groupingPerformed == false){ - return statisticsItemsList + return statisticsDatumsList } - return groupedStatisticsItemsList + return groupedStatisticsDatumsList } - chartStatisticsItemsList := getChartStatisticsItemsList() + chartStatisticsDatumsList := getChartStatisticsDatumsList() getChartTitle := func()string{ @@ -672,7 +673,7 @@ func setViewUserAttributeStatisticsPage_DonutChart( chartTitle := getChartTitle() - newChartImage, err := createCharts.CreateDonutChart(chartTitle, chartStatisticsItemsList) + newChartImage, err := createCharts.CreateDonutChart(chartTitle, chartStatisticsDatumsList) if (err != nil){ functionCompleteBoolMutex.Lock() @@ -692,7 +693,7 @@ func setViewUserAttributeStatisticsPage_DonutChart( pageHasChanged := checkIfPageHasChangedFunction() if (pageHasChanged == false){ - setViewUserAttributeStatisticsPage_DonutChart(window, identityType, attributeName, true, true, statisticsItemsList, groupingPerformed, groupedStatisticsItemsList, newChartImage, previousPage) + setViewUserAttributeStatisticsPage_DonutChart(window, identityType, attributeName, true, true, statisticsDatumsList, groupingPerformed, groupedStatisticsDatumsList, newChartImage, previousPage) } } @@ -934,9 +935,9 @@ func setViewUserStatisticsDataPage( attributeTitle string, rightColumnName string, showPercentageColumn bool, - statisticsItemsList []userStatistics.StatisticsItem, + statisticsDatumsList []statisticsDatum.StatisticsDatum, groupingPerformed bool, - groupedStatisticsItemsList []userStatistics.StatisticsItem, + groupedStatisticsDatumsList []statisticsDatum.StatisticsDatum, showGroupedStatistics bool, attributeColumnUnits string, rightColumnUnits string, @@ -954,13 +955,13 @@ func setViewUserStatisticsDataPage( if (showGroupedStatistics == false){ showDataGroupedButton := getWidgetCentered(widget.NewButton("Show Data Grouped", func(){ - setViewUserStatisticsDataPage(window, attributeTitle, rightColumnName, showPercentageColumn, statisticsItemsList, true, groupedStatisticsItemsList, true, attributeColumnUnits, rightColumnUnits, previousPage) + setViewUserStatisticsDataPage(window, attributeTitle, rightColumnName, showPercentageColumn, statisticsDatumsList, true, groupedStatisticsDatumsList, true, attributeColumnUnits, rightColumnUnits, previousPage) })) header.Add(showDataGroupedButton) } else { showDataRawButton := getWidgetCentered(widget.NewButton("Show Data Raw", func(){ - setViewUserStatisticsDataPage(window, attributeTitle, rightColumnName, showPercentageColumn, statisticsItemsList, true, groupedStatisticsItemsList, false, attributeColumnUnits, rightColumnUnits, previousPage) + setViewUserStatisticsDataPage(window, attributeTitle, rightColumnName, showPercentageColumn, statisticsDatumsList, true, groupedStatisticsDatumsList, false, attributeColumnUnits, rightColumnUnits, previousPage) })) header.Add(showDataRawButton) @@ -968,14 +969,14 @@ func setViewUserStatisticsDataPage( header.Add(widget.NewSeparator()) } - getStatisticsItemsListToShow := func()[]userStatistics.StatisticsItem{ + getStatisticsDatumsListToShow := func()[]statisticsDatum.StatisticsDatum{ if (groupingPerformed == true && showGroupedStatistics == true){ - return groupedStatisticsItemsList + return groupedStatisticsDatumsList } - return statisticsItemsList + return statisticsDatumsList } - statisticsItemsToShowList := getStatisticsItemsListToShow() + statisticsDatumsToShowList := getStatisticsDatumsListToShow() getStatisticsDataGrid := func()(*fyne.Container, error){ @@ -983,38 +984,38 @@ func setViewUserStatisticsDataPage( if (showPercentageColumn == true){ - for _, item := range statisticsItemsToShowList{ + for _, datum := range statisticsDatumsToShowList{ - itemValue := item.Value + datumValue := datum.Value - allValuesSummed += itemValue + allValuesSummed += datumValue } } - attributeColumnValuesList := make([]string, 0, len(statisticsItemsToShowList)) - rightColumnValuesList := make([]string, 0, len(statisticsItemsToShowList)) - percentageColumnValuesList := make([]string, 0, len(statisticsItemsToShowList)) + attributeColumnValuesList := make([]string, 0, len(statisticsDatumsToShowList)) + rightColumnValuesList := make([]string, 0, len(statisticsDatumsToShowList)) + percentageColumnValuesList := make([]string, 0, len(statisticsDatumsToShowList)) - for _, item := range statisticsItemsToShowList{ + for _, datum := range statisticsDatumsToShowList{ - // Each item is represents a row in the data grid. + // Each datum is represents a row in the data grid. - itemLabelFormatted := item.LabelFormatted + datumLabelFormatted := datum.LabelFormatted - itemValueFormatted := item.ValueFormatted + datumValueFormatted := datum.ValueFormatted - attributeColumnValuesList = append(attributeColumnValuesList, itemLabelFormatted) - rightColumnValuesList = append(rightColumnValuesList, itemValueFormatted) + attributeColumnValuesList = append(attributeColumnValuesList, datumLabelFormatted) + rightColumnValuesList = append(rightColumnValuesList, datumValueFormatted) if (showPercentageColumn == true){ - itemValue := item.Value + datumValue := datum.Value getValuePercentage := func()float64{ if (allValuesSummed == 0){ return 0 } - valuePercentage := (itemValue/allValuesSummed)*100 + valuePercentage := (datumValue/allValuesSummed)*100 return valuePercentage } diff --git a/gui/viewAnalysisGui_Couple.go b/gui/viewAnalysisGui_Couple.go index 26a4e51..2cc043f 100644 --- a/gui/viewAnalysisGui_Couple.go +++ b/gui/viewAnalysisGui_Couple.go @@ -8,20 +8,24 @@ import "fyne.io/fyne/v2/container" import "fyne.io/fyne/v2/layout" import "fyne.io/fyne/v2/theme" import "fyne.io/fyne/v2/widget" +import "fyne.io/fyne/v2/canvas" import "seekia/resources/geneticReferences/monogenicDiseases" import "seekia/resources/geneticReferences/polygenicDiseases" import "seekia/resources/geneticReferences/traits" import "seekia/internal/appMemory" +import "seekia/internal/createCharts" import "seekia/internal/encoding" import "seekia/internal/genetics/geneticAnalysis" import "seekia/internal/genetics/myGenomes" import "seekia/internal/genetics/myPeople" import "seekia/internal/genetics/readGeneticAnalysis" import "seekia/internal/helpers" +import "seekia/internal/statisticsDatum" import "slices" +import "image" import "errors" @@ -1006,7 +1010,7 @@ func setViewCoupleGeneticAnalysisPolygenicDiseasesPage(window fyne.Window, perso mainGenomePairIdentifier := helpers.JoinTwo16ByteArrays(pair1Person1GenomeIdentifier, pair1Person2GenomeIdentifier) - offspringRiskScoreKnown, _, offspringRiskScoreFormatted, _, conflictExists, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject, diseaseName, mainGenomePairIdentifier) + offspringRiskScoreKnown, _, offspringRiskScoreFormatted, _, _, conflictExists, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject, diseaseName, mainGenomePairIdentifier) if (err != nil) { return nil, err } getRiskScoreLabelText := func()string{ @@ -1116,25 +1120,27 @@ func setViewCoupleGeneticAnalysisPolygenicDiseaseDetailsPage(window fyne.Window, }) diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameLabel, diseaseNameText, diseaseNameInfoButton, layout.NewSpacer()) - emptyLabelA := widget.NewLabel("") - emptyLabelB := widget.NewLabel("") + emptyLabel1 := widget.NewLabel("") + emptyLabel2 := widget.NewLabel("") offspringRiskScoreLabel := getItalicLabelCentered("Offspring Risk Score") - emptyLabelC := widget.NewLabel("") - emptyLabelD := widget.NewLabel("") + emptyLabel3 := widget.NewLabel("") + emptyLabel4 := widget.NewLabel("") + emptyLabel5 := widget.NewLabel("") - viewGenomePairButtonsColumn := container.NewVBox(emptyLabelA, widget.NewSeparator()) - pairNameColumn := container.NewVBox(emptyLabelB, widget.NewSeparator()) + viewGenomePairButtonsColumn := container.NewVBox(emptyLabel1, widget.NewSeparator()) + pairNameColumn := container.NewVBox(emptyLabel2, widget.NewSeparator()) offspringRiskScoreColumn := container.NewVBox(offspringRiskScoreLabel, widget.NewSeparator()) - viewLifetimeRiskButtonsColumn := container.NewVBox(emptyLabelC, widget.NewSeparator()) - viewOffspringLociButtonsColumn := container.NewVBox(emptyLabelD, widget.NewSeparator()) + viewSampleOffspringsChartButtonsColumn := container.NewVBox(emptyLabel3, widget.NewSeparator()) + viewLifetimeRiskButtonsColumn := container.NewVBox(emptyLabel4, widget.NewSeparator()) + viewOffspringLociButtonsColumn := container.NewVBox(emptyLabel5, widget.NewSeparator()) addGenomePairRow := func(genomePairName string, person1GenomeIdentifier [16]byte, person2GenomeIdentifier [16]byte)error{ genomePairIdentifier := helpers.JoinTwo16ByteArrays(person1GenomeIdentifier, person2GenomeIdentifier) - offspringRiskScoreKnown, _, offspringRiskScoreFormatted, _, _, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject, diseaseName, genomePairIdentifier) + offspringRiskScoreKnown, _, offspringRiskScoreFormatted, sampleOffspringRiskScoresList, numberOfLociTested, _, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject, diseaseName, genomePairIdentifier) if (err != nil) { return err } getRiskScoreLabelText := func()string{ @@ -1158,6 +1164,10 @@ func setViewCoupleGeneticAnalysisPolygenicDiseaseDetailsPage(window fyne.Window, setViewCoupleGeneticAnalysisPolygenicDiseaseGenomePairDetailsPage(window, person1Name, person2Name, person1AnalysisObject, person2AnalysisObject, coupleAnalysisObject, diseaseName, genomePairIdentifier, genomePairName, currentPage) }) + viewSampleOffspringsChartButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + setViewPolygenicDiseaseSampleOffspringRiskScoresChart(window, diseaseName, sampleOffspringRiskScoresList, numberOfLociTested, currentPage) + }) + viewOffspringLifetimeRiskButton := widget.NewButtonWithIcon("", theme.HistoryIcon(), func(){ diseaseObject, err := polygenicDiseases.GetPolygenicDiseaseObject(diseaseName) @@ -1190,12 +1200,15 @@ func setViewCoupleGeneticAnalysisPolygenicDiseaseDetailsPage(window fyne.Window, viewGenomePairButtonsColumn.Add(viewGenomePairButton) pairNameColumn.Add(genomePairNameLabel) offspringRiskScoreColumn.Add(offspringRiskScoreLabel) + viewSampleOffspringsChartButtonsColumn.Add(viewSampleOffspringsChartButton) viewLifetimeRiskButtonsColumn.Add(viewOffspringLifetimeRiskButton) viewOffspringLociButtonsColumn.Add(viewOffspringLociButton) viewGenomePairButtonsColumn.Add(widget.NewSeparator()) pairNameColumn.Add(widget.NewSeparator()) offspringRiskScoreColumn.Add(widget.NewSeparator()) + viewSampleOffspringsChartButtonsColumn.Add(widget.NewSeparator()) + viewLifetimeRiskButtonsColumn.Add(widget.NewSeparator()) viewOffspringLociButtonsColumn.Add(widget.NewSeparator()) return nil @@ -1221,7 +1234,7 @@ func setViewCoupleGeneticAnalysisPolygenicDiseaseDetailsPage(window fyne.Window, offspringRiskScoreColumn.Add(offspringRiskScoreHelpButton) - genomesContainer := container.NewHBox(layout.NewSpacer(), viewGenomePairButtonsColumn, pairNameColumn, offspringRiskScoreColumn, viewOffspringLociButtonsColumn, layout.NewSpacer()) + genomesContainer := container.NewHBox(layout.NewSpacer(), viewGenomePairButtonsColumn, pairNameColumn, offspringRiskScoreColumn, viewSampleOffspringsChartButtonsColumn, viewLifetimeRiskButtonsColumn, viewOffspringLociButtonsColumn, layout.NewSpacer()) page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionSection, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), genomesContainer) @@ -1314,7 +1327,7 @@ func setViewCoupleGeneticAnalysisPolygenicDiseaseGenomePairDetailsPage(window fy genomeNameLabel := getBoldLabelCentered(genomeName) - personRiskScoreKnown, _, personRiskScoreFormatted, numberOfLociTested, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject, diseaseName, personAnalysisGenomeIdentifier) + personRiskScoreKnown, _, personRiskScoreFormatted, _, numberOfLociTested, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject, diseaseName, personAnalysisGenomeIdentifier) if (err != nil) { return err } getPersonRiskScoreLabelText := func()string{ @@ -1408,12 +1421,12 @@ func setViewCouplePolygenicDiseaseLociPage(window fyne.Window, person1Name strin genomePairRow := container.NewHBox(layout.NewSpacer(), genomePairLabel, genomePairNameLabel, viewGenomePairInfoButton, layout.NewSpacer()) - _, _, _, numberOfLociTested, _, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject, diseaseName, genomePairIdentifier) + _, _, _, _, numberOfLociTested, _, err := readGeneticAnalysis.GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject, diseaseName, genomePairIdentifier) if (err != nil){ setErrorEncounteredPage(window, err, previousPage) return } - + numberOfLociTestedString := helpers.ConvertIntToString(numberOfLociTested) diseaseLociMap, err := polygenicDiseases.GetPolygenicDiseaseLociMap(diseaseName) @@ -1719,6 +1732,133 @@ func setViewCoupleGeneticAnalysisPolygenicDiseaseLocusDetailsPage(window fyne.Wi setPageContent(page, window) } +// This is a page that shows the user 100 sample offspring polygenic disease risk scores on a bar chart +// This helps users to visualize the standard deviation of their offspring's disease risk with this user +func setViewPolygenicDiseaseSampleOffspringRiskScoresChart(window fyne.Window, diseaseName string, sampleOffspringRiskScoresList []int, numberOfLociTested int, previousPage func()){ + + currentPage := func(){setViewPolygenicDiseaseSampleOffspringRiskScoresChart(window, diseaseName, sampleOffspringRiskScoresList, numberOfLociTested, previousPage)} + + title := getPageTitleCentered("Viewing Sample Offspring Risk Scores Chart") + + backButton := getBackButtonCentered(previousPage) + + description := widget.NewLabel("Below is a chart of 100 sample offspring risk scores for this disease.") + descriptionHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + //TODO + showUnderConstructionDialog(window) + }) + descriptionRow := container.NewHBox(layout.NewSpacer(), description, descriptionHelpButton, layout.NewSpacer()) + + diseaseNameTitle := widget.NewLabel("Disease Name:") + diseaseNameLabel := getBoldLabel(diseaseName) + diseaseNameRow := container.NewHBox(layout.NewSpacer(), diseaseNameTitle, diseaseNameLabel, layout.NewSpacer()) + + if (len(sampleOffspringRiskScoresList) == 0){ + description2 := getBoldLabelCentered("There is no offspring information available for this disease.") + description3 := getBoldLabelCentered("This is because there were no disease loci for which both prospective parents had information.") + + page := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionRow, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), description2, description3) + + setPageContent(page, window) + return + } + + diseaseLociMap, err := polygenicDiseases.GetPolygenicDiseaseLociMap(diseaseName) + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + totalNumberOfLoci := len(diseaseLociMap) + totalNumberOfLociString := helpers.ConvertIntToString(totalNumberOfLoci) + + numberOfLociTestedTitle := widget.NewLabel("Number Of Loci Tested:") + numberOfLociTestedString := helpers.ConvertIntToString(numberOfLociTested) + numberOfLociTestedLabel := getBoldLabel(numberOfLociTestedString + "/" + totalNumberOfLociString) + + lociTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + setOffspringPolygenicDiseaseNumberOfLociTestedExplainerPage(window, currentPage) + }) + + numberOfLociTestedRow := container.NewHBox(layout.NewSpacer(), numberOfLociTestedTitle, numberOfLociTestedLabel, lociTestedHelpButton, layout.NewSpacer()) + + getOffspringSampleRiskScoresChartImage := func()(image.Image, error){ + + // Map Structure: Risk Score -> Number of offspring with that risk score + offspringRiskScoreCountsMap := make(map[int]int) + + for _, offspringRiskScore := range sampleOffspringRiskScoresList{ + offspringRiskScoreCountsMap[offspringRiskScore] += 1 + } + + //TODO: Move StatisticsDatum to its own package, because we are using it for non-user purposes, and will continue to do so + offspringStatisticsDatumsList := make([]statisticsDatum.StatisticsDatum, 0) + + for riskScore:=0; riskScore <= 10; riskScore += 1{ + + getOffspringCount := func()int{ + + offspringCount, exists := offspringRiskScoreCountsMap[riskScore] + if (exists == false){ + return 0 + } + return offspringCount + } + + offspringCount := getOffspringCount() + + riskScoreString := helpers.ConvertIntToString(riskScore) + offspringCountString := helpers.ConvertIntToString(offspringCount) + + newStatisticsDatum := statisticsDatum.StatisticsDatum{ + + Label: riskScoreString + "/10", + LabelFormatted: riskScoreString + "/10", + Value: float64(offspringCount), + ValueFormatted: offspringCountString, + } + + offspringStatisticsDatumsList = append(offspringStatisticsDatumsList, newStatisticsDatum) + } + + chartTitle := diseaseName + ": 100 Prospective Offspring Risk Scores" + + formatYAxisValuesFunction := func(inputRiskScore float64)(string, error){ + + inputRiskScoreInt, err := helpers.FloorFloat64ToInt(inputRiskScore) + if (err != nil){ return "", err } + + inputRiskScoreString := helpers.ConvertIntToString(inputRiskScoreInt) + + return inputRiskScoreString, nil + } + + offspringsChart, err := createCharts.CreateBarChart(chartTitle, offspringStatisticsDatumsList, formatYAxisValuesFunction, true, " Offspring") + if (err != nil) { return nil, err } + + return offspringsChart, nil + } + + offspringRiskScoresChartImage, err := getOffspringSampleRiskScoresChartImage() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + + viewChartFullscreenButton := getWidgetCentered(widget.NewButtonWithIcon("View Fullscreen", theme.ZoomInIcon(), func(){ + setViewFullpageImagePage(window, offspringRiskScoresChartImage, currentPage) + })) + + pageHeader := container.NewVBox(title, backButton, widget.NewSeparator(), descriptionRow, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), numberOfLociTestedRow, widget.NewSeparator()) + + chartFyneImage := canvas.NewImageFromImage(offspringRiskScoresChartImage) + chartFyneImage.FillMode = canvas.ImageFillContain + + page := container.NewBorder(pageHeader, viewChartFullscreenButton, nil, nil, chartFyneImage) + + setPageContent(page, window) +} + func setViewCoupleGeneticAnalysisTraitsPage(window fyne.Window, person1Name string, person2Name string, person1AnalysisObject geneticAnalysis.PersonAnalysis, person2AnalysisObject geneticAnalysis.PersonAnalysis, coupleAnalysisObject geneticAnalysis.CoupleAnalysis, previousPage func()){ diff --git a/gui/viewAnalysisGui_Person.go b/gui/viewAnalysisGui_Person.go index 9bd70d5..b2b6eec 100644 --- a/gui/viewAnalysisGui_Person.go +++ b/gui/viewAnalysisGui_Person.go @@ -935,7 +935,7 @@ func setViewPersonGeneticAnalysisPolygenicDiseasesPage(window fyne.Window, perso diseaseNameText := getBoldLabelCentered(diseaseName) - personRiskScoreKnown, _, personRiskScoreFormatted, _, conflictExists, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(analysisObject, diseaseName, mainGenomeIdentifier) + personRiskScoreKnown, _, personRiskScoreFormatted, _, _, conflictExists, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(analysisObject, diseaseName, mainGenomeIdentifier) if (err != nil) { return nil, err } getPersonRiskScoreLabelText := func()string{ @@ -1096,7 +1096,7 @@ func setViewPersonGeneticAnalysisPolygenicDiseaseDetailsPage(window fyne.Window, genomeNameCell := getGenomeNameCell() - diseaseRiskScoreKnown, _, diseaseRiskScoreFormatted, numberOfLociTested, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(analysisObject, diseaseName, genomeIdentifier) + diseaseRiskScoreKnown, _, diseaseRiskScoreFormatted, _, numberOfLociTested, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(analysisObject, diseaseName, genomeIdentifier) if (err != nil) { return err } getRiskScoreLabelText := func()string{ @@ -1418,7 +1418,7 @@ func setViewPersonGenomePolygenicDiseaseLociPage(window fyne.Window, geneticAnal return } - locusRiskWeightIsKnown, genomeLocusRiskWeight, _, _, _, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(geneticAnalysisObject, diseaseName, locusIdentifier, genomeIdentifier) + locusRiskWeightIsKnown, genomeLocusRiskWeight, _, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(geneticAnalysisObject, diseaseName, locusIdentifier, genomeIdentifier) if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return @@ -1484,7 +1484,7 @@ func setViewPersonGenomePolygenicDiseaseLociPage(window fyne.Window, geneticAnal locusIdentifier, err := encoding.DecodeHexStringTo3ByteArray(locusIdentifierHex) if (err != nil) { return err } - locusRiskWeightIsKnown, genomeLocusRiskWeight, _, _, locusOddsRatioIsKnown, _, locusOddsRatioFormatted, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(geneticAnalysisObject, diseaseName, locusIdentifier, genomeIdentifier) + locusRiskWeightIsKnown, genomeLocusRiskWeight, locusOddsRatioIsKnown, _, locusOddsRatioFormatted, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(geneticAnalysisObject, diseaseName, locusIdentifier, genomeIdentifier) if (err != nil) { return err } getGenomeLocusRiskWeightText := func()string{ @@ -1635,7 +1635,7 @@ func setViewPersonGeneticAnalysisPolygenicDiseaseLocusDetailsPage(window fyne.Wi addGenomeRow := func(genomeName string, genomeIdentifier [16]byte, isACombinedGenome bool)error{ - genomeRiskWeightKnown, genomeRiskWeight, _, _, genomeOddsRatioKnown, _, genomeOddsRatioFormatted, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(geneticAnalysisObject, diseaseName, locusIdentifier, genomeIdentifier) + genomeRiskWeightKnown, genomeRiskWeight, genomeOddsRatioIsKnown, _, genomeOddsRatioFormatted, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(geneticAnalysisObject, diseaseName, locusIdentifier, genomeIdentifier) if (err != nil) { return err } getGenomeRiskWeightText := func()string{ @@ -1654,7 +1654,7 @@ func setViewPersonGeneticAnalysisPolygenicDiseaseLocusDetailsPage(window fyne.Wi getGenomeOddsRatioText := func()string{ - if (genomeOddsRatioKnown == false){ + if (genomeOddsRatioIsKnown == false){ result := translate("Unknown") return result diff --git a/gui/viewProfileGui.go b/gui/viewProfileGui.go index a17a742..afa32f5 100644 --- a/gui/viewProfileGui.go +++ b/gui/viewProfileGui.go @@ -2567,9 +2567,6 @@ func setViewMateProfilePage_TotalDiseaseRisk(window fyne.Window, getAnyUserProfi numberOfOffspringPolygenicDiseasesTestedRow := container.NewHBox(layout.NewSpacer(), numberOfOffspringPolygenicDiseasesTestedTitle, numberOfOffspringPolygenicDiseasesTestedLabel, layout.NewSpacer()) - - - //TODO: Add help buttons page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), description1, description2, widget.NewSeparator(), offspringProbabilityOfAnyMonogenicDiseaseRow, numberOfOffspringMonogenicDiseasesTestedRow, widget.NewSeparator(), userTotalPolygenicDiseaseRiskScoreRow, numberOfUserPolygenicDiseasesTestedRow, widget.NewSeparator(), offspringTotalPolygenicDiseaseRiskScoreRow, numberOfOffspringPolygenicDiseasesTestedRow) @@ -2876,13 +2873,13 @@ func setViewMateProfilePage_PolygenicDiseases(window fyne.Window, userOrOffsprin getDiseaseInfoGrid := func()(*fyne.Container, error){ - emptyLabelA := widget.NewLabel("") + emptyLabel1 := widget.NewLabel("") diseaseNameLabel := getItalicLabelCentered("Disease Name") - emptyLabelB := widget.NewLabel("") + emptyLabel2 := widget.NewLabel("") userRiskScoreLabel := getItalicLabelCentered("User Risk Score") - emptyLabelC := widget.NewLabel("") + emptyLabel3 := widget.NewLabel("") offspringRiskScoreLabel := getItalicLabelCentered("Offspring Risk Score") userNumberOfLabel := getItalicLabelCentered("User Number Of") @@ -2891,15 +2888,19 @@ func setViewMateProfilePage_PolygenicDiseases(window fyne.Window, userOrOffsprin offspringNumberOfLabel := getItalicLabelCentered("Offspring Number Of") lociTestedLabelB := getItalicLabelCentered("Loci Tested") - emptyLabelD := widget.NewLabel("") - emptyLabelE := widget.NewLabel("") + emptyLabel4 := widget.NewLabel("") + emptyLabel5 := widget.NewLabel("") - diseaseNameColumn := container.NewVBox(emptyLabelA, diseaseNameLabel, widget.NewSeparator()) - userRiskScoreColumn := container.NewVBox(emptyLabelB, userRiskScoreLabel, widget.NewSeparator()) - offspringRiskScoreColumn := container.NewVBox(emptyLabelC, offspringRiskScoreLabel, widget.NewSeparator()) + emptyLabel6 := widget.NewLabel("") + emptyLabel7 := widget.NewLabel("") + + diseaseNameColumn := container.NewVBox(emptyLabel1, diseaseNameLabel, widget.NewSeparator()) + userRiskScoreColumn := container.NewVBox(emptyLabel2, userRiskScoreLabel, widget.NewSeparator()) + offspringRiskScoreColumn := container.NewVBox(emptyLabel3, offspringRiskScoreLabel, widget.NewSeparator()) userNumberOfLociTestedColumn := container.NewVBox(userNumberOfLabel, lociTestedLabelA, widget.NewSeparator()) offspringNumberOfLociTestedColumn := container.NewVBox(offspringNumberOfLabel, lociTestedLabelB, widget.NewSeparator()) - viewDiseaseInfoButtonsColumn := container.NewVBox(emptyLabelD, emptyLabelE, widget.NewSeparator()) + viewSampleOffspringsChartButtonsColumn := container.NewVBox(emptyLabel4, emptyLabel5, widget.NewSeparator()) + viewDiseaseInfoButtonsColumn := container.NewVBox(emptyLabel6, emptyLabel7, widget.NewSeparator()) myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisObject, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() if (err != nil) { return nil, err } @@ -2912,129 +2913,98 @@ func setViewMateProfilePage_PolygenicDiseases(window fyne.Window, userOrOffsprin diseaseName := diseaseObject.DiseaseName diseaseLociList := diseaseObject.LociList - userRiskWeightSum := 0 - userMinimumPossibleRiskWeightSum := 0 - userMaximumPossibleRiskWeightSum := 0 - userNumberOfLociTested := 0 + //Outputs: + // -map[int64]locusValue.LocusValue + // -error + getMyDiseaseLocusValuesMap := func()(map[int64]locusValue.LocusValue, error){ - offspringRiskWeightSum := 0 - offspringMinimumPossibleRiskWeightSum := 0 - offspringMaximumPossibleRiskWeightSum := 0 - offspringNumberOfLociTested := 0 + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + emptyMap := make(map[int64]locusValue.LocusValue) + return emptyMap, nil + } + + anyMyLociValuesExist, _, _, myDiseaseLocusValuesMap, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(myAnalysisObject, diseaseName, myGenomeIdentifier) + if (err != nil) { return nil, err } + if (anyMyLociValuesExist == false){ + emptyMap := make(map[int64]locusValue.LocusValue) + return emptyMap, nil + } + + return myDiseaseLocusValuesMap, nil + } + + myDiseaseLocusValuesMap, err := getMyDiseaseLocusValuesMap() + if (err != nil) { return nil, err } + + // Map Structure: Locus rsID -> Locus Value + userDiseaseLocusValuesMap := make(map[int64]locusValue.LocusValue) for _, locusObject := range diseaseLociList{ - locusIdentifierHex := locusObject.LocusIdentifier - - locusIdentifier, err := encoding.DecodeHexStringTo3ByteArray(locusIdentifierHex) - if (err != nil){ return nil, err } - locusRSID := locusObject.LocusRSID locusRSIDString := helpers.ConvertInt64ToString(locusRSID) - locusRiskWeightsMap := locusObject.RiskWeightsMap - locusOddsRatiosMap := locusObject.OddsRatiosMap - locusMinimumRiskWeight := locusObject.MinimumRiskWeight - locusMaximumRiskWeight := locusObject.MaximumRiskWeight - locusValueAttributeName := "LocusValue_rs" + locusRSIDString userLocusBasePairExists, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(locusValueAttributeName) if (err != nil) { return nil, err } - if (userLocusBasePairExists == true){ - - userNumberOfLociTested += 1 - - userMinimumPossibleRiskWeightSum += locusMinimumRiskWeight - userMaximumPossibleRiskWeightSum += locusMaximumRiskWeight - - userLocusRiskWeight, exists := locusRiskWeightsMap[userLocusBasePair] - if (exists == false){ - // We do not know the risk weight for this base pair - // We treat this as a 0 risk weight - } else { - userRiskWeightSum += userLocusRiskWeight - } + if (userLocusBasePairExists == false){ + continue } - //Outputs: - // -bool: My locus base pair exists - // -string: My locus base 1 - // -string: My locus base 2 - // -error - getMyLocusInfo := func()(bool, string, string, error){ - if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ - return false, "", "", nil - } - - locusInfoKnown, _, locusBase1, locusBase2, _, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(myAnalysisObject, diseaseName, locusIdentifier, myGenomeIdentifier) - if (err != nil){ return false, "", "", err } - if (locusInfoKnown == false){ - return false, "", "", nil - } - return true, locusBase1, locusBase2, nil + userLocusBase1, userLocusBase2, semicolonFound := strings.Cut(userLocusBasePair, ";") + if (semicolonFound == false){ + return nil, errors.New("Database corrupt: Contains profile with invalid " + locusValueAttributeName + " value: " + userLocusBasePair) } - myLocusBasePairExists, myLocusBase1, myLocusBase2, err := getMyLocusInfo() - if (err != nil) { return nil, err } - - if (userLocusBasePairExists == true && myLocusBasePairExists == true){ - - userLocusBase1, userLocusBase2, semicolonFound := strings.Cut(userLocusBasePair, ";") - if (semicolonFound == false){ - return nil, errors.New("Database contains profile containing invalid " + locusValueAttributeName + ": " + userLocusBasePair) - } - - offspringLocusRiskWeight, _, _, _, err := createGeneticAnalysis.GetOffspringPolygenicDiseaseLocusInfo(locusRiskWeightsMap, locusOddsRatiosMap, myLocusBase1, myLocusBase2, userLocusBase1, userLocusBase2) - if (err != nil) { return nil, err } - - offspringNumberOfLociTested += 1 - - offspringMinimumPossibleRiskWeightSum += locusMinimumRiskWeight - offspringMaximumPossibleRiskWeightSum += locusMaximumRiskWeight - - offspringRiskWeightSum += offspringLocusRiskWeight + userLocusValue := locusValue.LocusValue{ + Base1Value: userLocusBase1, + Base2Value: userLocusBase2, + //TODO: Share LocusIsPhased information in user profiles and retrieve it into this value + LocusIsPhased: false, } + + userDiseaseLocusValuesMap[locusRSID] = userLocusValue } + userDiseaseInfoIsKnown, userDiseaseRiskScore, userNumberOfLociTested, _, err := createGeneticAnalysis.GetPersonGenomePolygenicDiseaseInfo(diseaseLociList, userDiseaseLocusValuesMap, true) + if (err != nil) { return nil, err } + getUserDiseaseRiskScoreString := func()(string, error){ - if (userNumberOfLociTested == 0){ + if (userDiseaseInfoIsKnown == false){ result := translate("Unknown") return result, nil - } + } - userRiskScore, err := helpers.ScaleNumberProportionally(true, userRiskWeightSum, userMinimumPossibleRiskWeightSum, userMaximumPossibleRiskWeightSum, 0, 10) - if (err != nil) { return "", err } - - userRiskScoreString := helpers.ConvertIntToString(userRiskScore) + userRiskScoreString := helpers.ConvertIntToString(userDiseaseRiskScore) resultFormatted := userRiskScoreString + "/10" return resultFormatted, nil } - userDiseaseRiskScore, err := getUserDiseaseRiskScoreString() + userDiseaseRiskScoreString, err := getUserDiseaseRiskScoreString() if (err != nil) { return nil, err } - getOffspringDiseaseRiskScoreString := func()(string, error){ + anyOffspringLociTested, offspringDiseaseRiskScore, offspringNumberOfLociTested, _, offspringSampleRiskScoresList, err := createGeneticAnalysis.GetOffspringPolygenicDiseaseInfo(diseaseLociList, myDiseaseLocusValuesMap, userDiseaseLocusValuesMap) + if (err != nil) { return nil, err } - if (offspringNumberOfLociTested == 0){ + getOffspringDiseaseRiskScoreFormatted := func()(string, error){ + + if (anyOffspringLociTested == false){ result := translate("Unknown") return result, nil } - offspringRiskScore, err := helpers.ScaleNumberProportionally(true, offspringRiskWeightSum, offspringMinimumPossibleRiskWeightSum, offspringMaximumPossibleRiskWeightSum, 0, 10) - if (err != nil) { return "", err } - - offspringRiskScoreString := helpers.ConvertIntToString(offspringRiskScore) + offspringRiskScoreString := helpers.ConvertIntToString(offspringDiseaseRiskScore) resultFormatted := offspringRiskScoreString + "/10" return resultFormatted, nil } - offspringDiseaseRiskScore, err := getOffspringDiseaseRiskScoreString() + offspringDiseaseRiskScoreFormatted, err := getOffspringDiseaseRiskScoreFormatted() if (err != nil) { return nil, err } totalNumberOfDiseaseLoci := len(diseaseLociList) @@ -3046,10 +3016,16 @@ func setViewMateProfilePage_PolygenicDiseases(window fyne.Window, userOrOffsprin offspringNumberOfLociTestedFormatted := offspringNumberOfLociTestedString + "/" + totalNumberOfDiseaseLociString diseaseNameText := getBoldLabelCentered(diseaseName) - userRiskScoreLabel := getBoldLabelCentered(userDiseaseRiskScore) - offspringRiskScoreLabel := getBoldLabelCentered(offspringDiseaseRiskScore) + userRiskScoreLabel := getBoldLabelCentered(userDiseaseRiskScoreString) + offspringRiskScoreLabel := getBoldLabelCentered(offspringDiseaseRiskScoreFormatted) userNumberOfLociTestedLabel := getBoldLabelCentered(userNumberOfLociTestedFormatted) offspringNumberOfLociTestedLabel := getBoldLabelCentered(offspringNumberOfLociTestedFormatted) + + viewSampleOffspringsChartButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ + + setViewPolygenicDiseaseSampleOffspringRiskScoresChart(window, diseaseName, offspringSampleRiskScoresList, offspringNumberOfLociTested, currentPage) + }) + viewDiseaseDetailsButton := widget.NewButtonWithIcon("", theme.VisibilityIcon(), func(){ setViewMateProfilePage_PolygenicDiseaseLoci(window, diseaseName, userOrOffspring, getAnyUserProfileAttributeFunction, currentPage) }) @@ -3059,6 +3035,7 @@ func setViewMateProfilePage_PolygenicDiseases(window fyne.Window, userOrOffsprin offspringRiskScoreColumn.Add(offspringRiskScoreLabel) userNumberOfLociTestedColumn.Add(userNumberOfLociTestedLabel) offspringNumberOfLociTestedColumn.Add(offspringNumberOfLociTestedLabel) + viewSampleOffspringsChartButtonsColumn.Add(viewSampleOffspringsChartButton) viewDiseaseInfoButtonsColumn.Add(viewDiseaseDetailsButton) diseaseNameColumn.Add(widget.NewSeparator()) @@ -3066,6 +3043,7 @@ func setViewMateProfilePage_PolygenicDiseases(window fyne.Window, userOrOffsprin offspringRiskScoreColumn.Add(widget.NewSeparator()) userNumberOfLociTestedColumn.Add(widget.NewSeparator()) offspringNumberOfLociTestedColumn.Add(widget.NewSeparator()) + viewSampleOffspringsChartButtonsColumn.Add(widget.NewSeparator()) viewDiseaseInfoButtonsColumn.Add(widget.NewSeparator()) } @@ -3076,18 +3054,18 @@ func setViewMateProfilePage_PolygenicDiseases(window fyne.Window, userOrOffsprin offspringRiskScoreHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ setOffspringPolygenicDiseaseRiskScoreExplainerPage(window, currentPage) }) - userNumberOfLociTestedButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + userNumberOfLociTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ setPolygenicDiseaseNumberOfLociTestedExplainerPage(window, currentPage) }) - offspringNumberOfLociTestedButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ + offspringNumberOfLociTestedHelpButton := widget.NewButtonWithIcon("", theme.QuestionIcon(), func(){ setOffspringPolygenicDiseaseNumberOfLociTestedExplainerPage(window, currentPage) }) userRiskScoreColumn.Add(userRiskScoreHelpButton) offspringRiskScoreColumn.Add(offspringRiskScoreHelpButton) - userNumberOfLociTestedColumn.Add(userNumberOfLociTestedButton) - offspringNumberOfLociTestedColumn.Add(offspringNumberOfLociTestedButton) + userNumberOfLociTestedColumn.Add(userNumberOfLociTestedHelpButton) + offspringNumberOfLociTestedColumn.Add(offspringNumberOfLociTestedHelpButton) if (userOrOffspring == "User"){ diseaseInfoGrid := container.NewHBox(layout.NewSpacer(), diseaseNameColumn, userRiskScoreColumn, userNumberOfLociTestedColumn, viewDiseaseInfoButtonsColumn, layout.NewSpacer()) @@ -3095,7 +3073,7 @@ func setViewMateProfilePage_PolygenicDiseases(window fyne.Window, userOrOffsprin return diseaseInfoGrid, nil } - diseaseInfoGrid := container.NewHBox(layout.NewSpacer(), diseaseNameColumn, offspringRiskScoreColumn, offspringNumberOfLociTestedColumn, viewDiseaseInfoButtonsColumn, layout.NewSpacer()) + diseaseInfoGrid := container.NewHBox(layout.NewSpacer(), diseaseNameColumn, offspringRiskScoreColumn, offspringNumberOfLociTestedColumn, viewSampleOffspringsChartButtonsColumn, viewDiseaseInfoButtonsColumn, layout.NewSpacer()) return diseaseInfoGrid, nil } @@ -3148,114 +3126,107 @@ func setViewMateProfilePage_PolygenicDiseaseLoci(window fyne.Window, diseaseName numberOfDiseaseLoci := len(diseaseLocusObjectsList) - myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisObject, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + // Outputs: + // -map[int64]locusValue.LocusValue: Map Structure: Locus rsID -> Locus Value + // -error + getMyDiseaseLocusValuesMap := func()(map[int64]locusValue.LocusValue, error){ + + myPersonChosen, myGenomesExist, myAnalysisIsReady, myAnalysisObject, myGenomeIdentifier, _, err := myChosenAnalysis.GetMyChosenMateGeneticAnalysis() + if (err != nil) { return nil, err } + + if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ + + emptyMap := make(map[int64]locusValue.LocusValue) + return emptyMap, nil + } + + anyLocusValuesExist, _, _, myLocusValuesMap, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(myAnalysisObject, diseaseName, myGenomeIdentifier) + if (err != nil) { return nil, err } + if (anyLocusValuesExist == false){ + emptyMap := make(map[int64]locusValue.LocusValue) + return emptyMap, nil + } + + return myLocusValuesMap, nil + } + + myDiseaseLocusValuesMap, err := getMyDiseaseLocusValuesMap() if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } - //Outputs: - // -bool: User locus Info is known - // -int: User locus Risk weight - // -string: Locus base pair - // -bool: User locus odds ratio known - // -string: User locus odds ratio formatted - // -error - getUserLocusInfo := func(locusRSID int64, locusRiskWeightsMap map[string]int, locusOddsRatiosMap map[string]float64)(bool, int, string, bool, string, error){ + getUserLocusValuesMap := func()(map[int64]locusValue.LocusValue, error){ - locusRSIDString := helpers.ConvertInt64ToString(locusRSID) - - locusValueAttributeName := "LocusValue_rs" + locusRSIDString - - userLocusBasePairExists, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(locusValueAttributeName) - if (err != nil) { return false, 0, "", false, "", err } - if (userLocusBasePairExists == false){ - return false, 0, "", false, "", nil - } - - userLocusRiskWeight, exists := locusRiskWeightsMap[userLocusBasePair] - if (exists == false){ - // We do not know the risk weight for this base pair - // We treat this as a 0 risk weight - return true, 0, userLocusBasePair, false, "", nil - } - - locusOddsRatio, exists := locusOddsRatiosMap[userLocusBasePair] - if (exists == false){ - return true, userLocusRiskWeight, userLocusBasePair, false, "", nil - } - - locusOddsRatioString := helpers.ConvertFloat64ToStringRounded(locusOddsRatio, 2) - - locusOddsRatioFormatted := locusOddsRatioString + "x" - - return true, userLocusRiskWeight, userLocusBasePair, true, locusOddsRatioFormatted, nil - } - - //Outputs: - // -bool: My locus Info is known - // -string: My locus base 1 - // -string: My locus base 2 - // -error - getMyLocusInfo := func(locusIdentifierHex string)(bool, string, string, error){ - if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ - return false, "", "", nil - } - - locusIdentifier, err := encoding.DecodeHexStringTo3ByteArray(locusIdentifierHex) - if (err != nil) { return false, "", "", err } - - locusInfoKnown, _, locusBase1, locusBase2, _, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(myAnalysisObject, diseaseName, locusIdentifier, myGenomeIdentifier) - if (err != nil){ return false, "", "", err } - if (locusInfoKnown == false){ - return false, "", "", nil - } - return true, locusBase1, locusBase2, nil - } - - getNumberOfLociTested := func()(int, error){ - - if (userOrOffspring == "Offspring"){ - if (myPersonChosen == false || myGenomesExist == false || myAnalysisIsReady == false){ - return 0, nil - } - } - - numberOfLociTested := 0 + // Map Structure: Locus rsID -> Locus Value + userDiseaseLocusValuesMap := make(map[int64]locusValue.LocusValue) for _, locusObject := range diseaseLocusObjectsList{ - locusIdentifier := locusObject.LocusIdentifier locusRSID := locusObject.LocusRSID - locusRiskWeightsMap := locusObject.RiskWeightsMap - locusOddsRatiosMap := locusObject.OddsRatiosMap - userLocusInfoIsKnown, _, _, _, _, err := getUserLocusInfo(locusRSID, locusRiskWeightsMap, locusOddsRatiosMap) - if (err != nil) { return 0, err } - if (userLocusInfoIsKnown == false){ + locusRSIDString := helpers.ConvertInt64ToString(locusRSID) + + locusValueAttributeName := "LocusValue_rs" + locusRSIDString + + userLocusBasePairExists, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(locusValueAttributeName) + if (err != nil) { return nil, err } + if (userLocusBasePairExists == false){ continue } - if (userOrOffspring == "Offspring") { - - myLocusInfoKnown, _, _, err := getMyLocusInfo(locusIdentifier) - if (err != nil) { return 0, err } - if (myLocusInfoKnown == false){ - continue - } + userLocusBase1, userLocusBase2, semicolonFound := strings.Cut(userLocusBasePair, ";") + if (semicolonFound == false){ + return nil, errors.New("Database corrupt: Contains profile with invalid " + locusValueAttributeName + " value: " + userLocusBasePair) } - numberOfLociTested += 1 + userLocusValue := locusValue.LocusValue{ + Base1Value: userLocusBase1, + Base2Value: userLocusBase2, + //TODO: Share LocusIsPhased information in user profiles and retrieve it into this value + LocusIsPhased: false, + } + + userDiseaseLocusValuesMap[locusRSID] = userLocusValue } - return numberOfLociTested, nil + return userDiseaseLocusValuesMap, nil } - numberOfLociTested, err := getNumberOfLociTested() - if (err != nil){ + userDiseaseLocusValuesMap, err := getUserLocusValuesMap() + if (err != nil) { setErrorEncounteredPage(window, err, previousPage) return } + + anyUserLociTested, _, userNumberOfLociTested, userDiseaseLocusInfoMap, err := createGeneticAnalysis.GetPersonGenomePolygenicDiseaseInfo(diseaseLocusObjectsList, userDiseaseLocusValuesMap, true) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + anyOffspringLociTested, _, offspringNumberOfLociTested, offspringLociInfoMap, _, err := createGeneticAnalysis.GetOffspringPolygenicDiseaseInfo(diseaseLocusObjectsList, myDiseaseLocusValuesMap, userDiseaseLocusValuesMap) + if (err != nil) { + setErrorEncounteredPage(window, err, previousPage) + return + } + + getNumberOfLociTested := func()int{ + + if (userOrOffspring == "Offspring"){ + + if (anyOffspringLociTested == false){ + return 0 + } + + return offspringNumberOfLociTested + } + + return userNumberOfLociTested + } + + numberOfLociTested := getNumberOfLociTested() + numberOfLociTestedString := helpers.ConvertIntToString(numberOfLociTested) totalNumberOfDiseaseLociString := helpers.ConvertIntToString(numberOfDiseaseLoci) lociTestedString := numberOfLociTestedString + "/" + totalNumberOfDiseaseLociString @@ -3299,19 +3270,50 @@ func setViewMateProfilePage_PolygenicDiseaseLoci(window fyne.Window, diseaseName for _, locusObject := range diseaseLocusObjectsList{ - locusIdentifier := locusObject.LocusIdentifier + locusIdentifierHex := locusObject.LocusIdentifier + + locusIdentifier, err := encoding.DecodeHexStringTo3ByteArray(locusIdentifierHex) + if (err != nil) { return nil, err } locusRSID := locusObject.LocusRSID locusRSIDString := helpers.ConvertInt64ToString(locusRSID) locusName := "rs" + locusRSIDString - locusRiskWeightsMap := locusObject.RiskWeightsMap - locusOddsRatiosMap := locusObject.OddsRatiosMap + //Outputs: + // -bool: User locus Info is known + // -int: User locus Risk weight + // -bool: User locus odds ratio known + // -string: User locus odds ratio formatted + // -error + getUserLocusInfo := func()(bool, int, bool, string, error){ - userLocusInfoIsKnown, userLocusRiskWeight, userLocusBasePair, userLocusOddsRatioKnown, userLocusOddsRatioFormatted, err := getUserLocusInfo(locusRSID, locusRiskWeightsMap, locusOddsRatiosMap) - if (err != nil) { return nil, err } + if (anyUserLociTested == false){ + return false, 0, false, "", nil + } - myLocusInfoIsKnown, myLocusBase1, myLocusBase2, err := getMyLocusInfo(locusIdentifier) + locusInfoObject, exists := userDiseaseLocusInfoMap[locusIdentifier] + if (exists == false){ + return false, 0, false, "", nil + } + + userLocusRiskWeight := locusInfoObject.RiskWeight + + oddsRatioIsKnown := locusInfoObject.OddsRatioIsKnown + + if (oddsRatioIsKnown == false){ + return true, userLocusRiskWeight, false, "", nil + } + + userLocusOddsRatio := locusInfoObject.OddsRatio + + locusOddsRatioString := helpers.ConvertFloat64ToStringRounded(userLocusOddsRatio, 2) + + locusOddsRatioFormatted := locusOddsRatioString + "x" + + return true, userLocusRiskWeight, true, locusOddsRatioFormatted, nil + } + + userLocusInfoIsKnown, userLocusRiskWeight, userLocusOddsRatioIsKnown, userLocusOddsRatioFormatted, err := getUserLocusInfo() if (err != nil) { return nil, err } getUserRiskWeightString := func()string{ @@ -3328,7 +3330,7 @@ func setViewMateProfilePage_PolygenicDiseaseLoci(window fyne.Window, diseaseName userRiskWeightString := getUserRiskWeightString() getUserOddsRatioString := func()string{ - if (userLocusOddsRatioKnown == false){ + if (userLocusOddsRatioIsKnown == false){ result := translate("Unknown") return result } @@ -3345,22 +3347,25 @@ func setViewMateProfilePage_PolygenicDiseaseLoci(window fyne.Window, diseaseName // -error getOffspringDiseaseLocusInfo := func()(bool, int, bool, string, error){ - if (userLocusInfoIsKnown == false || myLocusInfoIsKnown == false){ + if (anyOffspringLociTested == false){ return false, 0, false, "", nil } - userLocusBase1, userLocusBase2, semicolonExists := strings.Cut(userLocusBasePair, ";") - if (semicolonExists == false){ - return false, 0, false, "", errors.New("Database corrupt: Contains profile with invalid " + locusName + " value: " + userLocusBasePair) + offspringLocusInfoObject, exists := offspringLociInfoMap[locusIdentifier] + if (exists == false){ + return false, 0, false, "", nil } - offspringLocusRiskWeight, offspringOddsRatioIsKnown, offspringOddsRatio, unknownOddsRatiosWeightSum, err := createGeneticAnalysis.GetOffspringPolygenicDiseaseLocusInfo(locusRiskWeightsMap, locusOddsRatiosMap, myLocusBase1, myLocusBase2, userLocusBase1, userLocusBase2) - if (err != nil) { return false, 0, false, "", err } + offspringLocusRiskWeight := offspringLocusInfoObject.OffspringAverageRiskWeight + offspringOddsRatioIsKnown := offspringLocusInfoObject.OffspringOddsRatioIsKnown if (offspringOddsRatioIsKnown == false){ return true, offspringLocusRiskWeight, false, "", nil } + offspringOddsRatio := offspringLocusInfoObject.OffspringAverageOddsRatio + unknownOddsRatiosWeightSum := offspringLocusInfoObject.OffspringAverageUnknownOddsRatiosWeightSum + getOddsRatioFormatted := func()string{ offspringOddsRatioString := helpers.ConvertFloat64ToStringRounded(offspringOddsRatio, 2) @@ -3414,7 +3419,7 @@ func setViewMateProfilePage_PolygenicDiseaseLoci(window fyne.Window, diseaseName userOddsRatioLabel := getBoldLabelCentered(userOddsRatioString) offspringOddsRatioLabel := getBoldLabelCentered(offspringOddsRatioString) locusInfoButton := widget.NewButtonWithIcon("", theme.InfoIcon(), func(){ - setViewPolygenicDiseaseLocusDetailsPage(window, diseaseName, locusIdentifier, currentPage) + setViewPolygenicDiseaseLocusDetailsPage(window, diseaseName, locusIdentifierHex, currentPage) }) locusNameColumn.Add(locusNameLabel) @@ -3460,7 +3465,7 @@ func setViewMateProfilePage_PolygenicDiseaseLoci(window fyne.Window, diseaseName return } - page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), diseaseNameRow, lociTestedRow, widget.NewSeparator(), diseaseLociGrid) + page := container.NewVBox(title, backButton, widget.NewSeparator(), subtitle, widget.NewSeparator(), diseaseNameRow, widget.NewSeparator(), lociTestedRow, widget.NewSeparator(), diseaseLociGrid) setPageContent(page, window) } @@ -3554,6 +3559,39 @@ func setViewMateProfilePage_GeneticTraits(window fyne.Window, userOrOffspring st viewTraitDetailsButtonsColumn.Add(viewTraitDetailsButton) + // We construct the user's trait locus values map + // Map Structure: Locus rsID -> locusValue.LocusValue + userTraitLocusValuesMap := make(map[int64]locusValue.LocusValue) + + traitLociList := traitObject.LociList + + for _, rsID := range traitLociList{ + + rsIDString := helpers.ConvertInt64ToString(rsID) + + userLocusValueAttributeName := "LocusValue_rs" + rsIDString + + userLocusBasePairIsKnown, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(userLocusValueAttributeName) + if (err != nil) { return nil, err } + if (userLocusBasePairIsKnown == false){ + continue + } + + userLocusBase1, userLocusBase2, semicolonFound := strings.Cut(userLocusBasePair, ";") + if (semicolonFound == false){ + return nil, errors.New("Database corrupt: Contains profile with invalid " + userLocusValueAttributeName + " value: " + userLocusBasePair) + } + + userLocusValue := locusValue.LocusValue{ + Base1Value: userLocusBase1, + Base2Value: userLocusBase2, + //TODO: Share LocusIsPhased information in user profiles and retrieve it into this value + LocusIsPhased: false, + } + + userTraitLocusValuesMap[rsID] = userLocusValue + } + //Outputs: // -bool: At least 1 rule is known // -map[string]int: Outcome name -> Outcome score @@ -3566,49 +3604,9 @@ func setViewMateProfilePage_GeneticTraits(window fyne.Window, userOrOffspring st for _, traitRuleObject := range traitRulesList{ - // Outputs: - // -bool: Status is known - // -bool: User passes rule - // -error - getUserPassesRuleStatus := func()(bool, bool, error){ + ruleLociList := traitRuleObject.LociList - ruleLociList := traitRuleObject.LociList - allRuleLociKnown := true - - for _, ruleLocusObject := range ruleLociList{ - - locusRSID := ruleLocusObject.LocusRSID - - locusRSIDString := helpers.ConvertInt64ToString(locusRSID) - - userLocusValueAttributeName := "LocusValue_rs" + locusRSIDString - - userLocusBasePairIsKnown, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(userLocusValueAttributeName) - if (err != nil) { return false, false, err } - if (userLocusBasePairIsKnown == false){ - // We know rule is not passed - // We keep searching to see if ruleIsPassed status is No or Unknown - allRuleLociKnown = false - continue - } - - ruleLocusBasePairsList := ruleLocusObject.BasePairsList - - userPassesRuleLocus := slices.Contains(ruleLocusBasePairsList, userLocusBasePair) - if (userPassesRuleLocus == false){ - // We know the rule is not passed - return true, false, nil - } - } - if (allRuleLociKnown == false){ - // Rule status is unknown. Any loci which we knew must have passed. - return false, false, nil - } - // Rule is passed - return true, true, nil - } - - userRuleStatusIsKnown, userPassesRule, err := getUserPassesRuleStatus() + userRuleStatusIsKnown, userPassesRule, err := createGeneticAnalysis.GetGenomePassesTraitRuleStatus(ruleLociList, userTraitLocusValuesMap, true) if (err != nil) { return false, nil, 0, err } if (userRuleStatusIsKnown == false){ continue @@ -3660,39 +3658,6 @@ func setViewMateProfilePage_GeneticTraits(window fyne.Window, userOrOffspring st myTraitLocusValuesMap, _, _, _, _, err := readGeneticAnalysis.GetPersonTraitInfoFromGeneticAnalysis(myAnalysisObject, traitName, myGenomeIdentifier) if (err != nil) { return false, nil, 0, err } - // We construct the user's trait locus values map - // Map Structure: Locus rsID -> locusValue.LocusValue - userTraitLocusValuesMap := make(map[int64]locusValue.LocusValue) - - traitLociList := traitObject.LociList - - for _, rsID := range traitLociList{ - - rsIDString := helpers.ConvertInt64ToString(rsID) - - userLocusValueAttributeName := "LocusValue_rs" + rsIDString - - userLocusBasePairIsKnown, _, userLocusBasePair, err := getAnyUserProfileAttributeFunction(userLocusValueAttributeName) - if (err != nil) { return false, nil, 0, err } - if (userLocusBasePairIsKnown == false){ - continue - } - - userLocusBase1, userLocusBase2, semicolonFound := strings.Cut(userLocusBasePair, ";") - if (semicolonFound == false){ - return false, nil, 0, errors.New("Database corrupt: Contains profile with invalid " + userLocusValueAttributeName + " value: " + userLocusBasePair) - } - - userLocusValue := locusValue.LocusValue{ - Base1Value: userLocusBase1, - Base2Value: userLocusBase2, - //TODO: Share LocusIsPhased information in user profiles and retrieve it into this value - LocusIsPhased: false, - } - - userTraitLocusValuesMap[rsID] = userLocusValue - } - anyRuleTested, offspringNumberOfRulesTested, _, offspringAverageOutcomeScoresMap, err := createGeneticAnalysis.GetOffspringTraitInfo(traitObject, myTraitLocusValuesMap, userTraitLocusValuesMap) if (err != nil) { return false, nil, 0, err } if (anyRuleTested == false){ @@ -3944,50 +3909,6 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use return } - //Outputs: - // -bool: Status is known - // -bool: User passes rule - // -error - getUserPassesRuleBool := func(ruleLociList []traits.RuleLocus)(bool, bool, error){ - - if (anyUserTraitLocusValueExists == false){ - return false, false, nil - } - - allRuleLociKnown := true - - for _, ruleLocusObject := range ruleLociList{ - - locusRSID := ruleLocusObject.LocusRSID - - userLocusValue, userLocusValueIsKnown := userTraitLocusValuesMap[locusRSID] - if (userLocusValueIsKnown == false){ - // We know rule is not passed - // We keep searching to see if ruleIsPassed status is No or Unknown - allRuleLociKnown = false - continue - } - - userLocusBase1Value := userLocusValue.Base1Value - userLocusBase2Value := userLocusValue.Base2Value - - userLocusBasePair := userLocusBase1Value + ";" + userLocusBase2Value - - ruleLocusBasePairsList := ruleLocusObject.BasePairsList - - userPassesRuleLocus := slices.Contains(ruleLocusBasePairsList, userLocusBasePair) - if (userPassesRuleLocus == false){ - // We know the rule is not passed - return true, false, nil - } - } - if (allRuleLociKnown == false){ - // We don't know if the user passes the rule. Any loci which we knew must have passed. - return false, false, nil - } - return true, true, nil - } - //Outputs: // -bool: Any offspring probability of passing rule is known // -map[[3]byte]int: Offspring probability of passing rules map @@ -4025,6 +3946,10 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use return numberOfRulesTested, nil } + + if (anyUserTraitLocusValueExists == false){ + return 0, nil + } numberOfRulesTested := 0 @@ -4032,7 +3957,7 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use ruleLociList := ruleObject.LociList - ruleStatusIsKnown, _, err := getUserPassesRuleBool(ruleLociList) + ruleStatusIsKnown, _, err := createGeneticAnalysis.GetGenomePassesTraitRuleStatus(ruleLociList, userTraitLocusValuesMap, true) if (err != nil) { return 0, err } if (ruleStatusIsKnown == true){ numberOfRulesTested += 1 @@ -4125,7 +4050,7 @@ func setViewMateProfilePage_TraitRules(window fyne.Window, traitName string, use getUserPassesRuleString := func()(string, error){ - userRuleStatusIsKnown, userPassesRule, err := getUserPassesRuleBool(ruleLociList) + userRuleStatusIsKnown, userPassesRule, err := createGeneticAnalysis.GetGenomePassesTraitRuleStatus(ruleLociList, userTraitLocusValuesMap, true) if (err != nil) { return "", err } if (userRuleStatusIsKnown == false){ @@ -4300,7 +4225,7 @@ func setViewMateProfilePage_Diet(window fyne.Window, getAnyUserProfileAttributeF getRatingLabel := func()*fyne.Container{ if (ratingExists == false){ - result := getBoldItalicLabelCentered(translate("Unknown")) + result := getBoldItalicLabelCentered(translate("No Response")) return result } diff --git a/internal/createCharts/createCharts.go b/internal/createCharts/createCharts.go index 2af6450..8044202 100644 --- a/internal/createCharts/createCharts.go +++ b/internal/createCharts/createCharts.go @@ -5,7 +5,7 @@ package createCharts -import "seekia/internal/profiles/userStatistics" +import "seekia/internal/statisticsDatum" import goChart "github.com/wcharczuk/go-chart/v2" import "github.com/wcharczuk/go-chart/v2/drawing" @@ -15,7 +15,7 @@ import "errors" // Inputs: // -string: Chart title -// -[]userStatistics.StatisticsItem: Statistics items list +// -[]statisticsDatum.StatisticsDatum: Statistics datums list // -func(float64)(string, error): formatYAxisValuesFunction // -This will take values such as 1000000 and turn them to "1 million" // -bool: Y-axis units exist @@ -23,38 +23,38 @@ import "errors" // Outputs: // -image.Image // -error -func CreateBarChart(chartTitle string, chartStatisticsItemsList []userStatistics.StatisticsItem, formatYAxisValuesFunction func(float64)(string, error), yAxisUnitsProvided bool, yAxisUnits string)(image.Image, error){ +func CreateBarChart(chartTitle string, chartStatisticsDatumsList []statisticsDatum.StatisticsDatum, formatYAxisValuesFunction func(float64)(string, error), yAxisUnitsProvided bool, yAxisUnits string)(image.Image, error){ - if (len(chartStatisticsItemsList) == 0) { - return nil, errors.New("CreateBarChart called with empty chartStatisticsItemsList") + if (len(chartStatisticsDatumsList) == 0) { + return nil, errors.New("CreateBarChart called with empty chartStatisticsDatumsList") } - chartItemsList := make([]goChart.Value, 0, len(chartStatisticsItemsList)) + chartDatumsList := make([]goChart.Value, 0, len(chartStatisticsDatumsList)) - for _, statisticsItem := range chartStatisticsItemsList{ + for _, statisticsDatum := range chartStatisticsDatumsList{ - itemLabel := statisticsItem.LabelFormatted + datumLabel := statisticsDatum.LabelFormatted - itemValue := statisticsItem.Value + datumValue := statisticsDatum.Value // We make sure this function does not error - _, err := formatYAxisValuesFunction(itemValue) + _, err := formatYAxisValuesFunction(datumValue) if (err != nil){ - return nil, errors.New("Invalid chartStatisticsItemsList: Item value is invalid. Reason: " + err.Error()) + return nil, errors.New("Invalid chartStatisticsDatumsList: Datum value is invalid. Reason: " + err.Error()) } newChartValue := goChart.Value{ - Label: itemLabel, - Value: itemValue, + Label: datumLabel, + Value: datumValue, } - chartItemsList = append(chartItemsList, newChartValue) + chartDatumsList = append(chartDatumsList, newChartValue) } - if (len(chartItemsList) == 1){ + if (len(chartDatumsList) == 1){ - // This package cannot create bar charts with only 1 item - // Thus, we must add an empty item + // This package cannot create bar charts with only 1 datum + // Thus, we must add an empty datum newChartValue := goChart.Value{ Style: goChart.Style{ @@ -65,7 +65,7 @@ func CreateBarChart(chartTitle string, chartStatisticsItemsList []userStatistics Value: .001, } - chartItemsList = append(chartItemsList, newChartValue) + chartDatumsList = append(chartDatumsList, newChartValue) } chartStyleObject := goChart.Style{ @@ -110,7 +110,7 @@ func CreateBarChart(chartTitle string, chartStatisticsItemsList []userStatistics Background: chartStyleObject, Height: 500, BarWidth: 60, - Bars: chartItemsList, + Bars: chartDatumsList, YAxis: yAxisObject, } @@ -123,28 +123,28 @@ func CreateBarChart(chartTitle string, chartStatisticsItemsList []userStatistics return goImage, nil } -func CreateDonutChart(chartTitle string, chartStatisticsItemsList []userStatistics.StatisticsItem)(image.Image, error){ +func CreateDonutChart(chartTitle string, chartStatisticsDatumsList []statisticsDatum.StatisticsDatum)(image.Image, error){ - if (len(chartStatisticsItemsList) == 0) { + if (len(chartStatisticsDatumsList) == 0) { - return nil, errors.New("CreateDonutChart called with empty chartStatisticsItemsList") + return nil, errors.New("CreateDonutChart called with empty chartStatisticsDatumsList") } - chartItemsList := make([]goChart.Value, 0, len(chartStatisticsItemsList)) + chartDatumsList := make([]goChart.Value, 0, len(chartStatisticsDatumsList)) - for _, statisticsItem := range chartStatisticsItemsList{ + for _, statisticsDatum := range chartStatisticsDatumsList{ - itemLabel := statisticsItem.LabelFormatted + datumLabel := statisticsDatum.LabelFormatted // Value is always a number representing the percentage of the donut - itemValue := statisticsItem.Value + datumValue := statisticsDatum.Value newChartValue := goChart.Value{ - Label: itemLabel, - Value: itemValue, + Label: datumLabel, + Value: datumValue, } - chartItemsList = append(chartItemsList, newChartValue) + chartDatumsList = append(chartDatumsList, newChartValue) } chartStyleObject := goChart.Style{ @@ -166,10 +166,10 @@ func CreateDonutChart(chartTitle string, chartStatisticsItemsList []userStatisti TitleStyle: titleStyleObject, Background: chartStyleObject, Height: 500, - Values: chartItemsList, + Values: chartDatumsList, } - if (len(chartItemsList) == 1){ + if (len(chartDatumsList) == 1){ // Default is transparent, we need to add color sliceStyleObject := goChart.Style{ diff --git a/internal/genetics/createGeneticAnalysis/createGeneticAnalysis.go b/internal/genetics/createGeneticAnalysis/createGeneticAnalysis.go index da95902..b99104a 100644 --- a/internal/genetics/createGeneticAnalysis/createGeneticAnalysis.go +++ b/internal/genetics/createGeneticAnalysis/createGeneticAnalysis.go @@ -375,7 +375,6 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom return false, 0, nil } - base1HasVariant := personVariantInfoObject.Base1HasVariant base2HasVariant := personVariantInfoObject.Base2HasVariant @@ -623,109 +622,53 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom // It then adds the pair entry to the offspringPolygenicDiseaseInfoMap addGenomePairDiseaseInfoToDiseaseMap := func(person1GenomeIdentifier [16]byte, person2GenomeIdentifier [16]byte)error{ - summedRiskWeights := 0 - minimumPossibleRiskWeightSum := 0 - maximumPossibleRiskWeightSum := 0 + //Outputs: + // -bool: Any locus values exist + // -map[int64]locusValue.LocusValue + // -error + getPersonGenomeDiseaseLocusValuesMap := func(personGenomeIdentifier [16]byte, personDiseaseAnalysisObject geneticAnalysis.PersonPolygenicDiseaseInfo)(bool, map[int64]locusValue.LocusValue, error){ - // Map Structure: Locus Identifier -> OffspringPolygenicDiseaseLocusInfo - offspringDiseaseLociInfoMap := make(map[[3]byte]geneticAnalysis.OffspringPolygenicDiseaseLocusInfo) + personPolygenicDiseaseInfoMap := personDiseaseAnalysisObject.PolygenicDiseaseInfoMap - for _, locusObject := range diseaseLociList{ - - locusIdentifierHex := locusObject.LocusIdentifier - - locusIdentifier, err := encoding.DecodeHexStringTo3ByteArray(locusIdentifierHex) - if (err != nil) { return err } - - locusRiskWeightsMap := locusObject.RiskWeightsMap - locusOddsRatiosMap := locusObject.OddsRatiosMap - - //Outputs: - // -bool: Locus value exists - // -string: Base1 Value - // -string: Base2 Value - // -error - getPersonGenomeLocusValues := func(personGenomeIdentifier [16]byte, personDiseaseAnalysisObject geneticAnalysis.PersonPolygenicDiseaseInfo)(bool, string, string, error){ - - personPolygenicDiseaseInfoMap := personDiseaseAnalysisObject.PolygenicDiseaseInfoMap - - personGenomeDiseaseInfoObject, exists := personPolygenicDiseaseInfoMap[personGenomeIdentifier] - if (exists == false){ - // This person's genome has no information about loci related to this disease - return false, "", "", nil - } - - personGenomeLociMap := personGenomeDiseaseInfoObject.LociInfoMap - - personGenomeLocusInfoObject, exists := personGenomeLociMap[locusIdentifier] - if (exists == false){ - // This person's genome doesn't have information about this locus - return false, "", "", nil - } - - locusBase1 := personGenomeLocusInfoObject.LocusBase1 - locusBase2 := personGenomeLocusInfoObject.LocusBase2 - - return true, locusBase1, locusBase2, nil + personGenomeDiseaseInfoObject, exists := personPolygenicDiseaseInfoMap[personGenomeIdentifier] + if (exists == false){ + // This person's genome has no information about loci related to this disease + return false, nil, nil } - person1LocusBasePairKnown, person1LocusBase1, person1LocusBase2, err := getPersonGenomeLocusValues(person1GenomeIdentifier, person1DiseaseAnalysisObject) - if (err != nil) { return err } - if (person1LocusBasePairKnown == false){ - // Offspring's disease info for this locus on this genome pair is unknown - continue - } + personGenomeLocusValuesMap := personGenomeDiseaseInfoObject.LocusValuesMap - person2LocusBasePairKnown, person2LocusBase1, person2LocusBase2, err := getPersonGenomeLocusValues(person2GenomeIdentifier, person2DiseaseAnalysisObject) - if (err != nil) { return err } - if (person2LocusBasePairKnown == false){ - // Offspring's disease info for this locus on this genome pair is unknown - continue - } - - offspringAverageRiskWeight, offspringOddsRatioIsKnown, offspringAverageOddsRatio, averageUnknownOddsRatiosWeightSum, err := GetOffspringPolygenicDiseaseLocusInfo(locusRiskWeightsMap, locusOddsRatiosMap, person1LocusBase1, person1LocusBase2, person2LocusBase1, person2LocusBase2) - if (err != nil) { return err } - - newOffspringPolygenicDiseaseLocusInfo := geneticAnalysis.OffspringPolygenicDiseaseLocusInfo{ - OffspringRiskWeight: offspringAverageRiskWeight, - OffspringUnknownOddsRatiosWeightSum: averageUnknownOddsRatiosWeightSum, - } - - if (offspringOddsRatioIsKnown == true){ - - newOffspringPolygenicDiseaseLocusInfo.OffspringOddsRatioIsKnown = true - newOffspringPolygenicDiseaseLocusInfo.OffspringOddsRatio = offspringAverageOddsRatio - } - - offspringDiseaseLociInfoMap[locusIdentifier] = newOffspringPolygenicDiseaseLocusInfo - - // Now we add risk weight info for this locus - - locusMinimumRiskWeight := locusObject.MinimumRiskWeight - locusMaximumRiskWeight := locusObject.MaximumRiskWeight - - minimumPossibleRiskWeightSum += locusMinimumRiskWeight - maximumPossibleRiskWeightSum += locusMaximumRiskWeight - - summedRiskWeights += offspringAverageRiskWeight + return true, personGenomeLocusValuesMap, nil } - numberOfLociTested := len(offspringDiseaseLociInfoMap) + anyPerson1LociValuesExist, person1LocusValuesMap, err := getPersonGenomeDiseaseLocusValuesMap(person1GenomeIdentifier, person1DiseaseAnalysisObject) + if (err != nil) { return err } + if (anyPerson1LociValuesExist == false){ + // Offspring's disease info for this locus on this genome pair is unknown + return nil + } - if (numberOfLociTested == 0){ + anyPerson2LociValuesExist, person2LocusValuesMap, err := getPersonGenomeDiseaseLocusValuesMap(person2GenomeIdentifier, person2DiseaseAnalysisObject) + if (err != nil) { return err } + if (anyPerson2LociValuesExist == false){ + // Offspring's disease info for this locus on this genome pair is unknown + return nil + } + + anyOffspringLocusTested, genomePairOffspringAverageRiskScore, numberOfLociTested, genomePairOffspringDiseaseLociInfoMap, genomePairSampleOffspringRiskScoresList, err := GetOffspringPolygenicDiseaseInfo(diseaseLociList, person1LocusValuesMap, person2LocusValuesMap) + if (err != nil) { return err } + if (anyOffspringLocusTested == false){ // We have no information about this genome pair's disease risk // We don't add this genome pair's disease info to the diseaseInfoMap return nil } - genomePairDiseaseRiskScore, err := helpers.ScaleNumberProportionally(true, summedRiskWeights, minimumPossibleRiskWeightSum, maximumPossibleRiskWeightSum, 0, 10) - if (err != nil) { return err } - newOffspringGenomePairPolygenicDiseaseInfo := geneticAnalysis.OffspringGenomePairPolygenicDiseaseInfo{ NumberOfLociTested: numberOfLociTested, - OffspringRiskScore: genomePairDiseaseRiskScore, - LociInfoMap: offspringDiseaseLociInfoMap, + OffspringAverageRiskScore: genomePairOffspringAverageRiskScore, + LociInfoMap: genomePairOffspringDiseaseLociInfoMap, + SampleOffspringRiskScoresList: genomePairSampleOffspringRiskScoresList, } genomePairIdentifier := helpers.JoinTwo16ByteArrays(person1GenomeIdentifier, person2GenomeIdentifier) @@ -756,7 +699,7 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom checkIfConflictExists := func()(bool, error){ numberOfLociTested := 0 - offspringRiskScore := 0 + offspringAverageRiskScore := 0 offspringLociInfoMap := make(map[[3]byte]geneticAnalysis.OffspringPolygenicDiseaseLocusInfo) firstItemReached := false @@ -764,12 +707,12 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom for _, genomePairDiseaseInfoObject := range offspringPolygenicDiseaseInfoMap{ genomePairNumberOfLociTested := genomePairDiseaseInfoObject.NumberOfLociTested - genomePairOffspringRiskScore := genomePairDiseaseInfoObject.OffspringRiskScore + genomePairOffspringAverageRiskScore := genomePairDiseaseInfoObject.OffspringAverageRiskScore genomePairLociInfoMap := genomePairDiseaseInfoObject.LociInfoMap if (firstItemReached == false){ numberOfLociTested = genomePairNumberOfLociTested - offspringRiskScore = genomePairOffspringRiskScore + offspringAverageRiskScore = genomePairOffspringAverageRiskScore offspringLociInfoMap = genomePairLociInfoMap firstItemReached = true continue @@ -777,7 +720,7 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom if (numberOfLociTested != genomePairNumberOfLociTested){ return true, nil } - if (offspringRiskScore != genomePairOffspringRiskScore){ + if (offspringAverageRiskScore != genomePairOffspringAverageRiskScore){ return true, nil } areEqual := maps.Equal(offspringLociInfoMap, genomePairLociInfoMap) @@ -938,6 +881,512 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom return true, analysisString, nil } + +// We also use this function when calculating offspring probabilities between users in viewProfileGui.go +//Outputs: +// -bool: Probability offspring has disease is known +// -int: Percentage probability offspring has disease (0-100) +// -bool: Probability offspring has variant is known +// -int: Percentage probability offspring has variant (0-100) +// -error +func GetOffspringMonogenicDiseaseProbabilities(dominantOrRecessive string, person1ProbabilityIsKnown bool, person1WillPassVariantPercentageProbability int, person2ProbabilityIsKnown bool, person2WillPassVariantPercentageProbability int)(bool, int, bool, int, error){ + + if (dominantOrRecessive != "Dominant" && dominantOrRecessive != "Recessive"){ + return false, 0, false, 0, errors.New("GetOffspringMonogenicDiseaseProbabilities called with invalid dominantOrRecessive: " + dominantOrRecessive) + } + if (person1ProbabilityIsKnown == false && person2ProbabilityIsKnown == false){ + return false, 0, false, 0, nil + } + + if (person1ProbabilityIsKnown == true){ + if (person1WillPassVariantPercentageProbability < 0 || person1WillPassVariantPercentageProbability > 100){ + return false, 0, false, 0, errors.New("GetOffspringMonogenicDiseaseProbabilities called with invalid person1WillPassVariantProbability") + } + } + if (person2ProbabilityIsKnown == true){ + if (person2WillPassVariantPercentageProbability < 0 || person2WillPassVariantPercentageProbability > 100){ + return false, 0, false, 0, errors.New("GetOffspringMonogenicDiseaseProbabilities called with invalid person2WillPassVariantProbability") + } + } + if (person1ProbabilityIsKnown == false || person2ProbabilityIsKnown == false){ + // Only 1 of the person's probabilities are known + + getPersonWhoHasVariantProbabilityOfPassingIt := func()int{ + if (person1ProbabilityIsKnown == true){ + return person1WillPassVariantPercentageProbability + } + return person2WillPassVariantPercentageProbability + } + + personWhoHasVariantProbabilityOfPassingIt := getPersonWhoHasVariantProbabilityOfPassingIt() + + if (personWhoHasVariantProbabilityOfPassingIt == 100){ + if (dominantOrRecessive == "Dominant"){ + // We know the offspring will have the disease and will have a variant + return true, 100, true, 100, nil + } + //dominantOrRecessive == "Recessive" + // We don't know if the offspring will have the disease, but we know they will have a variant + return false, 0, true, 100, nil + } + if (personWhoHasVariantProbabilityOfPassingIt == 0){ + + if (dominantOrRecessive == "Recessive"){ + // We know the offspring will not have the disease, but we don't know if they will have a variant + return true, 0, false, 0, nil + } + } + + // We don't know the probabilities that the offspring will have the disease or if they will have a variant + return false, 0, false, 0, nil + } + + person1WillPassVariantProbability := float64(person1WillPassVariantPercentageProbability)/100 + person2WillPassVariantProbability := float64(person2WillPassVariantPercentageProbability)/100 + + // The probability offspring has a variant = the probability that either parent passes a variant (inclusive or) + // We find the probability of the offspring having a monogenic disease variant as follows: + // P(A U B) = P(A) + P(B) - P(A ∩ B) + // (Probability of person 1 passing a variant) + (Probability of person 2 passing a variant) - (Probability of offspring having disease) + // A person with a variant may have the disease, or just be a carrier. + probabilityOffspringHasVariant := person1WillPassVariantProbability + person2WillPassVariantProbability - (person1WillPassVariantProbability * person2WillPassVariantProbability) + + if (dominantOrRecessive == "Dominant"){ + + // The probability of having the monogenic disease is the same as the probability of having a variant + + percentageProbabilityOffspringHasVariant := int(probabilityOffspringHasVariant * 100) + + return true, percentageProbabilityOffspringHasVariant, true, percentageProbabilityOffspringHasVariant, nil + } + + // We find the probability of the offspring having the mongenic disease as follows: + // P(A and B) = P(A) * P(B) + // (Probability of person 1 Passing a variant) * (Probability of person 2 passing a variant) + probabilityOffspringHasDisease := person1WillPassVariantProbability * person2WillPassVariantProbability + + percentageProbabilityOffspringHasDisease := probabilityOffspringHasDisease * 100 + percentageProbabilityOffspringHasVariant := probabilityOffspringHasVariant * 100 + + // This conversion remove any digits after the radix point + // This will not result in any false 0% values, an example being 0.9% becoming 0% + // This is because the lowest non-zero probability a person can have for passing a variant is 50% + // Thus, the lowest non-zero probability of an offspring having a disease is 25% + percentageProbabilityOffspringHasDiseaseInt := int(percentageProbabilityOffspringHasDisease) + percentageProbabilityOffspringHasVariantInt := int(percentageProbabilityOffspringHasVariant) + + return true, percentageProbabilityOffspringHasDiseaseInt, true, percentageProbabilityOffspringHasVariantInt, nil +} + + +// This is used to calculate user polygenic disease info for users +// It is faster to do it this way, because we don't create 100 prospective offspring +// We instead create 4 outcomes for each locus +// We can do this because testing each locus's risk score is independent of every other locus +// This is not true for traits, because trait rules are effected by multiple different loci +// When using the fast method for polygenic diseases, we don't get a sample of 100 offspring disease risk scores. +// The average risk score should still be the same for the fast and normal methods +// This function is also faster because we don't calculate odds ratio information or information about each locus +//Outputs: +// -bool: Any loci tested (if false, no offspring polygenic disease information is known) +// -int: Offspring Risk Score (Value between 0-10) +// -int: Number of loci tested +// -error +func GetOffspringPolygenicDiseaseInfo_Fast(diseaseLociList []polygenicDiseases.DiseaseLocus, person1LocusValuesMap map[int64]locusValue.LocusValue, person2LocusValuesMap map[int64]locusValue.LocusValue)(bool, int, int, error){ + + if (len(person1LocusValuesMap) == 0){ + return false, 0, 0, nil + } + if (len(person2LocusValuesMap) == 0){ + return false, 0, 0, nil + } + + numberOfLociTested := 0 + + offspringSummedRiskWeights := 0 + offspringMinimumPossibleRiskWeightSum := 0 + offspringMaximumPossibleRiskWeightSum := 0 + + for _, locusObject := range diseaseLociList{ + + locusRSID := locusObject.LocusRSID + locusRiskWeightsMap := locusObject.RiskWeightsMap + locusMinimumWeight := locusObject.MinimumRiskWeight + locusMaximumWeight := locusObject.MaximumRiskWeight + + person1LocusValueFound, person1LocusBase1Value, person1LocusBase2Value, _, _, err := getLocusValueFromGenomeMap(true, person1LocusValuesMap, locusRSID) + if (err != nil) { return false, 0, 0, err } + if (person1LocusValueFound == false){ + // None of the offspring will have a value for this locus + continue + } + + person2LocusValueFound, person2LocusBase1Value, person2LocusBase2Value, _, _, err := getLocusValueFromGenomeMap(true, person2LocusValuesMap, locusRSID) + if (err != nil) { return false, 0, 0, err } + if (person2LocusValueFound == false){ + // None of the offspring will have a value for this locus + continue + } + + numberOfLociTested += 1 + + offspringBasePairOutcome1 := person1LocusBase1Value + ";" + person2LocusBase1Value + offspringBasePairOutcome2 := person1LocusBase2Value + ";" + person2LocusBase2Value + offspringBasePairOutcome3 := person1LocusBase1Value + ";" + person2LocusBase2Value + offspringBasePairOutcome4 := person1LocusBase2Value + ";" + person2LocusBase1Value + + baseOutcomesList := []string{offspringBasePairOutcome1, offspringBasePairOutcome2, offspringBasePairOutcome3, offspringBasePairOutcome4} + + outcomesSummedRiskWeight := 0 + + for _, outcomeBasePair := range baseOutcomesList{ + + isValid := verifyBasePair(outcomeBasePair) + if (isValid == false){ + return false, 0, 0, errors.New("GetOffspringPolygenicDiseaseInfo_Fast called with genomeMap containing invalid locus value base pair: " + outcomeBasePair) + } + + offspringOutcomeRiskWeight, exists := locusRiskWeightsMap[outcomeBasePair] + if (exists == false){ + // We do not know the risk weight for this base pair + // We treat this as a 0 risk weight + continue + } + + outcomesSummedRiskWeight += offspringOutcomeRiskWeight + } + + locusAverageRiskWeight := outcomesSummedRiskWeight/4 + + offspringSummedRiskWeights += locusAverageRiskWeight + + offspringMinimumPossibleRiskWeightSum += locusMinimumWeight + offspringMaximumPossibleRiskWeightSum += locusMaximumWeight + } + + offspringAverageDiseaseRiskScore, err := helpers.ScaleNumberProportionally(true, offspringSummedRiskWeights, offspringMinimumPossibleRiskWeightSum, offspringMaximumPossibleRiskWeightSum, 0, 10) + if (err != nil) { return false, 0, 0, err } + + if (numberOfLociTested == 0){ + // No locations were tested + return false, 0, 0, nil + } + + return true, offspringAverageDiseaseRiskScore, numberOfLociTested, nil +} + +//Outputs: +// -bool: Any loci tested (if false, no offspring polygenic disease information is known) +// -int: Offspring Risk Score (Value between 0-10) +// -int: Number of loci tested +// -map[[3]byte]geneticAnalysis.OffspringPolygenicDiseaseLocusInfo: Offspring Locus information map +// Map Structure: Locus identifier -> OffspringPolygenicDiseaseLocusInfo +// -[]int: Sample offspring risks scores list +// -error +func GetOffspringPolygenicDiseaseInfo(diseaseLociList []polygenicDiseases.DiseaseLocus, person1LocusValuesMap map[int64]locusValue.LocusValue, person2LocusValuesMap map[int64]locusValue.LocusValue)(bool, int, int, map[[3]byte]geneticAnalysis.OffspringPolygenicDiseaseLocusInfo, []int, error){ + + if (len(person1LocusValuesMap) == 0){ + return false, 0, 0, nil, nil, nil + } + if (len(person2LocusValuesMap) == 0){ + return false, 0, 0, nil, nil, nil + } + + // First, we create 100 prospective offspring genomes. + + diseaseLociRSIDsList := make([]int64, 0) + + for _, diseaseLocusObject := range diseaseLociList{ + + locusRSID := diseaseLocusObject.LocusRSID + diseaseLociRSIDsList = append(diseaseLociRSIDsList, locusRSID) + } + + anyLocusValueExists, prospectiveOffspringGenomesList, err := getProspectiveOffspringGenomesList(diseaseLociRSIDsList, person1LocusValuesMap, person2LocusValuesMap) + if (err != nil) { return false, 0, 0, nil, nil, err } + if (anyLocusValueExists == false){ + return false, 0, 0, nil, nil, nil + } + + // This will sum every offspring's average disease risk score + offspringAverageRiskScoreSum := 0 + + // This stores a list of every prospective offspring's risk score + sampleOffspringRiskScoresList := make([]int, 0) + + type offspringSummedLocusInfoObject struct{ + + SummedLocusRiskWeights int + + SummedOddsRatios float64 + + NumberOfSummedOddsRatios int + + // This is the number of unknown-odds-ratio-weight sums that we summed up + NumberOfUnknownOddsRatioWeightSums int + + // This is the sum of every unknown-odds-ratio-weight-sum for each prospective offspring for this genome + UnknownOddsRatioWeightSumsSummed int + } + + // Map Structure: Locus Identifier -> offspringSummedLocusInfoObject + offspringLocusInfoSumsMap := make(map[[3]byte]offspringSummedLocusInfoObject) + + for offspringIndex, offspringGenomeMap := range prospectiveOffspringGenomesList{ + + offspringSummedRiskWeights := 0 + offspringMinimumPossibleRiskWeightSum := 0 + offspringMaximumPossibleRiskWeightSum := 0 + + for _, locusObject := range diseaseLociList{ + + locusIdentifierHex := locusObject.LocusIdentifier + + locusIdentifier, err := encoding.DecodeHexStringTo3ByteArray(locusIdentifierHex) + if (err != nil) { return false, 0, 0, nil, nil, err } + + offspringLocusInfoSumsObject, exists := offspringLocusInfoSumsMap[locusIdentifier] + if (exists == false){ + + if (offspringIndex != 0){ + // We already checked a previous offspring for this locus, and it's value doesn't exist + continue + } + } + + locusRSID := locusObject.LocusRSID + locusRiskWeightsMap := locusObject.RiskWeightsMap + locusOddsRatiosMap := locusObject.OddsRatiosMap + locusMinimumWeight := locusObject.MinimumRiskWeight + locusMaximumWeight := locusObject.MaximumRiskWeight + + basePairValueFound, locusBase1Value, locusBase2Value, _, _, err := getLocusValueFromGenomeMap(true, offspringGenomeMap, locusRSID) + if (err != nil) { return false, 0, 0, nil, nil, err } + if (basePairValueFound == false){ + // None of the offspring will have a value for this locus + continue + } + + locusRiskWeight, locusOddsRatioIsKnown, locusOddsRatio, err := getGenomePolygenicDiseaseLocusRiskInfo(locusRiskWeightsMap, locusOddsRatiosMap, locusBase1Value, locusBase2Value) + if (err != nil) { return false, 0, 0, nil, nil, err } + + offspringLocusInfoSumsObject.SummedLocusRiskWeights += locusRiskWeight + + if (locusOddsRatioIsKnown == true){ + offspringLocusInfoSumsObject.SummedOddsRatios += locusOddsRatio + offspringLocusInfoSumsObject.NumberOfSummedOddsRatios += 1 + } else { + offspringLocusInfoSumsObject.UnknownOddsRatioWeightSumsSummed += locusRiskWeight + offspringLocusInfoSumsObject.NumberOfUnknownOddsRatioWeightSums += 1 + } + + offspringLocusInfoSumsMap[locusIdentifier] = offspringLocusInfoSumsObject + + offspringSummedRiskWeights += locusRiskWeight + + offspringMinimumPossibleRiskWeightSum += locusMinimumWeight + offspringMaximumPossibleRiskWeightSum += locusMaximumWeight + } + + offspringAverageDiseaseRiskScore, err := helpers.ScaleNumberProportionally(true, offspringSummedRiskWeights, offspringMinimumPossibleRiskWeightSum, offspringMaximumPossibleRiskWeightSum, 0, 10) + if (err != nil) { return false, 0, 0, nil, nil, err } + + sampleOffspringRiskScoresList = append(sampleOffspringRiskScoresList, offspringAverageDiseaseRiskScore) + + offspringAverageRiskScoreSum += offspringAverageDiseaseRiskScore + } + + numberOfLociTested := len(offspringLocusInfoSumsMap) + + if (numberOfLociTested == 0){ + // No locations were tested + return false, 0, 0, nil, nil, nil + } + + offspringAverageRiskScore := offspringAverageRiskScoreSum/100 + + // Map Structure: Locus Identifier -> OffspringPolygenicDiseaseLocusInfo + offspringDiseaseLociInfoMap := make(map[[3]byte]geneticAnalysis.OffspringPolygenicDiseaseLocusInfo) + + for locusIdentifier, summedLocusInfoObject := range offspringLocusInfoSumsMap{ + + summedLocusRiskWeights := summedLocusInfoObject.SummedLocusRiskWeights + summedOddsRatios := summedLocusInfoObject.SummedOddsRatios + numberOfSummedOddsRatios := summedLocusInfoObject.NumberOfSummedOddsRatios + numberOfUnknownOddsRatioWeightSums := summedLocusInfoObject.NumberOfUnknownOddsRatioWeightSums + unknownOddsRatioWeightSumsSummed := summedLocusInfoObject.UnknownOddsRatioWeightSumsSummed + + // There are 100 prospective offspring, so we divide by 100 + locusAverageRiskWeight := summedLocusRiskWeights/100 + + newLocusInfoObject := geneticAnalysis.OffspringPolygenicDiseaseLocusInfo{ + OffspringAverageRiskWeight: locusAverageRiskWeight, + } + + if (numberOfSummedOddsRatios != 0){ + newLocusInfoObject.OffspringOddsRatioIsKnown = true + + offspringAverageOddsRatio := summedOddsRatios/float64(numberOfSummedOddsRatios) + + newLocusInfoObject.OffspringAverageOddsRatio = offspringAverageOddsRatio + } + + if (numberOfUnknownOddsRatioWeightSums != 0){ + + offspringAverageUnknownOddsRatiosWeightSum := unknownOddsRatioWeightSumsSummed/numberOfUnknownOddsRatioWeightSums + + newLocusInfoObject.OffspringAverageUnknownOddsRatiosWeightSum = offspringAverageUnknownOddsRatiosWeightSum + } + + offspringDiseaseLociInfoMap[locusIdentifier] = newLocusInfoObject + } + + return true, offspringAverageRiskScore, numberOfLociTested, offspringDiseaseLociInfoMap, sampleOffspringRiskScoresList, nil +} + + +//Outputs: +// -bool: Any rules tested (if false, no offspring trait information is known) +// -int: Number of rules tested +// -map[[3]byte]int: Offspring probability of passing rules map +// Map Structure: Rule identifier -> Offspring probability of passing rule (1-100) +// -map[string]float64: Offspring average outcome scores map +// Map Structure: Outcome Name -> Offspring average outcome score +// -error +func GetOffspringTraitInfo(traitObject traits.Trait, person1LocusValuesMap map[int64]locusValue.LocusValue, person2LocusValuesMap map[int64]locusValue.LocusValue)(bool, int, map[[3]byte]int, map[string]float64, error){ + + if (len(person1LocusValuesMap) == 0){ + return false, 0, nil, nil, nil + } + if (len(person2LocusValuesMap) == 0){ + return false, 0, nil, nil, nil + } + + // First, we create 100 prospective offspring genomes. + + traitLociList := traitObject.LociList + + anyLocusValueExists, prospectiveOffspringGenomesList, err := getProspectiveOffspringGenomesList(traitLociList, person1LocusValuesMap, person2LocusValuesMap) + if (err != nil) { return false, 0, nil, nil, err } + if (anyLocusValueExists == false){ + return false, 0, nil, nil, nil + } + + traitRulesList := traitObject.RulesList + + // Map Structure: Rule Identifier -> Number of offspring who pass the rule (out of 100 prospective offspring) + offspringPassesRulesCountMap := make(map[[3]byte]int) + + // We use this map to keep track of the rules for which we know every offspring's passes-rule status + // Map Structure: Rule Identifier -> Rule Object + offspringRulesWithKnownStatusMap := make(map[[3]byte]traits.TraitRule) + + for offspringIndex, offspringGenomeMap := range prospectiveOffspringGenomesList{ + + // We iterate through rules to determine genome pair trait info + + for _, ruleObject := range traitRulesList{ + + ruleIdentifierHex := ruleObject.RuleIdentifier + + ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) + if (err != nil) { return false, 0, nil, nil, err } + + if (offspringIndex == 0){ + + offspringRulesWithKnownStatusMap[ruleIdentifier] = ruleObject + } else { + + _, exists := offspringRulesWithKnownStatusMap[ruleIdentifier] + if (exists == false){ + // We already tried to check a previous offspring's passes-rule status for this rule + // We know that the offspring's passes-rule status will be unknown for every prospective offspring + continue + } + } + + // This is a list that describes the locus rsids and their values that must be fulfilled to pass the rule + ruleLocusObjectsList := ruleObject.LociList + + offspringPassesRuleIsKnown, offspringPassesRule, err := GetGenomePassesTraitRuleStatus(ruleLocusObjectsList, offspringGenomeMap, false) + if (err != nil){ return false, 0, nil, nil, err } + if (offspringPassesRuleIsKnown == false){ + continue + } + + if (offspringPassesRule == true){ + offspringPassesRulesCountMap[ruleIdentifier] += 1 + } + } + } + + // Map Structure: Rule Identifier -> Offspring Probability Of Passing Rule + // The map value stores the probability that the offspring will pass the rule + // This is a number between 0-100% + offspringProbabilityOfPassingRulesMap := make(map[[3]byte]int) + + // Map Structure: Outcome Name -> Outcome Score + // Example: "Intolerant" -> 2.5 + offspringAverageOutcomeScoresMap := make(map[string]float64) + + for ruleIdentifier, ruleObject := range offspringRulesWithKnownStatusMap{ + + //Output: + // -int: Offspring probability of passing rule (0-100%) + getOffspringPercentageProbabilityOfPassingRule := func()int{ + + numberOfOffspringWhoPassRule, exists := offspringPassesRulesCountMap[ruleIdentifier] + if (exists == false){ + // None of the offspring passed the rule + return 0 + } + + // There are 100 tested offspring + // Thus, the percentage of offspring who passed the rule is the same as the number of offspring who passed the rule + // The probability of the offspring passing the rule is the same as the percentage of offspring who passed the rule + + return numberOfOffspringWhoPassRule + } + + offspringPercentageProbabilityOfPassingRule := getOffspringPercentageProbabilityOfPassingRule() + + offspringProbabilityOfPassingRulesMap[ruleIdentifier] = offspringPercentageProbabilityOfPassingRule + + // This is the 0 - 1 probability value + offspringProbabilityOfPassingRule := float64(offspringPercentageProbabilityOfPassingRule)/100 + + ruleOutcomePointsMap := ruleObject.OutcomePointsMap + + for outcomeName, outcomePointsEffect := range ruleOutcomePointsMap{ + + pointsToAdd := float64(outcomePointsEffect) * offspringProbabilityOfPassingRule + + offspringAverageOutcomeScoresMap[outcomeName] += pointsToAdd + } + } + + numberOfRulesTested := len(offspringProbabilityOfPassingRulesMap) + + if (numberOfRulesTested == 0){ + return false, 0, nil, nil, nil + } + + traitOutcomesList := traitObject.OutcomesList + + // We add all outcomes for which there were no points + + for _, traitOutcome := range traitOutcomesList{ + + _, exists := offspringAverageOutcomeScoresMap[traitOutcome] + if (exists == false){ + offspringAverageOutcomeScoresMap[traitOutcome] = 0 + } + } + + return true, numberOfRulesTested, offspringProbabilityOfPassingRulesMap, offspringAverageOutcomeScoresMap, nil +} + + // This function will return a list of 100 prospective offspring genomes // Each genome represents an equal-probability offspring genome from both people's genomes // This function takes into account the effects of genetic linkage @@ -1207,413 +1656,6 @@ func getProspectiveOffspringGenomesList(lociList []int64, person1LociMap map[int return true, offspringGenomesList, nil } -// We also use this function when calculating offspring probabilities between users in viewProfileGui.go -//Outputs: -// -bool: Probability offspring has disease is known -// -int: Percentage probability offspring has disease (0-100) -// -bool: Probability offspring has variant is known -// -int: Percentage probability offspring has variant (0-100) -// -error -func GetOffspringMonogenicDiseaseProbabilities(dominantOrRecessive string, person1ProbabilityIsKnown bool, person1WillPassVariantPercentageProbability int, person2ProbabilityIsKnown bool, person2WillPassVariantPercentageProbability int)(bool, int, bool, int, error){ - - if (dominantOrRecessive != "Dominant" && dominantOrRecessive != "Recessive"){ - return false, 0, false, 0, errors.New("GetOffspringMonogenicDiseaseProbabilities called with invalid dominantOrRecessive: " + dominantOrRecessive) - } - if (person1ProbabilityIsKnown == false && person2ProbabilityIsKnown == false){ - return false, 0, false, 0, nil - } - - if (person1ProbabilityIsKnown == true){ - if (person1WillPassVariantPercentageProbability < 0 || person1WillPassVariantPercentageProbability > 100){ - return false, 0, false, 0, errors.New("GetOffspringMonogenicDiseaseProbabilities called with invalid person1WillPassVariantProbability") - } - } - if (person2ProbabilityIsKnown == true){ - if (person2WillPassVariantPercentageProbability < 0 || person2WillPassVariantPercentageProbability > 100){ - return false, 0, false, 0, errors.New("GetOffspringMonogenicDiseaseProbabilities called with invalid person2WillPassVariantProbability") - } - } - if (person1ProbabilityIsKnown == false || person2ProbabilityIsKnown == false){ - // Only 1 of the person's probabilities are known - - getPersonWhoHasVariantProbabilityOfPassingIt := func()int{ - if (person1ProbabilityIsKnown == true){ - return person1WillPassVariantPercentageProbability - } - return person2WillPassVariantPercentageProbability - } - - personWhoHasVariantProbabilityOfPassingIt := getPersonWhoHasVariantProbabilityOfPassingIt() - - if (personWhoHasVariantProbabilityOfPassingIt == 100){ - if (dominantOrRecessive == "Dominant"){ - // We know the offspring will have the disease and will have a variant - return true, 100, true, 100, nil - } - //dominantOrRecessive == "Recessive" - // We don't know if the offspring will have the disease, but we know they will have a variant - return false, 0, true, 100, nil - } - if (personWhoHasVariantProbabilityOfPassingIt == 0){ - - if (dominantOrRecessive == "Recessive"){ - // We know the offspring will not have the disease, but we don't know if they will have a variant - return true, 0, false, 0, nil - } - } - - // We don't know the probabilities that the offspring will have the disease or if they will have a variant - return false, 0, false, 0, nil - } - - person1WillPassVariantProbability := float64(person1WillPassVariantPercentageProbability)/100 - person2WillPassVariantProbability := float64(person2WillPassVariantPercentageProbability)/100 - - // The probability offspring has a variant = the probability that either parent passes a variant (inclusive or) - // We find the probability of the offspring having a monogenic disease variant as follows: - // P(A U B) = P(A) + P(B) - P(A ∩ B) - // (Probability of person 1 passing a variant) + (Probability of person 2 passing a variant) - (Probability of offspring having disease) - // A person with a variant may have the disease, or just be a carrier. - probabilityOffspringHasVariant := person1WillPassVariantProbability + person2WillPassVariantProbability - (person1WillPassVariantProbability * person2WillPassVariantProbability) - - if (dominantOrRecessive == "Dominant"){ - - // The probability of having the monogenic disease is the same as the probability of having a variant - - percentageProbabilityOffspringHasVariant := int(probabilityOffspringHasVariant * 100) - - return true, percentageProbabilityOffspringHasVariant, true, percentageProbabilityOffspringHasVariant, nil - } - - // We find the probability of the offspring having the mongenic disease as follows: - // P(A and B) = P(A) * P(B) - // (Probability of person 1 Passing a variant) * (Probability of person 2 passing a variant) - probabilityOffspringHasDisease := person1WillPassVariantProbability * person2WillPassVariantProbability - - percentageProbabilityOffspringHasDisease := probabilityOffspringHasDisease * 100 - percentageProbabilityOffspringHasVariant := probabilityOffspringHasVariant * 100 - - // This conversion remove any digits after the radix point - // This will not result in any false 0% values, an example being 0.9% becoming 0% - // This is because the lowest non-zero probability a person can have for passing a variant is 50% - // Thus, the lowest non-zero probability of an offspring having a disease is 25% - percentageProbabilityOffspringHasDiseaseInt := int(percentageProbabilityOffspringHasDisease) - percentageProbabilityOffspringHasVariantInt := int(percentageProbabilityOffspringHasVariant) - - return true, percentageProbabilityOffspringHasDiseaseInt, true, percentageProbabilityOffspringHasVariantInt, nil -} - -//Outputs: -// -int: Offspring average risk weight -// -bool: Odds ratio is known -// -float64: Offspring average odds ratio -// -int: Offspring unknown odds ratios weight sum -// -error -func GetOffspringPolygenicDiseaseLocusInfo(locusRiskWeightsMap map[string]int, locusOddsRatiosMap map[string]float64, person1LocusBase1 string, person1LocusBase2 string, person2LocusBase1 string, person2LocusBase2 string)(int, bool, float64, int, error){ - - // We create the 4 options for the offspring's bases at this locus - - offspringBasePairOutcome1 := person1LocusBase1 + ";" + person2LocusBase1 - offspringBasePairOutcome2 := person1LocusBase2 + ";" + person2LocusBase2 - offspringBasePairOutcome3 := person1LocusBase1 + ";" + person2LocusBase2 - offspringBasePairOutcome4 := person1LocusBase2 + ";" + person2LocusBase1 - - baseOutcomesList := []string{offspringBasePairOutcome1, offspringBasePairOutcome2, offspringBasePairOutcome3, offspringBasePairOutcome4} - - summedRiskWeight := 0 - - numberOfSummedOddsRatios := 0 - summedOddsRatios := float64(0) - - numberOfSummedUnknownOddsRatioWeights := 0 - summedUnknownOddsRatioWeights := 0 - - for _, outcomeBasePair := range baseOutcomesList{ - - isValid := verifyBasePair(outcomeBasePair) - if (isValid == false){ - return 0, false, 0, 0, errors.New("GetOffspringPolygenicDiseaseLocusInfo called with invalid locus base pair: " + outcomeBasePair) - } - - offspringOutcomeRiskWeight, exists := locusRiskWeightsMap[outcomeBasePair] - if (exists == false){ - // We do not know the risk weight for this base pair - // We treat this as a 0 risk for both weight and odds ratio - // No effect on risk is represented as an odds ratio of 1 - - summedOddsRatios += 1 - numberOfSummedOddsRatios += 1 - continue - } - summedRiskWeight += offspringOutcomeRiskWeight - - offspringOutcomeOddsRatio, exists := locusOddsRatiosMap[outcomeBasePair] - if (exists == false){ - // This particular outcome has no known odds ratio - // We add it to the unknown odds ratio weights sum - summedUnknownOddsRatioWeights += offspringOutcomeRiskWeight - numberOfSummedUnknownOddsRatioWeights += 1 - } else { - summedOddsRatios += offspringOutcomeOddsRatio - numberOfSummedOddsRatios += 1 - } - } - - averageRiskWeight := summedRiskWeight/4 - - getAverageUnknownOddsRatiosWeightSum := func()int{ - - if (numberOfSummedUnknownOddsRatioWeights == 0){ - return 0 - } - averageUnknownOddsRatiosWeightSum := summedUnknownOddsRatioWeights/numberOfSummedUnknownOddsRatioWeights - return averageUnknownOddsRatiosWeightSum - } - - averageUnknownOddsRatiosWeightSum := getAverageUnknownOddsRatiosWeightSum() - - if (numberOfSummedOddsRatios == 0){ - - return averageRiskWeight, false, 0, averageUnknownOddsRatiosWeightSum, nil - } - - averageOddsRatio := summedOddsRatios/float64(numberOfSummedOddsRatios) - - return averageRiskWeight, true, averageOddsRatio, averageUnknownOddsRatiosWeightSum, nil -} - - -//Outputs: -// -bool: Any rules tested (if false, no offspring trait information is known) -// -int: Number of rules tested -// -map[[3]byte]int: Offspring probability of passing rules map -// Map Structure: Rule identifier -> Offspring probability of passing rule (1-100) -// -map[string]float64: Offspring average outcome scores map -// Map Structure: Outcome Name -> Offspring average outcome score -// -error -func GetOffspringTraitInfo(traitObject traits.Trait, person1LocusValuesMap map[int64]locusValue.LocusValue, person2LocusValuesMap map[int64]locusValue.LocusValue)(bool, int, map[[3]byte]int, map[string]float64, error){ - - // First, we create 100 prospective offspring genomes. - - traitLociList := traitObject.LociList - - anyLocusValueExists, prospectiveOffspringGenomesList, err := getProspectiveOffspringGenomesList(traitLociList, person1LocusValuesMap, person2LocusValuesMap) - if (err != nil) { return false, 0, nil, nil, err } - if (anyLocusValueExists == false){ - return false, 0, nil, nil, nil - } - - traitRulesList := traitObject.RulesList - - // Map Structure: Rule Identifier -> Number of offspring who pass the rule (out of 100 prospective offspring) - offspringPassesRulesCountMap := make(map[[3]byte]int) - - // We use this map to keep track of the rules for which we know every offspring's passes-rule status - // Map Structure: Rule Identifier -> Rule Object - offspringRulesWithKnownStatusMap := make(map[[3]byte]traits.TraitRule) - - for offspringIndex, offspringGenomeMap := range prospectiveOffspringGenomesList{ - - // We iterate through rules to determine genome pair trait info - - for _, ruleObject := range traitRulesList{ - - ruleIdentifierHex := ruleObject.RuleIdentifier - - ruleIdentifier, err := encoding.DecodeHexStringTo3ByteArray(ruleIdentifierHex) - if (err != nil) { return false, 0, nil, nil, err } - - if (offspringIndex != 0){ - - _, exists := offspringRulesWithKnownStatusMap[ruleIdentifier] - if (exists == false){ - // We already tried to check a previous offspring's passes-rule status for this rule - // We know that the offspring's passes-rule status will be unknown for every prospective offspring - continue - } - } - - // This is a list that describes the locus rsids and their values that must be fulfilled to pass the rule - ruleLocusObjectsList := ruleObject.LociList - - //Outputs: - // -bool: Offspring passes rule is known - // -bool: Offspring passes rule - getOffspringPassesRuleStatus := func()(bool, bool){ - - // If any rule locus status is unknown, then we consider the offspring-passes-rule status to be unknown, - // unless we know that there is a rule that the offspring does not pass - anyRuleIsUnknown := false - - for _, ruleLocusObject := range ruleLocusObjectsList{ - - locusRSID := ruleLocusObject.LocusRSID - - locusRequiredBasePairsList := ruleLocusObject.BasePairsList - - offspringLocusValue, exists := offspringGenomeMap[locusRSID] - if (exists == false){ - anyRuleIsUnknown = true - // We keep searching to see if there are any rules we know the offspring does not pass - continue - } - - offspringBase1 := offspringLocusValue.Base1Value - offspringBase2 := offspringLocusValue.Base2Value - - offspringBasePair := offspringBase1 + ";" + offspringBase2 - - offspringPassesRuleLocus := slices.Contains(locusRequiredBasePairsList, offspringBasePair) - if (offspringPassesRuleLocus == false){ - // The offspring does not pass this rule locus - // Thus, the offspring does not pass the rule - return true, false - } - } - - if (anyRuleIsUnknown == true){ - // We don't know if the offspring passes the rule - return false, false - } - - // The offspring passes the rule - return true, true - } - - offspringPassesRuleIsKnown, offspringPassesRule := getOffspringPassesRuleStatus() - if (offspringPassesRuleIsKnown == false){ - continue - } - - offspringRulesWithKnownStatusMap[ruleIdentifier] = ruleObject - - if (offspringPassesRule == true){ - offspringPassesRulesCountMap[ruleIdentifier] += 1 - } - } - } - - // Map Structure: Rule Identifier -> Offspring Probability Of Passing Rule - // The map value stores the probability that the offspring will pass the rule - // This is a number between 0-100% - offspringProbabilityOfPassingRulesMap := make(map[[3]byte]int) - - // Map Structure: Outcome Name -> Outcome Score - // Example: "Intolerant" -> 2.5 - offspringAverageOutcomeScoresMap := make(map[string]float64) - - for ruleIdentifier, ruleObject := range offspringRulesWithKnownStatusMap{ - - //Output: - // -int: Offspring probability of passing rule (0-100%) - getOffspringPercentageProbabilityOfPassingRule := func()int{ - - numberOfOffspringWhoPassRule, exists := offspringPassesRulesCountMap[ruleIdentifier] - if (exists == false){ - // None of the offspring passed the rule - return 0 - } - - // There are 100 tested offspring - // Thus, the percentage of offspring who passed the rule is the same as the number of offspring who passed the rule - // The probability of the offspring passing the rule is the same as the percentage of offspring who passed the rule - - return numberOfOffspringWhoPassRule - } - - offspringPercentageProbabilityOfPassingRule := getOffspringPercentageProbabilityOfPassingRule() - - offspringProbabilityOfPassingRulesMap[ruleIdentifier] = offspringPercentageProbabilityOfPassingRule - - // This is the 0 - 1 probability value - offspringProbabilityOfPassingRule := float64(offspringPercentageProbabilityOfPassingRule)/100 - - ruleOutcomePointsMap := ruleObject.OutcomePointsMap - - for outcomeName, outcomePointsEffect := range ruleOutcomePointsMap{ - - pointsToAdd := float64(outcomePointsEffect) * offspringProbabilityOfPassingRule - - offspringAverageOutcomeScoresMap[outcomeName] += pointsToAdd - } - } - - numberOfRulesTested := len(offspringProbabilityOfPassingRulesMap) - - if (numberOfRulesTested == 0){ - return false, 0, nil, nil, nil - } - - traitOutcomesList := traitObject.OutcomesList - - // We add all outcomes for which there were no points - - for _, traitOutcome := range traitOutcomesList{ - - _, exists := offspringAverageOutcomeScoresMap[traitOutcome] - if (exists == false){ - offspringAverageOutcomeScoresMap[traitOutcome] = 0 - } - } - - return true, numberOfRulesTested, offspringProbabilityOfPassingRulesMap, offspringAverageOutcomeScoresMap, nil -} - -// This function will retrieve the base pair of the locus from the input genome map -// We need this because each rsID has aliases, so we must sometimes check those aliases to find locus values -// -// Outputs: -// -bool: Valid base pair value found -// -string: Base 1 Value (Nucleotide base for the SNP) -// -string: Base 2 Value (Nucleotide base for the SNP) -// -bool: Locus base pair is phased -// -error -func getGenomeLocusBasePair(inputGenomeMap map[int64]locusValue.LocusValue, locusRSID int64)(bool, string, string, bool, error){ - - // Outputs: - // -bool: Locus value found - // -locusValue.LocusValue - // -error - getLocusValue := func()(bool, locusValue.LocusValue, error){ - - currentLocusValue, exists := inputGenomeMap[locusRSID] - if (exists == true){ - return true, currentLocusValue, nil - } - - // We check for aliases - - anyAliasesExist, rsidAliasesList, err := locusMetadata.GetRSIDAliases(locusRSID) - if (err != nil) { return false, locusValue.LocusValue{}, err } - if (anyAliasesExist == false){ - return false, locusValue.LocusValue{}, nil - } - - for _, rsidAlias := range rsidAliasesList{ - - currentLocusValue, exists := inputGenomeMap[rsidAlias] - if (exists == true){ - return true, currentLocusValue, nil - } - } - - return false, locusValue.LocusValue{}, nil - } - - locusValueFound, locusValueObject, err := getLocusValue() - if (err != nil) { return false, "", "", false, err } - if (locusValueFound == false){ - return false, "", "", false, nil - } - - base1Value := locusValueObject.Base1Value - base2Value := locusValueObject.Base2Value - locusIsPhased := locusValueObject.LocusIsPhased - - return true, base1Value, base2Value, locusIsPhased, nil -} - - //Outputs: // -geneticAnalysis.PersonMonogenicDiseaseInfo: Monogenic disease analysis object // -error @@ -1717,7 +1759,7 @@ func getPersonMonogenicDiseaseAnalysis(inputGenomesWithMetadataList []prepareRaw variantRSID := variantObject.VariantRSID - basePairValueFound, base1Value, base2Value, locusIsPhased, err := getGenomeLocusBasePair(genomeMap, variantRSID) + basePairValueFound, base1Value, base2Value, locusIsPhased, _, err := getLocusValueFromGenomeMap(true, genomeMap, variantRSID) if (err != nil) { return emptyDiseaseInfoObject, err } if (basePairValueFound == false){ @@ -1777,7 +1819,7 @@ func getPersonMonogenicDiseaseAnalysis(inputGenomesWithMetadataList []prepareRaw for rsID, _ := range allUniqueRSIDsMap{ - locusValueExists, _, _, locusIsPhased, err := getGenomeLocusBasePair(genomeMap, rsID) + locusValueExists, _, _, locusIsPhased, _, err := getLocusValueFromGenomeMap(true, genomeMap, rsID) if (err != nil) { return emptyDiseaseInfoObject, err } if (locusValueExists == false){ continue @@ -2109,6 +2151,79 @@ func getPersonMonogenicDiseaseAnalysis(inputGenomesWithMetadataList []prepareRaw } +//Outputs: +// -bool: Any loci tested +// -int: Person genome risk score (value between 0-10) +// -int: Person Genome Number of loci tested +// -map[[3]byte]geneticAnalysis.PersonGenomePolygenicDiseaseLocusInfo: Person disease locus info map +// Map Structure: Locus Identifier -> PersonGenomePolygenicDiseaseLocusInfo +// -error +func GetPersonGenomePolygenicDiseaseInfo(diseaseLociList []polygenicDiseases.DiseaseLocus, personLocusValuesMap map[int64]locusValue.LocusValue, lookForLocusAliases bool)(bool, int, int, map[[3]byte]geneticAnalysis.PersonGenomePolygenicDiseaseLocusInfo, error){ + + if (len(personLocusValuesMap) == 0){ + return false, 0, 0, nil, nil + } + + // Map Structure: Locus Identifier -> PersonGenomePolygenicDiseaseLocusInfo + genomeLociInfoMap := make(map[[3]byte]geneticAnalysis.PersonGenomePolygenicDiseaseLocusInfo) + + summedDiseaseRiskWeight := 0 + + minimumPossibleRiskWeightSum := 0 + maximumPossibleRiskWeightSum := 0 + + for _, locusObject := range diseaseLociList{ + + locusRSID := locusObject.LocusRSID + locusRiskWeightsMap := locusObject.RiskWeightsMap + locusOddsRatiosMap := locusObject.OddsRatiosMap + locusMinimumWeight := locusObject.MinimumRiskWeight + locusMaximumWeight := locusObject.MaximumRiskWeight + + locusValueFound, locusBase1Value, locusBase2Value, _, _, err := getLocusValueFromGenomeMap(lookForLocusAliases, personLocusValuesMap, locusRSID) + if (err != nil) { return false, 0, 0, nil, err } + if (locusValueFound == false){ + continue + } + + locusRiskWeight, locusOddsRatioIsKnown, locusOddsRatio, err := getGenomePolygenicDiseaseLocusRiskInfo(locusRiskWeightsMap, locusOddsRatiosMap, locusBase1Value, locusBase2Value) + if (err != nil) { return false, 0, 0, nil, err } + + newLocusInfoObject := geneticAnalysis.PersonGenomePolygenicDiseaseLocusInfo{ + RiskWeight: locusRiskWeight, + OddsRatioIsKnown: locusOddsRatioIsKnown, + } + + if (locusOddsRatioIsKnown == true){ + newLocusInfoObject.OddsRatio = locusOddsRatio + } + + locusIdentifierHex := locusObject.LocusIdentifier + + locusIdentifier, err := encoding.DecodeHexStringTo3ByteArray(locusIdentifierHex) + if (err != nil) { return false, 0, 0, nil, err } + + genomeLociInfoMap[locusIdentifier] = newLocusInfoObject + + minimumPossibleRiskWeightSum += locusMinimumWeight + maximumPossibleRiskWeightSum += locusMaximumWeight + + summedDiseaseRiskWeight += locusRiskWeight + } + + numberOfLociTested := len(genomeLociInfoMap) + if (numberOfLociTested == 0){ + // We have no information about this disease for this genome + return false, 0, 0, nil, nil + } + + diseaseRiskScore, err := helpers.ScaleNumberProportionally(true, summedDiseaseRiskWeight, minimumPossibleRiskWeightSum, maximumPossibleRiskWeightSum, 0, 10) + if (err != nil) { return false, 0, 0, nil, err } + + return true, diseaseRiskScore, numberOfLociTested, genomeLociInfoMap, nil +} + + //Outputs: // -geneticAnalysis.PersonPolygenicDiseaseInfo // -error @@ -2130,96 +2245,33 @@ func getPersonPolygenicDiseaseAnalysis(inputGenomesWithMetadataList []prepareRaw genomeIdentifier := genomeWithMetadataObject.GenomeIdentifier genomeMap := genomeWithMetadataObject.GenomeMap - // Map Structure: Locus Identifier -> PersonGenomePolygenicDiseaseLocusInfo - genomeLociInfoMap := make(map[[3]byte]geneticAnalysis.PersonGenomePolygenicDiseaseLocusInfo) - - minimumPossibleRiskWeightSum := 0 - maximumPossibleRiskWeightSum := 0 - - summedDiseaseRiskWeight := 0 + // This map stores the loci for this disease and does not contain loci which do not belong to this disease + // Map Structure: rsID -> Locus Value + genomeLocusValuesMap := make(map[int64]locusValue.LocusValue) for _, locusObject := range diseaseLociList{ - locusIdentifierHex := locusObject.LocusIdentifier - - locusIdentifier, err := encoding.DecodeHexStringTo3ByteArray(locusIdentifierHex) - if (err != nil) { return emptyDiseaseInfoObject, err } - locusRSID := locusObject.LocusRSID - locusRiskWeightsMap := locusObject.RiskWeightsMap - locusOddsRatiosMap := locusObject.OddsRatiosMap - locusMinimumWeight := locusObject.MinimumRiskWeight - locusMaximumWeight := locusObject.MaximumRiskWeight - basePairValueFound, locusBase1Value, locusBase2Value, _, err := getGenomeLocusBasePair(genomeMap, locusRSID) + locusValueFound, _, _, _, locusValueObject, err := getLocusValueFromGenomeMap(true, genomeMap, locusRSID) if (err != nil) { return emptyDiseaseInfoObject, err } - if (basePairValueFound == false){ + if (locusValueFound == false){ continue } - //Outputs: - // -int: Genome disease locus risk weight - // -bool: Genome disease locus odds ratio known - // -float64: Genome disease locus odds ratio - // -error - getGenomeDiseaseLocusRiskInfo := func()(int, bool, float64, error){ - - locusBasePairJoined := locusBase1Value + ";" + locusBase2Value - - riskWeight, exists := locusRiskWeightsMap[locusBasePairJoined] - if (exists == false){ - // This is an unknown base combination - // We will treat it as a 0 risk weight - return 0, true, 1, nil - } - - if (riskWeight == 0){ - return 0, true, 1, nil - } - - oddsRatio, exists := locusOddsRatiosMap[locusBasePairJoined] - if (exists == false){ - return riskWeight, false, 0, nil - } - - return riskWeight, true, oddsRatio, nil - } - - locusRiskWeight, locusOddsRatioIsKnown, locusOddsRatio, err := getGenomeDiseaseLocusRiskInfo() - if (err != nil) { return emptyDiseaseInfoObject, err } - - newLocusInfoObject := geneticAnalysis.PersonGenomePolygenicDiseaseLocusInfo{ - LocusBase1: locusBase1Value, - LocusBase2: locusBase2Value, - RiskWeight: locusRiskWeight, - OddsRatioIsKnown: locusOddsRatioIsKnown, - } - - if (locusOddsRatioIsKnown == true){ - newLocusInfoObject.OddsRatio = locusOddsRatio - } - - genomeLociInfoMap[locusIdentifier] = newLocusInfoObject - - minimumPossibleRiskWeightSum += locusMinimumWeight - maximumPossibleRiskWeightSum += locusMaximumWeight - - summedDiseaseRiskWeight += locusRiskWeight + genomeLocusValuesMap[locusRSID] = locusValueObject } - numberOfLociTested := len(genomeLociInfoMap) - - if (numberOfLociTested == 0){ - // We have no information about this disease for this genome + anyLociTested, personDiseaseRiskScore, genomeNumberOfLociTested, genomeLociInfoMap, err := GetPersonGenomePolygenicDiseaseInfo(diseaseLociList, genomeLocusValuesMap, false) + if (err != nil) { return emptyDiseaseInfoObject, err } + if (anyLociTested == false){ continue } - diseaseRiskScore, err := helpers.ScaleNumberProportionally(true, summedDiseaseRiskWeight, minimumPossibleRiskWeightSum, maximumPossibleRiskWeightSum, 0, 10) - if (err != nil) { return emptyDiseaseInfoObject, err } - newDiseaseInfoObject := geneticAnalysis.PersonGenomePolygenicDiseaseInfo{ - NumberOfLociTested: numberOfLociTested, - RiskScore: diseaseRiskScore, + NumberOfLociTested: genomeNumberOfLociTested, + RiskScore: personDiseaseRiskScore, + LocusValuesMap: genomeLocusValuesMap, LociInfoMap: genomeLociInfoMap, } @@ -2354,19 +2406,13 @@ func getPersonTraitAnalysis(inputGenomesWithMetadataList []prepareRawGenomes.Gen for _, locusRSID := range traitLociList{ - locusBasePairKnown, locusBase1, locusBase2, locusIsPhased, err := getGenomeLocusBasePair(genomeMap, locusRSID) + locusBasePairKnown, _, _, _, locusValueObject, err := getLocusValueFromGenomeMap(true, genomeMap, locusRSID) if (err != nil) { return emptyPersonTraitInfo, err } if (locusBasePairKnown == false){ continue } - newLocusValue := locusValue.LocusValue{ - LocusIsPhased: locusIsPhased, - Base1Value: locusBase1, - Base2Value: locusBase2, - } - - genomeLocusValuesMap[locusRSID] = newLocusValue + genomeLocusValuesMap[locusRSID] = locusValueObject } // This map contains the trait outcome scores for the genome @@ -2390,52 +2436,7 @@ func getPersonTraitAnalysis(inputGenomesWithMetadataList []prepareRawGenomes.Gen ruleLociList := ruleObject.LociList - // Outputs: - // -bool: Genome passes rule is known - // -bool: Genome passes rule - // -error - getGenomePassesRuleBool := func()(bool, bool, error){ - - // We check to see if genome passes all rule loci - // We consider a rule Known if the genome either passes all loci, or fails to pass 1 locus - // We consider a rule Unknown if any loci are unknown, and all loci which are known pass the rule - - anyLocusIsUnknown := false - - for _, locusObject := range ruleLociList{ - - locusRSID := locusObject.LocusRSID - - locusBasePairKnown, locusBase1, locusBase2, _, err := getGenomeLocusBasePair(genomeMap, locusRSID) - if (err != nil) { return false, false, err } - if (locusBasePairKnown == false){ - anyLocusIsUnknown = true - continue - } - - locusBasePairJoined := locusBase1 + ";" + locusBase2 - - locusBasePairsList := locusObject.BasePairsList - - genomePassesRuleLocus := slices.Contains(locusBasePairsList, locusBasePairJoined) - if (genomePassesRuleLocus == false){ - // The genome has failed to pass a single rule locus, thus, the rule is not passed - return true, false, nil - } - } - - if (anyLocusIsUnknown == true){ - // The rule is not passed, but it's status is unknown - // There were no rules which were known not to pass - return false, false, nil - } - - // All rules were passed - - return true, true, nil - } - - genomePassesRuleIsKnown, genomePassesRule, err := getGenomePassesRuleBool() + genomePassesRuleIsKnown, genomePassesRule, err := GetGenomePassesTraitRuleStatus(ruleLociList, genomeMap, false) if (err != nil) { return emptyPersonTraitInfo, err } if (genomePassesRuleIsKnown == false){ continue @@ -2543,3 +2544,138 @@ func getPersonTraitAnalysis(inputGenomesWithMetadataList []prepareRawGenomes.Gen } +//Outputs: +// -int: Base pair disease locus risk weight +// -bool: Base pair disease locus odds ratio known +// -float64: Base pair disease locus odds ratio +// -error +func getGenomePolygenicDiseaseLocusRiskInfo(locusRiskWeightsMap map[string]int, locusOddsRatiosMap map[string]float64, locusBase1Value string, locusBase2Value string)(int, bool, float64, error){ + + locusBasePairJoined := locusBase1Value + ";" + locusBase2Value + + riskWeight, exists := locusRiskWeightsMap[locusBasePairJoined] + if (exists == false){ + // This is an unknown base combination + // We will treat it as a 0 risk weight + return 0, true, 1, nil + } + + if (riskWeight == 0){ + return 0, true, 1, nil + } + + oddsRatio, exists := locusOddsRatiosMap[locusBasePairJoined] + if (exists == false){ + return riskWeight, false, 0, nil + } + + return riskWeight, true, oddsRatio, nil +} + +// This function checks to see if a genome will pass a trait rule +// Outputs: +// -bool: Genome passes trait rule status is known +// -bool: Genome passes trait rule +// -error +func GetGenomePassesTraitRuleStatus(ruleLociList []traits.RuleLocus, genomeMap map[int64]locusValue.LocusValue, checkForAliases bool)(bool, bool, error){ + + // We check to see if genome passes all rule loci + // To pass a rule, all of the rule's loci must be passed by the provided genome + // We consider a rule Known if the genome either passes all loci, or fails to pass 1 locus + // We consider a rule Unknown if any loci are unknown, and there are no rules which are known not to be passed + + anyLocusIsUnknown := false + + for _, locusObject := range ruleLociList{ + + locusRSID := locusObject.LocusRSID + + locusBasePairKnown, locusBase1, locusBase2, _, _, err := getLocusValueFromGenomeMap(checkForAliases, genomeMap, locusRSID) + if (err != nil) { return false, false, err } + if (locusBasePairKnown == false){ + anyLocusIsUnknown = true + // We keep searching to see if any of the rule's loci are known to not pass + continue + } + + locusBasePairJoined := locusBase1 + ";" + locusBase2 + + locusBasePairsList := locusObject.BasePairsList + + genomePassesRuleLocus := slices.Contains(locusBasePairsList, locusBasePairJoined) + if (genomePassesRuleLocus == false){ + // The genome has failed to pass a single rule locus, thus, the rule is not passed + return true, false, nil + } + } + + if (anyLocusIsUnknown == true){ + // The rule is not passed, but it's status is unknown + // There were no rules which were known not to pass + return false, false, nil + } + + // All rules were passed + + return true, true, nil +} + + +// This function will retrieve the base pair of the locus from the input genome map +// We use this function because each rsID has aliases, so we must sometimes check those aliases to find locus values +// +// Outputs: +// -bool: Valid base pair value found +// -string: Base 1 Value (Nucleotide base for the SNP) +// -string: Base 2 Value (Nucleotide base for the SNP) +// -bool: Locus base pair is phased +// -locusValue.LocusValue +// -error +func getLocusValueFromGenomeMap(checkForAliases bool, inputGenomeMap map[int64]locusValue.LocusValue, locusRSID int64)(bool, string, string, bool, locusValue.LocusValue, error){ + + // Outputs: + // -bool: Locus value found + // -locusValue.LocusValue + // -error + getLocusValue := func()(bool, locusValue.LocusValue, error){ + + currentLocusValue, exists := inputGenomeMap[locusRSID] + if (exists == true){ + return true, currentLocusValue, nil + } + + if (checkForAliases == false){ + return false, locusValue.LocusValue{}, nil + } + + // We check for aliases + + anyAliasesExist, rsidAliasesList, err := locusMetadata.GetRSIDAliases(locusRSID) + if (err != nil) { return false, locusValue.LocusValue{}, err } + if (anyAliasesExist == false){ + return false, locusValue.LocusValue{}, nil + } + + for _, rsidAlias := range rsidAliasesList{ + + currentLocusValue, exists := inputGenomeMap[rsidAlias] + if (exists == true){ + return true, currentLocusValue, nil + } + } + + return false, locusValue.LocusValue{}, nil + } + + locusValueFound, locusValueObject, err := getLocusValue() + if (err != nil) { return false, "", "", false, locusValue.LocusValue{}, err } + if (locusValueFound == false){ + return false, "", "", false, locusValue.LocusValue{}, nil + } + + base1Value := locusValueObject.Base1Value + base2Value := locusValueObject.Base2Value + locusIsPhased := locusValueObject.LocusIsPhased + + return true, base1Value, base2Value, locusIsPhased, locusValueObject, nil +} diff --git a/internal/genetics/geneticAnalysis/geneticAnalysis.go b/internal/genetics/geneticAnalysis/geneticAnalysis.go index fde620f..1bfb17b 100644 --- a/internal/genetics/geneticAnalysis/geneticAnalysis.go +++ b/internal/genetics/geneticAnalysis/geneticAnalysis.go @@ -98,6 +98,11 @@ type PersonGenomePolygenicDiseaseInfo struct{ // This should be len(LociInfoList) NumberOfLociTested int + // This map contains the locus values for the genome for this trait + // If an locus's entry doesn't exist, its value is unknown + // Map Structure: Locus rsID -> Locus Value + LocusValuesMap map[int64]locusValue.LocusValue + // This is total risk score for this disease for the person's genome // This is a number between 1-10 RiskScore int @@ -110,11 +115,6 @@ type PersonGenomePolygenicDiseaseInfo struct{ type PersonGenomePolygenicDiseaseLocusInfo struct{ - // The person's genome locus base pair value for this variant's locus - // Example: "G", "C" - LocusBase1 string - LocusBase2 string - // This is the risk weight that this person's genome has for this variant // A higher risk weight means more risk of getting the disease RiskWeight int @@ -141,10 +141,10 @@ type PersonTraitInfo struct{ type PersonGenomeTraitInfo struct{ - // This should be len(RulesList) + // This should be len(GenomePassesRulesMap) NumberOfRulesTested int - // This map contains the locus values for the genome + // This map contains the locus values for the genome for this trait // If an locus's entry doesn't exist, its value is unknown // Map Structure: Locus rsID -> Locus Value LocusValuesMap map[int64]locusValue.LocusValue @@ -262,33 +262,38 @@ type OffspringGenomePairPolygenicDiseaseInfo struct{ // This should be len(DiseaseLociList) NumberOfLociTested int - // A number between 1-10 representing the offspring's risk + // A number between 1-10 representing the offspring's average risk score // 1 == lowest risk, 10 == highest risk - OffspringRiskScore int + OffspringAverageRiskScore int // A map of the offspring's locus information // Map Structure: Locus Identifier -> OffspringPolygenicDiseaseLocusInfo LociInfoMap map[[3]byte]OffspringPolygenicDiseaseLocusInfo + + // This is a list of prospective offspring risk scores + // This is useful for plotting on a graph to understand the standard deviation of risk + SampleOffspringRiskScoresList []int } type OffspringPolygenicDiseaseLocusInfo struct{ - // This is the offspring's risk weight for this locus value + // This is the offspring's average risk weight for this locus value // A higher weight means a higher risk of the disease - OffspringRiskWeight int + OffspringAverageRiskWeight int + // This is true if any of the 100 prospective offspring had a known odds ratio for this locus OffspringOddsRatioIsKnown bool // This value represent's the offspring's average odds ratio for the disease locus // A value <1 denotes a lesser risk, a value >1 denotes an increased risk - OffspringOddsRatio float64 + OffspringAverageOddsRatio float64 - // This is the sum of weights for the loci which have no odds ratios + // This is the average of the sum of weights for the loci which have no odds ratios for each prospective offspring // We do this to understand what effect those loci are having on the odds ratio // If the sum is <0, we say the ratio is probably lower // If the sum is >0, we say the ratio is probably higher - OffspringUnknownOddsRatiosWeightSum int + OffspringAverageUnknownOddsRatiosWeightSum int } diff --git a/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go b/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go index 4636042..df47362 100644 --- a/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go +++ b/internal/genetics/readGeneticAnalysis/readGeneticAnalysis.go @@ -482,118 +482,119 @@ func GetOffspringMonogenicDiseaseVariantInfoFromGeneticAnalysis(coupleAnalysisOb //Outputs: -// -bool: Polygenic Disease Risk Score known -// -int: Disease risk score -// -string: Disease risk score formatted (has "/10" suffix) +// -bool: Polygenic Disease Risk Score known (any loci values exist) +// -int: Person Disease risk score +// -string: Person Disease risk score formatted (has "/10" suffix) +// -map[int]locusValue.LocusValue: Person locus values map // -int: Number of loci tested // -bool: Conflict exists // -error -func GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis, diseaseName string, genomeIdentifier [16]byte)(bool, int, string, int, bool, error){ +func GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnalysis, diseaseName string, genomeIdentifier [16]byte)(bool, int, string, map[int64]locusValue.LocusValue, int, bool, error){ personPolygenicDiseasesMap := personAnalysisObject.PolygenicDiseasesMap personPolygenicDiseaseInfo, exists := personPolygenicDiseasesMap[diseaseName] if (exists == false){ - return false, 0, "", 0, false, nil + return false, 0, "", nil, 0, false, nil } personPolygenicDiseaseInfoMap := personPolygenicDiseaseInfo.PolygenicDiseaseInfoMap genomePolygenicDiseaseInfo, exists := personPolygenicDiseaseInfoMap[genomeIdentifier] if (exists == false){ - return false, 0, "", 0, false, nil + return false, 0, "", nil, 0, false, nil } conflictExists := personPolygenicDiseaseInfo.ConflictExists personDiseaseRiskScore := genomePolygenicDiseaseInfo.RiskScore - numberOfLociTested := genomePolygenicDiseaseInfo.NumberOfLociTested - personDiseaseRiskScoreString := helpers.ConvertIntToString(personDiseaseRiskScore) personDiseaseRiskScoreFormatted := personDiseaseRiskScoreString + "/10" - return true, personDiseaseRiskScore, personDiseaseRiskScoreFormatted, numberOfLociTested, conflictExists, nil + personLocusValuesMap := genomePolygenicDiseaseInfo.LocusValuesMap + + numberOfLociTested := genomePolygenicDiseaseInfo.NumberOfLociTested + + return true, personDiseaseRiskScore, personDiseaseRiskScoreFormatted, personLocusValuesMap, numberOfLociTested, conflictExists, nil } //Outputs: // -bool: Offspring Disease Risk Score known -// -int: Disease risk score -// -string: Disease risk score formatted (has "/10" suffix) +// -int: Offspring average disease risk score +// -string: Offspring Disease average risk score formatted (has "/10" suffix) +// -[]int: Sample Offspring Risk Scores List // -int: Number of loci tested // -bool: Conflict exists // -error -func GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject geneticAnalysis.CoupleAnalysis, diseaseName string, genomePairIdentifier [32]byte)(bool, int, string, int, bool, error){ +func GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject geneticAnalysis.CoupleAnalysis, diseaseName string, genomePairIdentifier [32]byte)(bool, int, string, []int, int, bool, error){ couplePolygenicDiseasesMap := coupleAnalysisObject.PolygenicDiseasesMap couplePolygenicDiseaseInfo, exists := couplePolygenicDiseasesMap[diseaseName] if (exists == false){ - return false, 0, "", 0, false, nil + return false, 0, "", nil, 0, false, nil } polygenicDiseaseInfoMap := couplePolygenicDiseaseInfo.PolygenicDiseaseInfoMap genomePairPolygenicDiseaseInfo, exists := polygenicDiseaseInfoMap[genomePairIdentifier] if (exists == false){ - return false, 0, "", 0, false, nil + return false, 0, "", nil, 0, false, nil } conflictExists := couplePolygenicDiseaseInfo.ConflictExists numberOfLociTested := genomePairPolygenicDiseaseInfo.NumberOfLociTested - offspringRiskScore := genomePairPolygenicDiseaseInfo.OffspringRiskScore + offspringAverageRiskScore := genomePairPolygenicDiseaseInfo.OffspringAverageRiskScore - offspringRiskScoreString := helpers.ConvertIntToString(offspringRiskScore) + offspringAverageRiskScoreString := helpers.ConvertIntToString(offspringAverageRiskScore) - offspringRiskScoreFormatted := offspringRiskScoreString + "/10" + offspringAverageRiskScoreFormatted := offspringAverageRiskScoreString + "/10" - return true, offspringRiskScore, offspringRiskScoreFormatted, numberOfLociTested, conflictExists, nil + sampleOffspringRiskScoresList := genomePairPolygenicDiseaseInfo.SampleOffspringRiskScoresList + + return true, offspringAverageRiskScore, offspringAverageRiskScoreFormatted, sampleOffspringRiskScoresList, numberOfLociTested, conflictExists, nil } //Outputs: // -bool: Risk Weight and base pair known // -int: Locus risk weight -// -string: Locus base 1 -// -string: Locus base 2 // -bool: Locus odds ratio known // -float64: Locus odds ratio // -string: Locus odds ratio formatted (with x suffix) // -error -func GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(personAnalyisObject geneticAnalysis.PersonAnalysis, diseaseName string, locusIdentifier [3]byte, genomeIdentifier [16]byte)(bool, int, string, string, bool, float64, string, error){ +func GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(personAnalyisObject geneticAnalysis.PersonAnalysis, diseaseName string, locusIdentifier [3]byte, genomeIdentifier [16]byte)(bool, int, bool, float64, string, error){ personPolygenicDiseasesMap := personAnalyisObject.PolygenicDiseasesMap personPolygenicDiseaseMap, exists := personPolygenicDiseasesMap[diseaseName] if (exists == false){ - return false, 0, "", "", false, 0, "", nil + return false, 0, false, 0, "", nil } personPolygenicDiseaseInfoMap := personPolygenicDiseaseMap.PolygenicDiseaseInfoMap personGenomePolygenicDiseaseInfo, exists := personPolygenicDiseaseInfoMap[genomeIdentifier] if (exists == false){ - return false, 0, "", "", false, 0, "", nil + return false, 0, false, 0, "", nil } genomeLociInfoMap := personGenomePolygenicDiseaseInfo.LociInfoMap locusInfoObject, exists := genomeLociInfoMap[locusIdentifier] if (exists == false){ - return false, 0, "", "", false, 0, "", nil + return false, 0, false, 0, "", nil } locusRiskWeight := locusInfoObject.RiskWeight - locusBase1 := locusInfoObject.LocusBase1 - locusBase2 := locusInfoObject.LocusBase2 - locusOddsRatioIsKnown := locusInfoObject.OddsRatioIsKnown if (locusOddsRatioIsKnown == false){ - return true, locusRiskWeight, locusBase1, locusBase2, false, 0, "", nil + return true, locusRiskWeight, false, 0, "", nil } locusOddsRatio := locusInfoObject.OddsRatio @@ -602,7 +603,7 @@ func GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(personAnalyisObject g locusOddsRatioFormatted := genomeLocusOddsRatioString + "x" - return true, locusRiskWeight, locusBase1, locusBase2, true, locusOddsRatio, locusOddsRatioFormatted, nil + return true, locusRiskWeight, true, locusOddsRatio, locusOddsRatioFormatted, nil } //Outputs: @@ -635,40 +636,40 @@ func GetOffspringPolygenicDiseaseLocusInfoFromGeneticAnalysis(coupleAnalysisObje return false, 0, false, 0, "", nil } - offspringRiskWeight := locusInfoObject.OffspringRiskWeight + offspringAverageRiskWeight := locusInfoObject.OffspringAverageRiskWeight offspringOddsRatioIsKnown := locusInfoObject.OffspringOddsRatioIsKnown if (offspringOddsRatioIsKnown == false){ - return true, offspringRiskWeight, false, 0, "", nil + return true, offspringAverageRiskWeight, false, 0, "", nil } - offspringOddsRatio := locusInfoObject.OffspringOddsRatio + offspringAverageOddsRatio := locusInfoObject.OffspringAverageOddsRatio getOddsRatioFormatted := func()string{ - offspringUnknownOddsRatiosWeightSum := locusInfoObject.OffspringUnknownOddsRatiosWeightSum + offspringAverageUnknownOddsRatiosWeightSum := locusInfoObject.OffspringAverageUnknownOddsRatiosWeightSum - offspringOddsRatioString := helpers.ConvertFloat64ToStringRounded(offspringOddsRatio, 2) + offspringAverageOddsRatioString := helpers.ConvertFloat64ToStringRounded(offspringAverageOddsRatio, 2) - if (offspringUnknownOddsRatiosWeightSum == 0){ - result := offspringOddsRatioString + "x" + if (offspringAverageUnknownOddsRatiosWeightSum == 0){ + result := offspringAverageOddsRatioString + "x" return result } - if (offspringUnknownOddsRatiosWeightSum < 0){ - result := "<" + offspringOddsRatioString + "x" + if (offspringAverageUnknownOddsRatiosWeightSum < 0){ + result := "<" + offspringAverageOddsRatioString + "x" return result } - // offspringUnknownOddsRatiosWeightSum > 0 - result := offspringOddsRatioString + "x+" + // offspringAverageUnknownOddsRatiosWeightSum > 0 + result := offspringAverageOddsRatioString + "x+" return result } oddsRatioFormatted := getOddsRatioFormatted() - return true, offspringRiskWeight, true, offspringOddsRatio, oddsRatioFormatted, nil + return true, offspringAverageRiskWeight, true, offspringAverageOddsRatio, oddsRatioFormatted, nil } //Outputs: @@ -858,7 +859,7 @@ func VerifyPersonGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnal for _, genomeIdentifier := range allGenomeIdentifiersList{ - _, _, _, _, _, err := GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject, diseaseName, genomeIdentifier) + _, _, _, _, _, _, err := GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(personAnalysisObject, diseaseName, genomeIdentifier) if (err != nil) { return err } } @@ -873,7 +874,7 @@ func VerifyPersonGeneticAnalysis(personAnalysisObject geneticAnalysis.PersonAnal for _, genomeIdentifier := range allGenomeIdentifiersList{ - _, _, _, _, _, _, _, err := GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(personAnalysisObject, diseaseName, locusIdentifier, genomeIdentifier) + _, _, _, _, _, err := GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(personAnalysisObject, diseaseName, locusIdentifier, genomeIdentifier) if (err != nil) { return err } } } @@ -970,7 +971,7 @@ func VerifyCoupleGeneticAnalysis(coupleAnalysisObject geneticAnalysis.CoupleAnal for _, genomePairIdentifier := range allGenomePairIdentifiersList{ - _, _, _, _, _, err := GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject, diseaseName, genomePairIdentifier) + _, _, _, _, _, _, err := GetOffspringPolygenicDiseaseInfoFromGeneticAnalysis(coupleAnalysisObject, diseaseName, genomePairIdentifier) if (err != nil) { return err } } diff --git a/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.messagepack b/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.messagepack index 6773a86..2de4ccb 100644 Binary files a/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.messagepack and b/internal/genetics/sampleAnalyses/SampleCoupleAnalysis.messagepack differ diff --git a/internal/genetics/sampleAnalyses/SamplePerson1Analysis.messagepack b/internal/genetics/sampleAnalyses/SamplePerson1Analysis.messagepack index bde2a22..3748396 100644 Binary files a/internal/genetics/sampleAnalyses/SamplePerson1Analysis.messagepack and b/internal/genetics/sampleAnalyses/SamplePerson1Analysis.messagepack differ diff --git a/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack b/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack index 114c1bd..bf99e65 100644 Binary files a/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack and b/internal/genetics/sampleAnalyses/SamplePerson2Analysis.messagepack differ diff --git a/internal/profiles/calculatedAttributes/calculatedAttributes.go b/internal/profiles/calculatedAttributes/calculatedAttributes.go index 0760de2..a551024 100644 --- a/internal/profiles/calculatedAttributes/calculatedAttributes.go +++ b/internal/profiles/calculatedAttributes/calculatedAttributes.go @@ -19,6 +19,7 @@ import "seekia/internal/desires/myMateDesires" import "seekia/internal/encoding" import "seekia/internal/genetics/companyAnalysis" import "seekia/internal/genetics/createGeneticAnalysis" +import "seekia/internal/genetics/locusValue" import "seekia/internal/genetics/myChosenAnalysis" import "seekia/internal/genetics/readGeneticAnalysis" import "seekia/internal/helpers" @@ -677,12 +678,10 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA for _, diseaseObject := range polygenicDiseaseObjectsList{ - diseaseLociList := diseaseObject.LociList + // Map Structure: Locus rsID -> Locus Value + userDiseaseLocusValuesMap := make(map[int64]locusValue.LocusValue) - userRiskWeightSum := 0 - userMinimumPossibleRiskWeightSum := 0 - userMaximumPossibleRiskWeightSum := 0 - userNumberOfLociTested := 0 + diseaseLociList := diseaseObject.LociList for _, locusObject := range diseaseLociList{ @@ -690,11 +689,7 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA locusRSIDString := helpers.ConvertInt64ToString(locusRSID) - locusRiskWeightsMap := locusObject.RiskWeightsMap - locusMinimumRiskWeight := locusObject.MinimumRiskWeight - locusMaximumRiskWeight := locusObject.MaximumRiskWeight - - locusValueAttributeName := "LocusValue_rs" + locusRSIDString + locusValueAttributeName := "LocusValue_rs" + locusRSIDString userLocusBasePairExists, _, userLocusBasePair, err := getProfileAttributesFunction(locusValueAttributeName) if (err != nil) { return false, 0, "", err } @@ -702,30 +697,30 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA continue } - userNumberOfLociTested += 1 - - userMinimumPossibleRiskWeightSum += locusMinimumRiskWeight - userMaximumPossibleRiskWeightSum += locusMaximumRiskWeight - - userLocusRiskWeight, exists := locusRiskWeightsMap[userLocusBasePair] - if (exists == false){ - // We do not know the risk weight for this base pair - // We treat this as a 0 risk weight - } else { - userRiskWeightSum += userLocusRiskWeight + userLocusBase1, userLocusBase2, semicolonFound := strings.Cut(userLocusBasePair, ";") + if (semicolonFound == false){ + return false, 0, "", errors.New("Database corrupt: Contains profile with invalid " + locusValueAttributeName + " value: " + userLocusBasePair) } + + userLocusValue := locusValue.LocusValue{ + Base1Value: userLocusBase1, + Base2Value: userLocusBase2, + //TODO: Share LocusIsPhased information in user profiles and retrieve it into this value + LocusIsPhased: false, + } + + userDiseaseLocusValuesMap[locusRSID] = userLocusValue } - if (userNumberOfLociTested == 0){ + anyLocusTested, userDiseaseRiskScore, _, _, err := createGeneticAnalysis.GetPersonGenomePolygenicDiseaseInfo(diseaseLociList, userDiseaseLocusValuesMap, true) + if (err != nil) { return false, 0, "", err } + if (anyLocusTested == false){ continue } numberOfDiseasesTested += 1 - userDiseaseRiskScore, err := helpers.ScaleNumberProportionally(true, userRiskWeightSum, userMinimumPossibleRiskWeightSum, userMaximumPossibleRiskWeightSum, 0, 100) - if (err != nil) { return false, 0, "", err } - - userRiskScoreFraction := float64(userDiseaseRiskScore)/float64(100) + userRiskScoreFraction := float64(userDiseaseRiskScore)/float64(10) allDiseasesAverageRiskScoreNumerator += userRiskScoreFraction } @@ -787,35 +782,20 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA diseaseName := diseaseObject.DiseaseName diseaseLociList := diseaseObject.LociList - offspringRiskWeightSum := 0 - offspringMinimumPossibleRiskWeightSum := 0 - offspringMaximumPossibleRiskWeightSum := 0 - offspringNumberOfLociTested := 0 + _, _, _, myDiseaseLocusValuesMap, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(myGeneticAnalysisObject, diseaseName, myGenomeIdentifier) + if (err != nil) { return false, 0, "", err } + + // Map Structure: rsID -> Locus Value + userDiseaseLocusValuesMap := make(map[int64]locusValue.LocusValue) for _, locusObject := range diseaseLociList{ - locusIdentifierHex := locusObject.LocusIdentifier - - locusIdentifier, err := encoding.DecodeHexStringTo3ByteArray(locusIdentifierHex) - if (err != nil) { return false, 0, "", err } - locusRSID := locusObject.LocusRSID locusRSIDString := helpers.ConvertInt64ToString(locusRSID) - locusRiskWeightsMap := locusObject.RiskWeightsMap - locusOddsRatiosMap := locusObject.OddsRatiosMap - locusMinimumRiskWeight := locusObject.MinimumRiskWeight - locusMaximumRiskWeight := locusObject.MaximumRiskWeight - locusValueAttributeName := "LocusValue_rs" + locusRSIDString - myLocusInfoIsKnown, _, myLocusBase1, myLocusBase2, _, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(myGeneticAnalysisObject, diseaseName, locusIdentifier, myGenomeIdentifier) - if (err != nil) { return false, 0, "", err } - if (myLocusInfoIsKnown == false){ - continue - } - userLocusBasePairExists, _, userLocusBasePair, err := getProfileAttributesFunction(locusValueAttributeName) if (err != nil) { return false, 0, "", err } if (userLocusBasePairExists == false){ @@ -827,27 +807,25 @@ func GetAnyProfileAttributeIncludingCalculated(attributeName string, getProfileA return false, 0, "", errors.New("GetAnyProfileAttributeIncludingCalculated called with profile containing invalid " + locusValueAttributeName + ": " + userLocusBasePair) } - offspringLocusRiskWeight, _, _, _, err := createGeneticAnalysis.GetOffspringPolygenicDiseaseLocusInfo(locusRiskWeightsMap, locusOddsRatiosMap, myLocusBase1, myLocusBase2, userLocusBase1, userLocusBase2) - if (err != nil) { return false, 0, "", err } - - offspringNumberOfLociTested += 1 - - offspringMinimumPossibleRiskWeightSum += locusMinimumRiskWeight - offspringMaximumPossibleRiskWeightSum += locusMaximumRiskWeight - - offspringRiskWeightSum += offspringLocusRiskWeight + newLocusValue := locusValue.LocusValue{ + Base1Value: userLocusBase1, + Base2Value: userLocusBase2, + //TODO: Share locusIsPhased information in user profiles are put it here + LocusIsPhased: false, + } + + userDiseaseLocusValuesMap[locusRSID] = newLocusValue } - if (offspringNumberOfLociTested == 0){ + anyLocusValuesTested, offspringAverageRiskScore, _, err := createGeneticAnalysis.GetOffspringPolygenicDiseaseInfo_Fast(diseaseLociList, myDiseaseLocusValuesMap, userDiseaseLocusValuesMap) + if (err != nil) { return false, 0, "", err } + if (anyLocusValuesTested == false){ continue } numberOfDiseasesTested += 1 - offspringRiskScore, err := helpers.ScaleNumberProportionally(true, offspringRiskWeightSum, offspringMinimumPossibleRiskWeightSum, offspringMaximumPossibleRiskWeightSum, 0, 100) - if (err != nil) { return false, 0, "", err } - - offspringRiskScoreFraction := float64(offspringRiskScore)/float64(100) + offspringRiskScoreFraction := float64(offspringAverageRiskScore)/float64(10) allDiseasesAverageRiskScoreNumerator += offspringRiskScoreFraction } diff --git a/internal/profiles/myProfileExports/myProfileExports.go b/internal/profiles/myProfileExports/myProfileExports.go index 304227a..f5b3345 100644 --- a/internal/profiles/myProfileExports/myProfileExports.go +++ b/internal/profiles/myProfileExports/myProfileExports.go @@ -205,26 +205,19 @@ func UpdateMyExportedProfile(myProfileType string, networkType byte)error{ continue } - diseaseLociList := diseaseObject.LociList + _, _, _, myDiseaseLocusValuesMap, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseInfoFromGeneticAnalysis(myGeneticAnalysisObject, diseaseName, genomeIdentifierToShare) + if (err != nil) { return err } - for _, locusObject := range diseaseLociList{ + for rsID, locusValueObject := range myDiseaseLocusValuesMap{ - locusIdentifierHex := locusObject.LocusIdentifier + rsIDString := helpers.ConvertInt64ToString(rsID) - locusIdentifier, err := encoding.DecodeHexStringTo3ByteArray(locusIdentifierHex) - if (err != nil) { return err } + locusBase1 := locusValueObject.Base1Value + locusBase2 := locusValueObject.Base2Value - locusValueKnown, _, locusBase1, locusBase2, _, _, _, err := readGeneticAnalysis.GetPersonPolygenicDiseaseLocusInfoFromGeneticAnalysis(myGeneticAnalysisObject, diseaseName, locusIdentifier, genomeIdentifierToShare) - if (err != nil) { return err } - if (locusValueKnown == false){ - continue - } + basePairValue := locusBase1 + ";" + locusBase2 - locusRSID := locusObject.LocusRSID - - locusRSIDString := helpers.ConvertInt64ToString(locusRSID) - - profileMap["LocusValue_rs" + locusRSIDString] = locusBase1 + ";" + locusBase2 + profileMap["LocusValue_rs" + rsIDString] = basePairValue } } diff --git a/internal/profiles/userStatistics/userStatistics.go b/internal/profiles/userStatistics/userStatistics.go index 3d4d516..94150e3 100644 --- a/internal/profiles/userStatistics/userStatistics.go +++ b/internal/profiles/userStatistics/userStatistics.go @@ -14,6 +14,7 @@ import "seekia/internal/profiles/calculatedAttributes" import "seekia/internal/profiles/profileStorage" import "seekia/internal/profiles/attributeDisplay" import "seekia/internal/translation" +import "seekia/internal/statisticsDatum" import "slices" import "strings" @@ -21,61 +22,27 @@ import "errors" import "math" -type StatisticsItem struct{ - - // The label for the statistics item - // For a bar chart, this represents the name of an X axis bar. - // For a donut chart, this represents the name of a donut slice - // Example: "Man", "100-200" - // This will never be translated, unless it is the Unknown/No Response item, in which case - // the label will be "Unknown"/"No Response" translated to the user's current app language - Label string - - // This is the formatted, human readable version of the label - // This will be translated into the application language - // Sometimes, the LabelFormatted will be identical to Label - // This does not include units (Example: " days", " users") - // Example: - // -"1000000-2000000" -> "1 million-2 million" - LabelFormatted string - - // The value corresponding to the label - // For a bar chart, this represents the Y axis value for a bar. - // For a donut chart, this represents the value (size) of one of the donut slices - // This will never be translated - // For example, the value could be 500 if 500 men responded Yes. - Value float64 - - // This is the formatted version of the value - // This does not include units (Example: " days", " users") - // Examples: - // -5 -> "5/10" - // -1500000000000 -> "1.5 trillion" - ValueFormatted string -} - - //Outputs: // -int: Number of users analyzed in statistics -// -[]StatisticsItem: Statistics items list (sorted, not grouped) +// -[]statisticsDatum.StatisticsDatum: Statistics datums list (sorted, not grouped) // -bool: Grouping performed -// -[]StatisticsItem: Grouped items list +// -[]statisticsDatum.StatisticsDatum: Grouped datums list // -func(float64)(string, error): Function to format y axis values // -This is used because the values must be passed to the chart code as pure floats, but they must be formatted after to be human readable // -Example: "1000000" -> "1 million" // -error -func GetUserStatisticsItemsLists_BarChart(identityType string, +func GetUserStatisticsDatumsLists_BarChart(identityType string, networkType byte, xAxisAttribute string, xAxisIsNumerical bool, formatXAxisValuesFunction func(string)(string, error), xAxisUnknownLabel string, - yAxisAttribute string)(int, []StatisticsItem, bool, []StatisticsItem, func(float64)(string, error), error){ + yAxisAttribute string)(int, []statisticsDatum.StatisticsDatum, bool, []statisticsDatum.StatisticsDatum, func(float64)(string, error), error){ isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) - return 0, nil, false, nil, nil, errors.New("GetUserStatisticsItemsLists_BarChart called with invalid networkType: " + networkTypeString) + return 0, nil, false, nil, nil, errors.New("GetUserStatisticsDatumsLists_BarChart called with invalid networkType: " + networkTypeString) } getYAxisRoundingPrecision := func()int{ @@ -95,16 +62,16 @@ func GetUserStatisticsItemsLists_BarChart(identityType string, //Outputs: // -int: Total analyzed users - // -[]StatisticsItem: Statistics items list + // -[]statisticsDatum.StatisticsDatum: Statistics datums list // -map[string]int: Response Counts map (X axis attribute response -> Number of y axis responses) // -bool: yAxisIsAverage // -map[string]float64: Response Sums map (X axis attribute response -> all y axis responses summed) (If yAxisIsAverage == true) // -func(float64)(string, error): Function to format statistics values - // -bool: Include a No Response/Unknown value item + // -bool: Include a No Response/Unknown value datum // -This is only needed if at least 1 user did not respond to the X axis attribute on their profile. - // -StatisticsItem: The unknown value item. + // -statisticsDatum.StatisticsDatum: The unknown value datum. // -error - getStatisticsItemsList := func()(int, []StatisticsItem, map[string]int, bool, map[string]float64, func(float64)(string, error), bool, StatisticsItem, error){ + getStatisticsDatumsList := func()(int, []statisticsDatum.StatisticsDatum, map[string]int, bool, map[string]float64, func(float64)(string, error), bool, statisticsDatum.StatisticsDatum, error){ //TODO: Add "Probability Of ..." // This will allow viewing of choice attribute probabilities @@ -113,8 +80,8 @@ func GetUserStatisticsItemsLists_BarChart(identityType string, if (yAxisAttribute == "Number Of Users"){ - totalAnalyzedUsers, statisticsItemsList, responseCountsMap, numberOfUnknownValueUsers, err := getProfileAttributeCountStatisticsItemsList(identityType, networkType, xAxisAttribute, formatXAxisValuesFunction) - if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, numberOfUnknownValueUsers, err := getProfileAttributeCountStatisticsDatumsList(identityType, networkType, xAxisAttribute, formatXAxisValuesFunction) + if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } formatValuesFunction := func(input float64)(string, error){ @@ -126,23 +93,23 @@ func GetUserStatisticsItemsLists_BarChart(identityType string, } if (numberOfUnknownValueUsers == 0){ - return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, false, nil, formatValuesFunction, false, StatisticsItem{}, nil + return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, false, nil, formatValuesFunction, false, statisticsDatum.StatisticsDatum{}, nil } unknownValueFormatted := helpers.ConvertIntToString(numberOfUnknownValueUsers) - unknownItem := StatisticsItem{ + unknownDatum := statisticsDatum.StatisticsDatum{ Label: xAxisUnknownLabel, LabelFormatted: xAxisUnknownLabel, Value: float64(numberOfUnknownValueUsers), ValueFormatted: unknownValueFormatted, } - return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, false, nil, formatValuesFunction, true, unknownItem, nil + return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, false, nil, formatValuesFunction, true, unknownDatum, nil } userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType) - if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } yAxisIsAverage := strings.HasPrefix(yAxisAttribute, "Average ") if (yAxisIsAverage == true){ @@ -152,69 +119,72 @@ func GetUserStatisticsItemsLists_BarChart(identityType string, getAttributeToGetAverageFor := func()string{ attributeTitle := strings.TrimPrefix(yAxisAttribute, "Average ") - - if (attributeTitle == "Wealth"){ - return "WealthInGold" - } - if (attributeTitle == "23andMe Neanderthal Variants"){ - return "23andMe_NeanderthalVariants" - } - if (attributeTitle == "Body Fat"){ - return "BodyFat" - } - if (attributeTitle == "Body Muscle"){ - return "BodyMuscle" - } - if (attributeTitle == "Fruit Rating"){ - return "FruitRating" - } - if (attributeTitle == "Vegetables Rating"){ - return "VegetablesRating" - } - if (attributeTitle == "Nuts Rating"){ - return "NutsRating" - } - if (attributeTitle == "Grains Rating"){ - return "GrainsRating" - } - if (attributeTitle == "Dairy Rating"){ - return "DairyRating" - } - if (attributeTitle == "Seafood Rating"){ - return "SeafoodRating" - } - if (attributeTitle == "Beef Rating"){ - return "BeefRating" - } - if (attributeTitle == "Pork Rating"){ - return "PorkRating" - } - if (attributeTitle == "Poultry Rating"){ - return "PoultryRating" - } - if (attributeTitle == "Eggs Rating"){ - return "EggsRating" - } - if (attributeTitle == "Beans Rating"){ - return "BeansRating" - } - if (attributeTitle == "Alcohol Frequency"){ - return "AlcoholFrequency" - } - if (attributeTitle == "Tobacco Frequency"){ - return "TobaccoFrequency" - } - if (attributeTitle == "Cannabis Frequency"){ - return "CannabisFrequency" - } - if (attributeTitle == "Pets Rating"){ - return "PetsRating" - } - if (attributeTitle == "Dogs Rating"){ - return "DogsRating" - } - if (attributeTitle == "Cats Rating"){ - return "CatsRating" + + switch attributeTitle{ + + case "Wealth":{ + return "WealthInGold" + } + case "23andMe Neanderthal Variants":{ + return "23andMe_NeanderthalVariants" + } + case "Body Fat":{ + return "BodyFat" + } + case "Body Muscle":{ + return "BodyMuscle" + } + case "Fruit Rating":{ + return "FruitRating" + } + case "Vegetables Rating":{ + return "VegetablesRating" + } + case "Nuts Rating":{ + return "NutsRating" + } + case "Grains Rating":{ + return "GrainsRating" + } + case "Dairy Rating":{ + return "DairyRating" + } + case "Seafood Rating":{ + return "SeafoodRating" + } + case "Beef Rating":{ + return "BeefRating" + } + case "Pork Rating":{ + return "PorkRating" + } + case "Poultry Rating":{ + return "PoultryRating" + } + case "Eggs Rating":{ + return "EggsRating" + } + case "Beans Rating":{ + return "BeansRating" + } + case "Alcohol Frequency":{ + return "AlcoholFrequency" + } + case "Tobacco Frequency":{ + return "TobaccoFrequency" + } + case "Cannabis Frequency":{ + return "CannabisFrequency" + } + case "Pets Rating":{ + return "PetsRating" + } + case "Dogs Rating":{ + return "DogsRating" + } + case "Cats Rating":{ + return "CatsRating" + } } return attributeTitle @@ -240,19 +210,19 @@ func GetUserStatisticsItemsLists_BarChart(identityType string, for _, userIdentityHash := range userIdentityHashesToAnalyzeList{ profileFound, getAnyUserAttributeValueFunction, err := getRetrieveAnyAttributeFromUserNewestProfileFunction(userIdentityHash, networkType) - if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } if (profileFound == false){ continue } userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled") - if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } if (userIsDisabled == true){ continue } attributeExists, _, userAttributeToGetAverageForValue, err := getAnyUserAttributeValueFunction(attributeToGetAverageFor) - if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } if (attributeExists == false){ // This user did not respond to the attribute we are getting the average for // We will not add them to the statistics maps @@ -263,11 +233,11 @@ func GetUserStatisticsItemsLists_BarChart(identityType string, userAttributeToGetAverageForValueFloat64, err := helpers.ConvertStringToFloat64(userAttributeToGetAverageForValue) if (err != nil) { - return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, errors.New("Database corrupt: Contains invalid " + userAttributeToGetAverageForValue + " value: " + userAttributeToGetAverageForValue) + return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, errors.New("Database corrupt: Contains invalid " + userAttributeToGetAverageForValue + " value: " + userAttributeToGetAverageForValue) } attributeFound, _, userXAxisAttributeValue, err := getAnyUserAttributeValueFunction(xAxisAttribute) - if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } if (attributeFound == false){ // This user did not respond to the X axis attribute // We calculate the average for users who do not respond and put it in its own category @@ -283,18 +253,18 @@ func GetUserStatisticsItemsLists_BarChart(identityType string, } _, _, formatYAxisValuesFunction, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeToGetAverageFor) - if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } - statisticsItemsList := make([]StatisticsItem, 0, len(responseCountsMap)) + statisticsDatumsList := make([]statisticsDatum.StatisticsDatum, 0, len(responseCountsMap)) for attributeResponse, responsesCount := range responseCountsMap{ attributeResponseFormatted, err := formatXAxisValuesFunction(attributeResponse) - if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } allResponsesSum, exists := responseSumsMap[attributeResponse] if (exists == false){ - return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, errors.New("Response sums map missing attribute value") + return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, errors.New("Response sums map missing attribute value") } averageValue := allResponsesSum/float64(responsesCount) @@ -302,16 +272,16 @@ func GetUserStatisticsItemsLists_BarChart(identityType string, averageValueString := helpers.ConvertFloat64ToStringRounded(averageValue, yAxisRoundingPrecision) averageValueFormatted, err := formatYAxisValuesFunction(averageValueString) - if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } - newStatisticsItem := StatisticsItem{ + newStatisticsDatum := statisticsDatum.StatisticsDatum{ Label: attributeResponse, LabelFormatted: attributeResponseFormatted, Value: averageValue, ValueFormatted: averageValueFormatted, } - statisticsItemsList = append(statisticsItemsList, newStatisticsItem) + statisticsDatumsList = append(statisticsDatumsList, newStatisticsDatum) } // We use this function to format values after grouping, if grouping is needed @@ -326,111 +296,111 @@ func GetUserStatisticsItemsLists_BarChart(identityType string, } if (numberOfUsersWithUnknownXAxisValue == 0){ - return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, false, StatisticsItem{}, nil + return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, false, statisticsDatum.StatisticsDatum{}, nil } unknownResponsesAverage := usersWithUnknownXAxisValueYAxisValuesSum/float64(numberOfUsersWithUnknownXAxisValue) unknownResponsesValueFormatted, err := formatValuesFunction(unknownResponsesAverage) - if (err != nil) { return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, err } + if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, err } - // This item represents the average value for the yAxisAttribute for users who did not respond. + // This datum represents the average value for the yAxisAttribute for users who did not respond. // For example, if the xAxisAttribute is Height, and the yAxisAttribute is AverageWealth, this value // will represent the average wealth for users who did not provide Height on their profile. - unknownStatisticsItem := StatisticsItem{ + unknownStatisticsDatum := statisticsDatum.StatisticsDatum{ Label: xAxisUnknownLabel, LabelFormatted: xAxisUnknownLabel, Value: unknownResponsesAverage, ValueFormatted: unknownResponsesValueFormatted, } - return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, true, unknownStatisticsItem, nil + return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, true, unknownStatisticsDatum, nil } - return 0, nil, nil, false, nil, nil, false, StatisticsItem{}, errors.New("Invalid y-axis attribute: " + yAxisAttribute) + return 0, nil, nil, false, nil, nil, false, statisticsDatum.StatisticsDatum{}, errors.New("Invalid y-axis attribute: " + yAxisAttribute) } - totalAnalyzedUsers, statisticsItemsList, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction, includeUnknownItem, unknownValueItem, err := getStatisticsItemsList() + totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction, includeUnknownDatum, unknownValueDatum, err := getStatisticsDatumsList() if (err != nil) { return 0, nil, false, nil, nil, err } - sortStatisticsItemsList(statisticsItemsList, xAxisIsNumerical) + sortStatisticsDatumsList(statisticsDatumsList, xAxisIsNumerical) - // We now see if we need to group the items in the list together + // We now see if we need to group the datums in the list together // We do this if there are more than 10 categories - if (len(statisticsItemsList) <= 10){ + if (len(statisticsDatumsList) <= 10){ // No grouping needed. We are done. - if (includeUnknownItem == true){ - statisticsItemsList = append(statisticsItemsList, unknownValueItem) + if (includeUnknownDatum == true){ + statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum) } - return totalAnalyzedUsers, statisticsItemsList, false, nil, formatValuesFunction, nil + return totalAnalyzedUsers, statisticsDatumsList, false, nil, formatValuesFunction, nil } - groupedStatisticsItemsList, err := getStatisticsItemsListGrouped(10, statisticsItemsList, xAxisIsNumerical, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction) + groupedStatisticsDatumsList, err := getStatisticsDatumsListGrouped(10, statisticsDatumsList, xAxisIsNumerical, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction) if (err != nil) { return 0, nil, false, nil, nil, err } - if (includeUnknownItem == true){ - statisticsItemsList = append(statisticsItemsList, unknownValueItem) - groupedStatisticsItemsList = append(groupedStatisticsItemsList, unknownValueItem) + if (includeUnknownDatum == true){ + statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum) + groupedStatisticsDatumsList = append(groupedStatisticsDatumsList, unknownValueDatum) } - return totalAnalyzedUsers, statisticsItemsList, true, groupedStatisticsItemsList, formatValuesFunction, nil + return totalAnalyzedUsers, statisticsDatumsList, true, groupedStatisticsDatumsList, formatValuesFunction, nil } //Outputs: // -int: Number of users analyzed in statistics -// -[]StatisticsItem: Statistics items list (sorted, not grouped) +// -[]statisticsDatum.StatisticsDatum: Statistics datums list (sorted, not grouped) // -bool: Grouping performed -// -[]StatisticsItem: Grouped items list +// -[]statisticsDatum.StatisticsDatum: Grouped datums list // -error -func GetUserStatisticsItemsLists_DonutChart(identityType string, +func GetUserStatisticsDatumsLists_DonutChart(identityType string, networkType byte, attributeToAnalyze string, attributeIsNumerical bool, formatAttributeLabelsFunction func(string)(string, error), - unknownLabelTranslated string)(int, []StatisticsItem, bool, []StatisticsItem, error){ + unknownLabelTranslated string)(int, []statisticsDatum.StatisticsDatum, bool, []statisticsDatum.StatisticsDatum, error){ isValid := helpers.VerifyNetworkType(networkType) if (isValid == false){ networkTypeString := helpers.ConvertByteToString(networkType) - return 0, nil, false, nil, errors.New("GetUserStatisticsItemsLists_DonutChart called with invalid networkType: " + networkTypeString) + return 0, nil, false, nil, errors.New("GetUserStatisticsDatumsLists_DonutChart called with invalid networkType: " + networkTypeString) } - totalAnalyzedUsers, statisticsItemsList, attributeCountsMap, numberOfUnknownResponders, err := getProfileAttributeCountStatisticsItemsList(identityType, networkType, attributeToAnalyze, formatAttributeLabelsFunction) + totalAnalyzedUsers, statisticsDatumsList, attributeCountsMap, numberOfUnknownResponders, err := getProfileAttributeCountStatisticsDatumsList(identityType, networkType, attributeToAnalyze, formatAttributeLabelsFunction) if (err != nil) { return 0, nil, false, nil, err } - sortStatisticsItemsList(statisticsItemsList, attributeIsNumerical) + sortStatisticsDatumsList(statisticsDatumsList, attributeIsNumerical) - getUnknownValueItem := func()StatisticsItem{ + getUnknownValueDatum := func()statisticsDatum.StatisticsDatum{ numberOfUnknownRespondersString := helpers.ConvertIntToString(numberOfUnknownResponders) - unknownValueItem := StatisticsItem{ + unknownValueDatum := statisticsDatum.StatisticsDatum{ Label: unknownLabelTranslated, LabelFormatted: unknownLabelTranslated, Value: float64(numberOfUnknownResponders), ValueFormatted: numberOfUnknownRespondersString, } - return unknownValueItem + return unknownValueDatum } - if (len(statisticsItemsList) <= 8){ + if (len(statisticsDatumsList) <= 8){ // No grouping needed. if (numberOfUnknownResponders != 0){ - unknownValueItem := getUnknownValueItem() + unknownValueDatum := getUnknownValueDatum() - statisticsItemsList = append(statisticsItemsList, unknownValueItem) + statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum) } - return totalAnalyzedUsers, statisticsItemsList, false, nil, nil + return totalAnalyzedUsers, statisticsDatumsList, false, nil, nil } formatValuesFunction := func(input float64)(string, error){ @@ -443,35 +413,35 @@ func GetUserStatisticsItemsLists_DonutChart(identityType string, return result, nil } - groupedStatisticsItemsList, err := getStatisticsItemsListGrouped(8, statisticsItemsList, attributeIsNumerical, attributeCountsMap, false, nil, formatValuesFunction) + groupedStatisticsDatumsList, err := getStatisticsDatumsListGrouped(8, statisticsDatumsList, attributeIsNumerical, attributeCountsMap, false, nil, formatValuesFunction) if (err != nil) { return 0, nil, false, nil, err } if (numberOfUnknownResponders != 0){ - unknownValueItem := getUnknownValueItem() + unknownValueDatum := getUnknownValueDatum() - statisticsItemsList = append(statisticsItemsList, unknownValueItem) - groupedStatisticsItemsList = append(groupedStatisticsItemsList, unknownValueItem) + statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum) + groupedStatisticsDatumsList = append(groupedStatisticsDatumsList, unknownValueDatum) } - return totalAnalyzedUsers, statisticsItemsList, true, groupedStatisticsItemsList, nil + return totalAnalyzedUsers, statisticsDatumsList, true, groupedStatisticsDatumsList, nil } -// This function will return a statistics items list of the following format: +// This function will return a statistics datums list of the following format: // "Label": Attribute name (Example: "Male") // "Value": The number of users who responded with the attribute (in this example: "Male") // // All users of provided identityType who are not disabled will be analyzed // -int: Number of analyzed users -// -[]StatisticsItem: Statistics items list (not sorted or grouped) +// -[]statisticsDatum.StatisticsDatum: Statistics datums list (not sorted or grouped) // -map[string]int: Response counts map (Response -> Number of responders) // -int: Number of No Response/Unknown value responders // -error -func getProfileAttributeCountStatisticsItemsList(identityType string, +func getProfileAttributeCountStatisticsDatumsList(identityType string, networkType byte, attributeName string, - formatLabelsFunction func(string)(string, error))(int, []StatisticsItem, map[string]int, int, error){ + formatLabelsFunction func(string)(string, error))(int, []statisticsDatum.StatisticsDatum, map[string]int, int, error){ userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType) if (err != nil) { return 0, nil, nil, 0, err } @@ -509,7 +479,7 @@ func getProfileAttributeCountStatisticsItemsList(identityType string, responseCountsMap[attributeValue] += 1 } - statisticsItemsList := make([]StatisticsItem, 0, len(responseCountsMap)) + statisticsDatumsList := make([]statisticsDatum.StatisticsDatum, 0, len(responseCountsMap)) for attributeResponse, numberOfUsers := range responseCountsMap{ @@ -518,88 +488,88 @@ func getProfileAttributeCountStatisticsItemsList(identityType string, attributeNumberOfUsersString := helpers.ConvertIntToString(numberOfUsers) - newStatisticsItem := StatisticsItem{ + newStatisticsDatum := statisticsDatum.StatisticsDatum{ Label: attributeResponse, LabelFormatted: attributeResponseFormatted, Value: float64(numberOfUsers), ValueFormatted: attributeNumberOfUsersString, } - statisticsItemsList = append(statisticsItemsList, newStatisticsItem) + statisticsDatumsList = append(statisticsDatumsList, newStatisticsDatum) } - return totalAnalyzedUsers, statisticsItemsList, responseCountsMap, numberOfUnknownValueUsers, nil + return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, numberOfUnknownValueUsers, nil } -func sortStatisticsItemsList(inputStatisticsItemsList []StatisticsItem, labelIsNumerical bool){ +func sortStatisticsDatumsList(inputStatisticsDatumsList []statisticsDatum.StatisticsDatum, labelIsNumerical bool){ - if (len(inputStatisticsItemsList) <= 1){ + if (len(inputStatisticsDatumsList) <= 1){ return } if (labelIsNumerical == true){ - // We sort the items by label values in ascending order + // We sort the datums by label values in ascending order // Example: Bar chart columns are ages, in order of youngest to oldest - compareItemsFunction := func(itemA StatisticsItem, itemB StatisticsItem)int{ + compareDatumsFunction := func(datumA statisticsDatum.StatisticsDatum, datumB statisticsDatum.StatisticsDatum)int{ - itemALabel := itemA.Label - itemBLabel := itemB.Label + datumALabel := datumA.Label + datumBLabel := datumB.Label - itemAFloat64, err := helpers.ConvertStringToFloat64(itemALabel) + datumAFloat64, err := helpers.ConvertStringToFloat64(datumALabel) if (err != nil) { - panic("Invalid statistics item: Item Label is not float: " + itemALabel) + panic("Invalid statistics datum: Datum Label is not float: " + datumALabel) } - itemBFloat64, err := helpers.ConvertStringToFloat64(itemBLabel) + datumBFloat64, err := helpers.ConvertStringToFloat64(datumBLabel) if (err != nil) { - panic("Invalid statistics item: Item Label is not float: " + itemBLabel) + panic("Invalid statistics datum: Datum Label is not float: " + datumBLabel) } - if (itemAFloat64 == itemBFloat64){ + if (datumAFloat64 == datumBFloat64){ return 0 } - if (itemAFloat64 < itemBFloat64){ + if (datumAFloat64 < datumBFloat64){ return -1 } return 1 } - slices.SortFunc(inputStatisticsItemsList, compareItemsFunction) + slices.SortFunc(inputStatisticsDatumsList, compareDatumsFunction) return } - // We sort the items by their values in descending order + // We sort the datums by their values in descending order - compareItemsFunction := func(itemA StatisticsItem, itemB StatisticsItem)int{ + compareDatumsFunction := func(datum1 statisticsDatum.StatisticsDatum, datum2 statisticsDatum.StatisticsDatum)int{ - itemAValue := itemA.Value - itemBValue := itemB.Value + datum1Value := datum1.Value + datum2Value := datum2.Value - if (itemAValue == itemBValue){ + if (datum1Value == datum2Value){ return 0 } - if (itemAValue < itemBValue){ + if (datum1Value < datum2Value){ return 1 } return -1 } - slices.SortFunc(inputStatisticsItemsList, compareItemsFunction) + slices.SortFunc(inputStatisticsDatumsList, compareDatumsFunction) } -// This function will group a statistics items list. +// This function will group a statistics datums list. // It will group Labels and their values to fit into a specified number of groups // Example: "1","2","3","4" -> "1-2", "3-4" //Inputs: // -int: Maximum groups to create -// -[]StatisticsItem: Statistics items list to group +// -[]statisticsDatum.StatisticsDatum: Statistics datums list to group // -bool: Label is numerical // -If it is, we will group labels into groups of numbers. // -Otherwise, we will group all categories after the first maximumGroupsToCreate into a group called Other @@ -612,27 +582,27 @@ func sortStatisticsItemsList(inputStatisticsItemsList []StatisticsItem, labelIsN // -Response Sums map (X-axis attribute response -> all Y-axis responses summed) (If yAxisIsAverage == true) // -func(float64)(string, error): This is the function we use to format the values //Outputs: -// -[]StatisticsItem: Grouped statistics items list +// -[]statisticsDatum.StatisticsDatum: Grouped statistics datums list // -error -func getStatisticsItemsListGrouped(maximumGroupsToCreate int, - inputStatisticsItemsList []StatisticsItem, +func getStatisticsDatumsListGrouped(maximumGroupsToCreate int, + inputStatisticsDatumsList []statisticsDatum.StatisticsDatum, labelIsNumerical bool, responseCountsMap map[string]int, valueIsAverage bool, responseSumsMap map[string]float64, - formatValuesFunction func(float64)(string, error))([]StatisticsItem, error){ + formatValuesFunction func(float64)(string, error))([]statisticsDatum.StatisticsDatum, error){ - if (len(inputStatisticsItemsList) <= maximumGroupsToCreate){ - return nil, errors.New("maximumGroupsToCreate is <= length of input statistics items list") + if (len(inputStatisticsDatumsList) <= maximumGroupsToCreate){ + return nil, errors.New("maximumGroupsToCreate is <= length of input statistics datums list") } - // We deep copy the statistics items list to retain the sorted version + // We deep copy the statistics datums list to retain the sorted version // We need to retain both versions because the user can view the raw or grouped data in the GUI - statisticsItemsList := slices.Clone(inputStatisticsItemsList) + statisticsDatumsList := slices.Clone(inputStatisticsDatumsList) // We use this function to get the new value for a group of labels - getGroupValue := func(itemsToCombineList []StatisticsItem)(float64, error){ + getGroupValue := func(datumsToCombineList []statisticsDatum.StatisticsDatum)(float64, error){ // This will count the total number of users who responded with the responses within this group // Example: Labels are "Blue", "Green", this variable will store the number of users who responded with either Blue or Green @@ -642,22 +612,22 @@ func getStatisticsItemsListGrouped(maximumGroupsToCreate int, // We only need to add to this sum if valueIsAverage == true allReponsesSummed := float64(0) - for _, statisticsItem := range itemsToCombineList{ + for _, statisticsDatum := range datumsToCombineList{ - itemLabel := statisticsItem.Label + datumLabel := statisticsDatum.Label - responderCount, exists := responseCountsMap[itemLabel] + responderCount, exists := responseCountsMap[datumLabel] if (exists == false){ - return 0, errors.New("responseCountsMap missing label: " + itemLabel) + return 0, errors.New("responseCountsMap missing label: " + datumLabel) } totalRespondersCount += float64(responderCount) if (valueIsAverage == true){ - yAxisAttributeResponsesSum, exists := responseSumsMap[itemLabel] + yAxisAttributeResponsesSum, exists := responseSumsMap[datumLabel] if (exists == false){ - return 0, errors.New("responseSumsMap missing label: " + itemLabel) + return 0, errors.New("responseSumsMap missing label: " + datumLabel) } allReponsesSummed += yAxisAttributeResponsesSum } @@ -670,9 +640,9 @@ func getStatisticsItemsListGrouped(maximumGroupsToCreate int, // The value is an average // We need to find the average for all of the user responses for the labels in the input list - // The Values in the inputStatisticsItemsList are averages + // The Values in the inputStatisticsDatumsList are averages // We can't average out the averages, because that will not give us the true average - // We have to use the original sums for all group items and average them + // We have to use the original sums for all group datums and average them if (totalRespondersCount == 0){ return 0, errors.New("totalRespondersCount is 0.") @@ -685,82 +655,82 @@ func getStatisticsItemsListGrouped(maximumGroupsToCreate int, if (labelIsNumerical == true){ - maximumItemsPerCategory := int(math.Ceil(float64(len(statisticsItemsList))/float64(maximumGroupsToCreate))) + maximumDatumsPerCategory := int(math.Ceil(float64(len(statisticsDatumsList))/float64(maximumGroupsToCreate))) - statisticsItemsListSublists, err := helpers.SplitListIntoSublists(statisticsItemsList, maximumItemsPerCategory) + statisticsDatumsListSublists, err := helpers.SplitListIntoSublists(statisticsDatumsList, maximumDatumsPerCategory) if (err != nil) { return nil, err } - groupedItemsList := make([]StatisticsItem, 0, len(statisticsItemsListSublists)) + groupedDatumsList := make([]statisticsDatum.StatisticsDatum, 0, len(statisticsDatumsListSublists)) - for _, groupItemsListSublist := range statisticsItemsListSublists{ + for _, groupDatumsListSublist := range statisticsDatumsListSublists{ - if (len(groupItemsListSublist) == 1){ - // Sometimes, a group with 1 item will be created + if (len(groupDatumsListSublist) == 1){ + // Sometimes, a group with 1 datum will be created // This happens if the groups cannot be evenly divided, so there is a remainder of 1. // Example: 10->4 groups = 3, 3, 3, 1. - //TODO: Prevent this from happening so groups always have more than 1 subitem + //TODO: Prevent this from happening so groups always have more than 1 subdatum - groupItem := groupItemsListSublist[0] + groupDatum := groupDatumsListSublist[0] - groupedItemsList = append(groupedItemsList, groupItem) + groupedDatumsList = append(groupedDatumsList, groupDatum) continue } - finalIndex := len(groupItemsListSublist)-1 + finalIndex := len(groupDatumsListSublist)-1 - initialItem := groupItemsListSublist[0] - finalItem := groupItemsListSublist[finalIndex] + initialDatum := groupDatumsListSublist[0] + finalDatum := groupDatumsListSublist[finalIndex] - initialLabel := initialItem.Label - initialLabelFormatted := initialItem.LabelFormatted + initialLabel := initialDatum.Label + initialLabelFormatted := initialDatum.LabelFormatted - finalLabel := finalItem.Label - finalLabelFormatted := finalItem.LabelFormatted + finalLabel := finalDatum.Label + finalLabelFormatted := finalDatum.LabelFormatted - groupValue, err := getGroupValue(groupItemsListSublist) + groupValue, err := getGroupValue(groupDatumsListSublist) if (err != nil) { return nil, err } groupValueFormatted, err := formatValuesFunction(groupValue) if (err != nil) { return nil, err } - newGroupStatisticsItem := StatisticsItem{ + newGroupStatisticsDatum := statisticsDatum.StatisticsDatum{ Label: initialLabel + "-" + finalLabel, LabelFormatted: initialLabelFormatted + "-" + finalLabelFormatted, Value: groupValue, ValueFormatted: groupValueFormatted, } - groupedItemsList = append(groupedItemsList, newGroupStatisticsItem) + groupedDatumsList = append(groupedDatumsList, newGroupStatisticsDatum) } - return groupedItemsList, nil + return groupedDatumsList, nil } // Label is not numerical // We combine all categories after the first maximumGroupsToCreate into a category called Other - itemsToKeep := statisticsItemsList[:maximumGroupsToCreate] + datumsToKeep := statisticsDatumsList[:maximumGroupsToCreate] - itemsToCombine := statisticsItemsList[maximumGroupsToCreate:] + datumsToCombine := statisticsDatumsList[maximumGroupsToCreate:] otherTranslated := translation.TranslateTextFromEnglishToMyLanguage("Other") - otherGroupValue, err := getGroupValue(itemsToCombine) + otherGroupValue, err := getGroupValue(datumsToCombine) if (err != nil) { return nil, err } otherGroupValueFormatted, err := formatValuesFunction(otherGroupValue) if (err != nil) { return nil, err } - otherGroupItem := StatisticsItem{ + otherGroupDatum := statisticsDatum.StatisticsDatum{ Label: "Other", LabelFormatted: otherTranslated, Value: otherGroupValue, ValueFormatted: otherGroupValueFormatted, } - groupedStatisticsItemsList := append(itemsToKeep, otherGroupItem) + groupedStatisticsDatumsList := append(datumsToKeep, otherGroupDatum) - return groupedStatisticsItemsList, nil + return groupedStatisticsDatumsList, nil } diff --git a/internal/statisticsDatum/statisticsDatum.go b/internal/statisticsDatum/statisticsDatum.go new file mode 100644 index 0000000..350283b --- /dev/null +++ b/internal/statisticsDatum/statisticsDatum.go @@ -0,0 +1,39 @@ + +//statisticsDatum implements the StatisticsDatum struct + +package statisticsDatum + + +type StatisticsDatum struct{ + + // The label for the statistics datum + // For a bar chart, this represents the name of an X axis bar. + // For a donut chart, this represents the name of a donut slice + // Example: "Man", "100-200" + // This will never be translated, unless it is the Unknown/No Response value, in which case + // the label will be "Unknown"/"No Response" translated to the user's current app language + Label string + + // This is the formatted, human readable version of the label + // This will be translated into the application language + // Sometimes, the LabelFormatted will be identical to Label + // This does not include units (Example: " days", " users") + // Example: + // -"1000000-2000000" -> "1 million-2 million" + LabelFormatted string + + // The value corresponding to the label + // For a bar chart, this represents the Y axis value for a bar. + // For a donut chart, this represents the value (size) of one of the donut slices + // This will never be translated + // For example, the value could be 500 if 500 men responded Yes. + Value float64 + + // This is the formatted version of the value + // This does not include units (Example: " days", " users") + // Examples: + // -5 -> "5/10" + // -1500000000000 -> "1.5 trillion" + ValueFormatted string +} +