From 56bbd1a797f90f2ac16c8cb244ec67967cad6de6 Mon Sep 17 00:00:00 2001
From: esikkala <esko.ikkala@aalto.fi>
Date: Wed, 5 Jan 2022 12:35:50 +0200
Subject: [PATCH] Support nested result classes

---
 .../components/facet_results/ApexCharts.js    | 85 ++++++------------
 .../facet_results/ResultClassRoute.js         | 62 ++-----------
 src/client/helpers/helpers.js                 |  4 +-
 .../ApexCharts/ApexChartsConfig.js            | 90 ++++++++++++-------
 src/client/reducers/index.js                  | 11 ++-
 src/server/sparql/Mappers.js                  | 35 +++++++-
 src/server/sparql/Utils.js                    | 14 +++
 7 files changed, 155 insertions(+), 146 deletions(-)

diff --git a/src/client/components/facet_results/ApexCharts.js b/src/client/components/facet_results/ApexCharts.js
index 77d8fe0f..a1d38814 100644
--- a/src/client/components/facet_results/ApexCharts.js
+++ b/src/client/components/facet_results/ApexCharts.js
@@ -33,16 +33,25 @@ class ApexChart extends React.Component {
   constructor (props) {
     super(props)
     this.chartRef = React.createRef()
+    const { resultClassConfig, apexChartsConfig } = this.props
+    let resultClass = this.props.resultClass
+    if (resultClassConfig.dropdownForResultClasses) {
+      resultClass = resultClassConfig.defaultResultClass
+    }
     this.state = {
-      resultClass: props.resultClass,
-      createChartData: props.createChartData,
-      chartType: props.dropdownForChartTypes ? props.chartTypes[0].id : null,
+      resultClass,
+      createChartData: resultClassConfig.createChartData
+        ? apexChartsConfig[resultClassConfig.createChartData]
+        : apexChartsConfig[resultClassConfig.chartTypes[0].createChartData],
+      chartType: resultClassConfig.dropdownForChartTypes ? resultClassConfig.chartTypes[0].id : null,
       dialogData: null
     }
   }
 
   componentDidMount = () => {
-    if (this.props.rawData && this.props.rawData.length > 0 && !this.props.doNotRenderOnMount) {
+    const { results } = this.props
+    const { doNotRenderOnMount } = this.props.resultClassConfig
+    if (results && results.length > 0 && !doNotRenderOnMount) {
       this.renderChart()
     }
     this.props.fetchData({
@@ -55,12 +64,11 @@ class ApexChart extends React.Component {
   }
 
   componentDidUpdate = (prevProps, prevState) => {
-    // Render the chart again if the raw data has changed
-    if (prevProps.rawDataUpdateID !== this.props.rawDataUpdateID) {
+    if (prevProps.resultUpdateID !== this.props.resultUpdateID) {
       this.renderChart()
     }
-    // check if filters have changed
-    if (this.props.pageType === 'facetResults' && prevProps.facetUpdateID !== this.props.facetUpdateID) {
+    const { pageType = 'facetResults' } = this.props
+    if (pageType === 'facetResults' && prevProps.facetUpdateID !== this.props.facetUpdateID) {
       this.props.fetchData({
         perspectiveID: this.props.perspectiveConfig.id,
         resultClass: this.state.resultClass,
@@ -102,24 +110,7 @@ class ApexChart extends React.Component {
     }
     this.chart = new ApexCharts(
       this.chartRef.current,
-      this.state.createChartData({
-        rawData: this.props.rawData,
-        title: this.props.title,
-        xaxisTitle: this.props.xaxisTitle || intl.get(`apexCharts.${this.state.resultClass}Xaxis`),
-        yaxisTitle: this.props.yaxisTitle || '',
-        seriesTitle: this.props.seriesTitle || '',
-        xaxisType: this.props.xaxisType || null,
-        xaxisTickAmount: this.props.xaxisTickAmount || null,
-        xaxisLabels: this.props.xaxisLabels || null,
-        stroke: this.props.stroke || null,
-        fill: this.props.fill || null,
-        tooltip: this.props.tooltip || null,
-        fetchInstanceAnalysis: this.props.fetchInstanceAnalysis,
-        resultClass: this.props.resultClass,
-        facetID: this.props.facetID,
-        facetClass: this.props.facetClass,
-        screenSize: this.props.screenSize
-      })
+      this.state.createChartData({ ...this.props })
     )
     this.chart.render()
   }
@@ -128,10 +119,10 @@ class ApexChart extends React.Component {
 
   handleChartTypeOnChanhge = event => {
     const chartType = event.target.value
-    const chartTypeObj = this.props.chartTypes.find(chartTypeObj => chartTypeObj.id === chartType)
+    const chartTypeObj = this.props.resultClassConfig.chartTypes.find(chartTypeObj => chartTypeObj.id === chartType)
     this.setState({
       chartType,
-      createChartData: chartTypeObj.createChartData
+      createChartData: this.props.apexChartsConfig[chartTypeObj.createChartData]
     })
   }
 
@@ -146,12 +137,12 @@ class ApexChart extends React.Component {
     if (this.isSmallScreen()) {
       return 'auto'
     }
-    const rootHeightReduction = this.props.layoutConfig.tabHeight + 2 * defaultPadding + 1
+    const rootHeightReduction = this.props.portalConfig.layoutConfig.tabHeight + 2 * defaultPadding + 1
     return `calc(100% - ${rootHeightReduction}px)`
   }
 
   getHeightForChartContainer = () => {
-    const { dropdownForResultClasses, dropdownForChartTypes } = this.props
+    const { dropdownForResultClasses, dropdownForChartTypes } = this.props.resultClassConfig
     if (this.isSmallScreen()) {
       return 600
     }
@@ -166,14 +157,8 @@ class ApexChart extends React.Component {
   }
 
   render () {
-    const {
-      fetching, pageType, classes, dropdownForResultClasses,
-      dropdownForChartTypes, facetResultsType
-    } = this.props
-    let facetResultsTypeCapitalized = ''
-    if (facetResultsType) {
-      facetResultsTypeCapitalized = facetResultsType[0].toUpperCase() + facetResultsType.substring(1).toLowerCase()
-    }
+    const { classes, fetching, resultClassConfig } = this.props
+    const { pageType = 'facetResults', dropdownForResultClasses, resultClasses, dropdownForChartTypes, chartTypes } = resultClassConfig
     let rootStyle = {
       width: '100%',
       height: '100%'
@@ -199,9 +184,7 @@ class ApexChart extends React.Component {
       width: '100%',
       height: this.getHeightForChartContainer()
     }
-    let dropdownText = intl.get('apexCharts.by') === ''
-      ? intl.get('apexCharts.grouping')
-      : `${facetResultsTypeCapitalized} ${intl.get('apexCharts.by')}`
+    let dropdownText = intl.get('apexCharts.grouping')
     if (this.props.xaxisType === 'numeric') {
       dropdownText = intl.get('apexCharts.property')
     }
@@ -216,7 +199,7 @@ class ApexChart extends React.Component {
                 value={this.state.resultClass}
                 onChange={this.handleResultClassOnChanhge}
               >
-                {this.props.resultClasses.map(resultClass =>
+                {Object.keys(resultClasses).map(resultClass =>
                   <MenuItem key={resultClass} value={resultClass}>{intl.get(`apexCharts.resultClasses.${resultClass}`)}</MenuItem>
                 )}
               </Select>
@@ -231,7 +214,7 @@ class ApexChart extends React.Component {
                 value={this.state.chartType}
                 onChange={this.handleChartTypeOnChanhge}
               >
-                {this.props.chartTypes.map(chartType =>
+                {chartTypes.map(chartType =>
                   <MenuItem key={chartType.id} value={chartType.id}>{intl.get(`apexCharts.${chartType.id}`)}</MenuItem>
                 )}
               </Select>
@@ -261,23 +244,9 @@ class ApexChart extends React.Component {
 }
 
 ApexChart.propTypes = {
-  pageType: PropTypes.string.isRequired,
-  createChartData: PropTypes.func,
-  rawData: PropTypes.oneOfType([
-    PropTypes.array,
-    PropTypes.object
-  ]),
-  rawDataUpdateID: PropTypes.number,
   fetchData: PropTypes.func.isRequired,
-  fetching: PropTypes.bool.isRequired,
   resultClass: PropTypes.string,
-  facetClass: PropTypes.string,
-  facetID: PropTypes.string,
-  uri: PropTypes.string,
-  dropdownForResultClasses: PropTypes.bool,
-  facetResultsType: PropTypes.string,
-  resultClasses: PropTypes.array,
-  layoutConfig: PropTypes.object.isRequired
+  facetClass: PropTypes.string
 }
 
 export const ApexChartComponent = ApexChart
diff --git a/src/client/components/facet_results/ResultClassRoute.js b/src/client/components/facet_results/ResultClassRoute.js
index 5189d9a9..8b15782a 100644
--- a/src/client/components/facet_results/ResultClassRoute.js
+++ b/src/client/components/facet_results/ResultClassRoute.js
@@ -247,65 +247,21 @@ const ResultClassRoute = props => {
       break
     }
     case 'ApexCharts': {
-      const {
-        pageType = 'facetResults',
-        title,
-        xaxisTitle,
-        xaxisType,
-        xaxisTickAmount,
-        yaxisTitle,
-        seriesTitle,
-        stroke,
-        fill,
-        createChartData,
-        doNotRenderOnMount = false,
-        dropdownForResultClasses = false,
-        dropdownForChartTypes = false
-      } = resultClassConfig
       const apexProps = {
         portalConfig,
         perspectiveConfig: perspective,
-        pageType,
+        resultClassConfig,
+        apexChartsConfig: props.apexChartsConfig,
+        screenSize,
         resultClass,
         facetClass,
-        rawData: perspectiveState.results,
-        rawDataUpdateID: perspectiveState.resultUpdateID,
+        perspectiveState,
+        results: perspectiveState.results,
         fetching: perspectiveState.fetching,
-        fetchData: props.fetchResults,
-        createChartData: props.apexChartsConfig[createChartData],
-        title,
-        xaxisTitle,
-        xaxisType,
-        xaxisTickAmount,
-        yaxisTitle,
-        seriesTitle,
-        stroke,
-        fill,
-        layoutConfig: props.layoutConfig,
-        doNotRenderOnMount,
-        dropdownForResultClasses
-      }
-      if (pageType === 'facetResults') {
-        apexProps.facetUpdateID = facetState.facetUpdateID
-      }
-      if (pageType === 'instancePage') {
-        apexProps.uri = perspectiveState.instanceTableData.id
-      }
-      if (dropdownForResultClasses && has(resultClassConfig, 'resultClasses')) {
-        apexProps.resultClass = resultClassConfig.resultClasses[0]
-        apexProps.resultClasses = resultClassConfig.resultClasses
-        apexProps.dropdownForResultClasses = true
-      }
-      if (dropdownForChartTypes && has(resultClassConfig, 'chartTypes')) {
-        const { chartTypes } = resultClassConfig
-        const newChartTypes = chartTypes.map(chartType => {
-          return {
-            id: chartType.id,
-            createChartData: props.apexChartsConfig[chartType.createChartData]
-          }
-        })
-        apexProps.chartTypes = newChartTypes
-        apexProps.dropdownForChartTypes = true
+        resultUpdateID: perspectiveState.resultUpdateID,
+        instanceAnalysisDataUpdateID: perspectiveState.instanceAnalysisDataUpdateID,
+        facetUpdateID: facetState.facetUpdateID,
+        fetchData: props.fetchResults
       }
       routeComponent = (
         <Route
diff --git a/src/client/helpers/helpers.js b/src/client/helpers/helpers.js
index 555a30b7..e3f127d9 100644
--- a/src/client/helpers/helpers.js
+++ b/src/client/helpers/helpers.js
@@ -161,9 +161,9 @@ export const arrayToObject = ({ array, keyField }) =>
     return obj
   }, {})
 
-export const generateLabelForMissingValue = ({ facetClass, facetID }) => {
+export const generateLabelForMissingValue = ({ perspective, property }) => {
   // Check if there is a translated label for missing value, or use defaults
-  return intl.get(`perspectives.${facetClass}.properties.${facetID}.missingValueLabel`) ||
+  return intl.get(`perspectives.${perspective}.properties.${property}.missingValueLabel`) ||
    intl.get('facetBar.defaultMissingValueLabel') || 'Unknown'
 }
 
diff --git a/src/client/library_configs/ApexCharts/ApexChartsConfig.js b/src/client/library_configs/ApexCharts/ApexChartsConfig.js
index 4577e011..7e01a689 100644
--- a/src/client/library_configs/ApexCharts/ApexChartsConfig.js
+++ b/src/client/library_configs/ApexCharts/ApexChartsConfig.js
@@ -2,6 +2,15 @@
 import intl from 'react-intl-universal'
 import { generateLabelForMissingValue } from '../../helpers/helpers'
 
+// list of colors generated with http://phrogz.net/css/distinct-colors.html
+const pieChartColors = ['#a12a3c', '#0f00b5', '#81c7a4', '#ffdea6', '#ff0033', '#424cff', '#1b6935', '#ff9d00', '#5c3c43',
+  '#5f74b8', '#18b532', '#3b3226', '#fa216d', '#153ca1', '#00ff09', '#703a00', '#b31772', '#a4c9fc', '#273623',
+  '#f57200', '#360e2c', '#001c3d', '#ccffa6', '#a18068', '#ba79b6', '#004e75', '#547500', '#c2774c', '#f321fa', '#1793b3',
+  '#929c65', '#b53218', '#563c5c', '#1ac2c4', '#c4c734', '#4c150a', '#912eb3', '#2a5252', '#524b00', '#bf7d7c', '#24005e',
+  '#20f2ba', '#b5882f']
+
+const defaultSliceVisibilityThreshold = 0.01
+
 export const createSingleLineChartData = ({
   rawData,
   title,
@@ -106,26 +115,37 @@ export const createMultipleLineChartData = ({
   return apexChartOptionsWithData
 }
 
-export const createApexPieChartData = ({ rawData, screenSize, facetClass, facetID }) => {
+export const createApexPieChartData = ({
+  resultClass,
+  facetClass,
+  perspectiveState,
+  results,
+  resultClassConfig,
+  screenSize
+}) => {
   const labels = []
   const series = []
   let otherCount = 0
-  const totalLength = rawData.length
-  const threshold = 0.15
-  rawData.forEach(item => {
-    const portion = parseInt(item.instanceCount) / totalLength
-    if (portion < threshold) {
-      otherCount += parseInt(item.instanceCount)
+  const arraySum = results.reduce((sum, current) => sum + current.instanceCount, 0)
+  let actualResultClassConfig = resultClassConfig
+  if (resultClassConfig.dropdownForResultClasses) {
+    actualResultClassConfig = resultClassConfig.resultClasses[perspectiveState.resultClass]
+  }
+  const { sliceVisibilityThreshold = defaultSliceVisibilityThreshold, propertyID } = actualResultClassConfig
+  results.forEach(item => {
+    const sliceFraction = item.instanceCount / arraySum
+    if (sliceFraction <= sliceVisibilityThreshold) {
+      otherCount += item.instanceCount
     } else {
-      if (item.id === 'http://ldf.fi/MISSING_VALUE') {
-        item.prefLabel = generateLabelForMissingValue({ facetClass, facetID })
+      if (item.id === 'http://ldf.fi/MISSING_VALUE' || item.category === 'http://ldf.fi/MISSING_VALUE') {
+        item.prefLabel = generateLabelForMissingValue({ perspective: facetClass, property: propertyID })
       }
       labels.push(item.prefLabel)
-      series.push(parseInt(item.instanceCount))
+      series.push(item.instanceCount)
     }
   })
   if (otherCount !== 0) {
-    labels.push('Other')
+    labels.push(intl.get('apexCharts.other') || 'Other')
     series.push(otherCount)
   }
   let chartColors = []
@@ -157,13 +177,6 @@ export const createApexPieChartData = ({ rawData, screenSize, facetClass, facetI
   return apexChartOptionsWithData
 }
 
-// list of colors generated with http://phrogz.net/css/distinct-colors.html
-const pieChartColors = ['#a12a3c', '#0f00b5', '#81c7a4', '#ffdea6', '#ff0033', '#424cff', '#1b6935', '#ff9d00', '#5c3c43',
-  '#5f74b8', '#18b532', '#3b3226', '#fa216d', '#153ca1', '#00ff09', '#703a00', '#b31772', '#a4c9fc', '#273623',
-  '#f57200', '#360e2c', '#001c3d', '#ccffa6', '#a18068', '#ba79b6', '#004e75', '#547500', '#c2774c', '#f321fa', '#1793b3',
-  '#929c65', '#b53218', '#563c5c', '#1ac2c4', '#c4c734', '#4c150a', '#912eb3', '#2a5252', '#524b00', '#bf7d7c', '#24005e',
-  '#20f2ba', '#b5882f']
-
 const apexPieChartOptions = {
   // see https://apexcharts.com/docs --> Options
   chart: {
@@ -214,30 +227,45 @@ const apexPieChartOptions = {
 }
 
 export const createApexBarChartData = ({
-  rawData,
-  title,
-  xaxisTitle,
-  yaxisTitle,
-  seriesTitle
+  resultClass,
+  facetClass,
+  perspectiveState,
+  results,
+  resultClassConfig,
+  screenSize
 }) => {
+  const {
+    title,
+    seriesTitle,
+    xaxisTitle,
+    yaxisTitle
+  } = resultClassConfig
   const categories = []
   const colors = []
   const data = []
   let otherCount = 0
-  const totalLength = rawData.length
-  const threshold = 0.3
-  rawData.forEach(item => {
-    const portion = parseInt(item.instanceCount) / totalLength
-    if (portion < threshold) {
-      otherCount += parseInt(item.instanceCount)
+  const arraySum = results.reduce((sum, current) => sum + current.instanceCount, 0)
+  let actualResultClassConfig = resultClassConfig
+  if (resultClassConfig.dropdownForResultClasses) {
+    actualResultClassConfig = resultClassConfig.resultClasses[perspectiveState.resultClass]
+  }
+  const { sliceVisibilityThreshold = defaultSliceVisibilityThreshold, propertyID } = actualResultClassConfig
+
+  results.forEach(item => {
+    const sliceFraction = item.instanceCount / arraySum
+    if (sliceFraction <= sliceVisibilityThreshold) {
+      otherCount += item.instanceCount
     } else {
+      if (item.id === 'http://ldf.fi/MISSING_VALUE' || item.category === 'http://ldf.fi/MISSING_VALUE') {
+        item.prefLabel = generateLabelForMissingValue({ perspective: facetClass, property: propertyID })
+      }
       categories.push(item.prefLabel)
       colors.push('#000000')
-      data.push(parseInt(item.instanceCount))
+      data.push(item.instanceCount)
     }
   })
   if (otherCount !== 0) {
-    categories.push('Other')
+    categories.push(intl.get('apexCharts.other') || 'Other')
     colors.push('#000000')
     data.push(otherCount)
   }
diff --git a/src/client/reducers/index.js b/src/client/reducers/index.js
index 4092b58d..5bef2176 100644
--- a/src/client/reducers/index.js
+++ b/src/client/reducers/index.js
@@ -83,9 +83,18 @@ for (const perspective of perspectiveConfig) {
       ...facetsInitialState,
       facets
     }
+    let extraResultClasses = {}
+    for (const resultClass in resultClasses) {
+      if (resultClasses[resultClass].resultClasses) {
+        extraResultClasses = {
+          ...extraResultClasses,
+          ...resultClasses[resultClass].resultClasses
+        }
+      }
+    }
     const resultsReducer = createResultsReducer(
       resultsInitialStateFull,
-      new Set(Object.keys({ ...resultClasses, ...instancePageResultClasses })))
+      new Set(Object.keys({ ...resultClasses, ...instancePageResultClasses, ...extraResultClasses })))
     const facetsReducer = createFacetsReducer(facetsInitialStateFull, perspectiveID)
     const facetsConstrainSelfReducer = createFacetsConstrainSelfReducer(facetsInitialStateFull, perspectiveID)
     reducers[perspectiveID] = resultsReducer
diff --git a/src/server/sparql/Mappers.js b/src/server/sparql/Mappers.js
index 04dc9b6e..e6b2c746 100644
--- a/src/server/sparql/Mappers.js
+++ b/src/server/sparql/Mappers.js
@@ -35,6 +35,39 @@ export const mapCoordinates = sparqlBindings => {
   return results
 }
 
+export const mapBirthYearCount = sparqlBindings => {
+  // console.log(sparqlBindings);
+  const results = sparqlBindings.map(b => {
+    return {
+      counted: b.counted.value,
+      count: b.count.value
+    }
+  })
+  return results
+}
+
+export const mapAgeCount = sparqlBindings => {
+  // console.log(sparqlBindings);
+  const results = sparqlBindings.map(b => {
+    return {
+      counted: b.counted.value,
+      count: b.count.value
+    }
+  })
+  return results
+}
+
+export const mapCountGroups = sparqlBindings => {
+  // console.log(sparqlBindings);
+  const results = sparqlBindings.map(b => {
+    return {
+      counted: b.counted.value,
+      count: b.count.value
+    }
+  })
+  return results
+}
+
 export const mapCount = sparqlBindings => {
   return sparqlBindings[0].count.value
 }
@@ -190,7 +223,7 @@ export const mapPieChart = sparqlBindings => {
     return {
       category: b.category.value,
       prefLabel: b.prefLabel.value,
-      instanceCount: b.instanceCount.value
+      instanceCount: parseInt(b.instanceCount.value)
     }
   })
   return results
diff --git a/src/server/sparql/Utils.js b/src/server/sparql/Utils.js
index e26f041d..203b93a5 100644
--- a/src/server/sparql/Utils.js
+++ b/src/server/sparql/Utils.js
@@ -49,10 +49,24 @@ export const createBackendSearchConfig = async () => {
         }
       }
       // handle other resultClasses
+      let extraResultClasses = {}
       for (const resultClass in perspectiveConfig.resultClasses) {
         if (resultClass === perspectiveID) { continue }
         const resultClassConfig = perspectiveConfig.resultClasses[resultClass]
         processResultClassConfig(resultClassConfig, sparqlQueries, resultMappers)
+        if (resultClassConfig.resultClasses) {
+          for (const extraResultClass in resultClassConfig.resultClasses) {
+            processResultClassConfig(resultClassConfig.resultClasses[extraResultClass], sparqlQueries, resultMappers)
+          }
+          extraResultClasses = {
+            ...extraResultClasses,
+            ...resultClassConfig.resultClasses
+          }
+        }
+      }
+      perspectiveConfig.resultClasses = {
+        ...perspectiveConfig.resultClasses,
+        ...extraResultClasses
       }
       // merge facet results and instance page result classes
       if (hasInstancePageResultClasses) {
-- 
GitLab