diff --git a/app/assets/stylesheets/tpi.scss b/app/assets/stylesheets/tpi.scss index 614daf86d..d3605b988 100644 --- a/app/assets/stylesheets/tpi.scss +++ b/app/assets/stylesheets/tpi.scss @@ -26,6 +26,7 @@ @import "tpi/navbar"; @import "tpi/publications"; @import "tpi/bubble-chart"; +@import "tpi/bubble-chart-countries"; @import "tpi/charts"; @import "tpi/base-tooltip"; @import "tpi/info-tooltip"; diff --git a/app/assets/stylesheets/tpi/_bubble-chart-countries.scss b/app/assets/stylesheets/tpi/_bubble-chart-countries.scss new file mode 100644 index 000000000..f1faf39ee --- /dev/null +++ b/app/assets/stylesheets/tpi/_bubble-chart-countries.scss @@ -0,0 +1,195 @@ +@import "colors"; +@import "typography"; +$tape-height: 8px; +$tape-color: rgba(25, 25, 25, 0.1); +$cell-height: 80px; +$cell-height-banks: 100px; +$legend-image-width: 60px; + +.bubble-chart__container__grid { + display: none; + @include desktop { + grid-template-columns: 0.5fr 0.5fr 1.5fr 1fr 1fr 1fr; + padding: 0; + display: grid; + } +} + +.bubble-chart__container__mobile { + display: block; + width: 100%; + padding: 10px; + + .country-bubble-mobile { + width: 100%; + border: 1px solid $ascor-background-color; + + > ul > li:last-of-type .country-bubble-mobile__item { + border-bottom: none; + } + + &__item { + width: 100%; + display: flex; + justify-content: space-between; + font-size: 16px; + font-style: normal; + + &.--pillar { + font-weight: 700; + font-family: $font-family-bold; + background: $ascor-background-color; + color: #fff; + padding: 20px 10px; + cursor: pointer; + border-bottom: 1px solid $grey-medium; + &.--open { + border-bottom: none; + } + } + &.--area { + color: #000; + padding: 10px; + font-family: $font-family-regular; + border-top: 1px solid $ascor-background-color; + cursor: pointer; + &.--open { + border-bottom: 1px solid $grey-medium; + } + } + + .chevron-icon { + width: 12px; + } + + &__result { + font-family: $font-family-regular; + font-size: 16px; + + &__title { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + padding: 10px; + border-bottom: 1px solid $grey-medium; + cursor: pointer; + min-height: 45px; + position: relative; + + div { + width: 20px; + height: 20px; + border-radius: 50%; + } + } + + &:not(:last-of-type) .country-bubble-mobile__item__result__title { + border-bottom-style: dashed; + } + + &.--open { + .country-bubble-mobile__item__result__title { + border-bottom: none; + position: absolute; + } + .country-bubble-mobile__item__result__countries { + border-bottom: 1px solid $grey-medium; + border-bottom-style: dashed; + } + } + + &:last-of-type { + .country-bubble-mobile__item__result__countries { + border-bottom: none; + } + } + + &__countries { + padding: 10px 0px 10px 42px; + min-height: 45px; + li:not(:last-of-type) { + padding-bottom: 10px; + } + } + } + } + } +} + +.bubble-chart__cell-country { + position: relative; + height: $cell-height-banks; + display: flex; + align-items: center; + border-right: calc(#{$tape-height / 2}) dashed $tape-color; + + & > *:first-child { + margin: auto; + z-index: 1; + } + + &::before { + background-color: $tape-color; + content: ""; + position: absolute; + top: calc(50% - #{$tape-height / 2}); + height: $tape-height; + width: calc(100% + #{$tape-height / 2}); + } +} + +.bubble-chart_circle_country { + circle:hover { + stroke-width: 3; + stroke: $black !important; + } +} + +.bubble-chart__level-country { + border-right: calc(#{$tape-height / 2}) dashed $tape-color; + position: relative; + padding-left: 20px; + height: 100%; +} + +.bubble-chart__level-title-country { + height: 100%; + font-family: $font-family-bold; + font-size: 16px; + color: $black; + margin-bottom: 20px; +} + +.bubble-chart__level-area-country { + font-family: $font-family-bold; + font-size: 16px; + background-color: $ascor-background-color; + color: white; + padding: 10px; + width: 100%; + display: flex; + align-items: center; + + color: $black; + text-align: end; + margin-right: 14px; + grid-column: span 2; + height: 100%; + padding: 46px 0 46px; + gap: 16px; + background-color: white; + + &__line { + border: 8px solid #e8e8e8; + border-right: none; + height: 100%; + flex: 1; + } + + &__area { + text-align: end; + padding-right: 14px; + flex: 1; + } +} diff --git a/app/assets/stylesheets/tpi/_bubble-chart.scss b/app/assets/stylesheets/tpi/_bubble-chart.scss index b290bdca1..67189583b 100644 --- a/app/assets/stylesheets/tpi/_bubble-chart.scss +++ b/app/assets/stylesheets/tpi/_bubble-chart.scss @@ -2,7 +2,7 @@ @import "typography"; $tape-height: 8px; -$tape-color: rgba(25,25,25,0.1); +$tape-color: rgba(25, 25, 25, 0.1); $cell-height: 80px; $cell-height-banks: 100px; $legend-image-width: 60px; @@ -21,7 +21,6 @@ $legend-image-width: 60px; .last { border-right: none; } - } &--banks { @@ -62,28 +61,6 @@ $legend-image-width: 60px; } } -.bubble-chart__cell-country { - position: relative; - height: $cell-height-banks; - display: flex; - align-items: center; - border-right: calc(#{$tape-height / 2}) dashed $tape-color; - - & > *:first-child { - margin: auto; - z-index: 1; - } - - &::before { - background-color: $tape-color; - content: ""; - position: absolute; - top: calc(50% - #{$tape-height / 2}); - height: $tape-height; - width: calc(100% + #{$tape-height / 2}); - } -} - .bubble-chart_circle { circle:hover { stroke-width: 14; @@ -91,13 +68,6 @@ $legend-image-width: 60px; } } -.bubble-chart_circle_country { - circle:hover { - stroke-width: 3; - stroke: $black!important; - } -} - .bubble-tip { font-size: 14px; padding: 10px; @@ -201,28 +171,6 @@ $legend-image-width: 60px; } } -.bubble-chart__level-country { - border-right: calc(#{$tape-height / 2}) dotted $tape-color; - position: relative; - padding-left: 20px; - height: 100%; -} - -.bubble-chart__level-title-country { - height: 100%; - font-family: $font-family-bold; - font-size: 16px; - color: $black; -} - -.bubble-chart__level-area-country { - font-family: $font-family-bold; - font-size: 16px; - color: $black; - text-align: end; - margin-right: 14px; -} - .bubble-chart__container--banks { .bubble-chart__level-title { font-family: $font-family-bold; diff --git a/app/javascript/components/tpi/charts/ascor-bubble/Chart.js b/app/javascript/components/tpi/charts/ascor-bubble/Chart.js index 914f40829..c24b6a9ef 100644 --- a/app/javascript/components/tpi/charts/ascor-bubble/Chart.js +++ b/app/javascript/components/tpi/charts/ascor-bubble/Chart.js @@ -1,8 +1,13 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import PropTypes from 'prop-types'; import SingleCell from './SingleCell'; -import { SCORE_RANGES } from './constants'; +import { SCORE_RANGES, VALUES } from './constants'; +import { groupBy, keys, pickBy, values } from 'lodash'; + +import ChartMobile from './chart-mobile'; + +const DESKTOP_MIN_WIDTH = 992; const SCALE = 1.25; @@ -18,55 +23,76 @@ const SINGLE_CELL_SVG_HEIGHT = 100; let tooltip = null; -const BubbleChart = ({ results, disabled_bubbles_areas }) => { +const BubbleChart = ({ results }) => { const tooltipEl = ''; useEffect(() => { document.body.insertAdjacentHTML('beforeend', tooltipEl); tooltip = document.getElementById('bubble-chart-tooltip'); }, []); - const ranges = SCORE_RANGES.map((range) => range.value); + const ranges = keys(SCORE_RANGES); + + const parsedData = useMemo( + () => values(values(groupBy(results, 'pillar'))).map((value) => ({ + pillar: value[0].pillar, + values: values(groupBy(value, 'area')).map((areaValues) => { + const vValues = pickBy( + groupBy(areaValues, 'result'), + (_value, key) => key in VALUES + ); + const v = { + ...VALUES, + ...vValues + }; + return { + area: areaValues[0].area, + values: values(v) + }; + }) + })), + [results] + ); - const parsedData = {}; - const pillars = {}; + const [isMobile, setIsMobile] = React.useState(true); - results.forEach((result) => { - if (parsedData[result.area] === undefined) { - parsedData[result.area] = Array.from({ length: ranges.length }, () => []); - } - const rangeIndex = SCORE_RANGES.findIndex( - (range) => result.result === range.value - ); - if (rangeIndex >= 0) { - parsedData[result.area][rangeIndex].push({ - ...result, - color: SCORE_RANGES[rangeIndex].color - }); + const handleResize = () => { + if (window.innerWidth < DESKTOP_MIN_WIDTH) { + setIsMobile(true); } else { - console.error('WRONG INDEX', result); + setIsMobile(false); } - if (pillars[result.pillar] === undefined) { - pillars[result.pillar] = [result.area]; - } else { - pillars[result.pillar] = pillars[result.pillar].includes(result.area) - ? pillars[result.pillar] - : [...pillars[result.pillar], result.area]; + }; + + useEffect(() => { + if (typeof window !== 'undefined') { + handleResize(); + window.addEventListener('resize', handleResize); } - }); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); return ( -
-
Pillar
-
Area
-
- {ranges.map((range) => ( -
-
{range}
+
+
+
Pillar
+
Area
+
+ {ranges.map((range) => ( +
+
{range}
+
+ ))} +
+ {isMobile ? ( +
+
- ))} - {Object.keys(parsedData).map((area) => createRow(parsedData[area], area, pillars, disabled_bubbles_areas))} + ) : ( +
+ +
+ )}
); }; @@ -115,82 +141,60 @@ const hideTooltip = () => { tooltip.setAttribute('hidden', true); }; -const createRow = (dataRow, area, pillars, disabled_bubbles_areas) => { - const pillarEntries = Object.entries(pillars); - - const pillarIndex = pillarEntries.findIndex(([, value]) => value.includes(area)); - const pillar = pillarEntries[pillarIndex]; - - const pillarSpan = pillar && pillar[1].length; - const pillarName = pillar[0]; +const ChartRows = ({ data }) => data?.map((pillar, pillarIndex) => { + const pillarName = pillar.pillar; + const pillarSpan = pillar.values.length; const pillarAcronym = pillarName .split(' ') .map((word) => word[0]) .join(''); - const areaIndex = pillar && pillar[1].findIndex((el) => el === area); - return ( - + <>
- {pillarIndex + 1}. {pillarName} -
- {areaIndex === 0 && ( -
-
1 && '8px solid #E8E8E8', - borderRight: 'none', - height: '100%' - }} - /> -
- )} -
- {pillarAcronym} {areaIndex + 1}. {area} + + {pillarIndex + 1}. {pillarName} + +
- {dataRow.map((el, i) => { - const countriesBubbles = disabled_bubbles_areas.includes(area) - ? [] - : el.map((result) => ({ - value: COMPANIES_MARKET_CAP_GROUPS[result.market_cap_group], - tooltipContent: { - header: result.country_name, - value: result.result - }, - path: result.country_path, - color: result.color, - result: result.result - })); - - // Remove special characters from the key to be able to use d3-select as it uses querySelector - const cleanKey = area.replace(/[^a-zA-Z\-_:.]/g, ''); - const uniqueKey = `${cleanKey}-${el.length}-${i}`; - - return ( -
- {ForceLayoutBubbleChart(countriesBubbles, uniqueKey)} + + {pillar.values.map(({ area, values: areaValues }, areaIndex) => ( + <> +
+ {pillarAcronym} {areaIndex + 1}. {area}
- ); - })} - + {areaValues.map((areaValuesResult, i) => { + const countriesBubbles = areaValuesResult.map((result) => ({ + value: COMPANIES_MARKET_CAP_GROUPS[result.market_cap_group], + tooltipContent: { + header: result.country_name, + value: result.result + }, + path: result.country_path, + color: result.color, + result: result.result + })); + + // Remove special characters from the key to be able to use d3-select as it uses querySelector + const cleanKey = area.replace(/[^a-zA-Z\-_:.]/g, ''); + const uniqueKey = `${cleanKey}-${areaIndex}-${i}`; + return ( +
+ {ForceLayoutBubbleChart(countriesBubbles, uniqueKey)} +
+ ); + })} + + ))} + ); -}; -BubbleChart.defaultProps = { - disabled_bubbles_areas: [] -}; +}); BubbleChart.propTypes = { results: PropTypes.arrayOf( @@ -203,7 +207,6 @@ BubbleChart.propTypes = { result: PropTypes.string.isRequired, pillar: PropTypes.string.isRequired }) - ).isRequired, - disabled_bubbles_areas: PropTypes.arrayOf(PropTypes.string) + ).isRequired }; export default BubbleChart; diff --git a/app/javascript/components/tpi/charts/ascor-bubble/SingleCell.js b/app/javascript/components/tpi/charts/ascor-bubble/SingleCell.js index ccf6cdc5e..ce1b2bf0a 100644 --- a/app/javascript/components/tpi/charts/ascor-bubble/SingleCell.js +++ b/app/javascript/components/tpi/charts/ascor-bubble/SingleCell.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { range } from 'd3-array'; import { select } from 'd3-selection'; import * as d3 from 'd3-force'; +import { SCORE_RANGES } from './constants'; const SingleCell = ({ width, @@ -20,7 +21,7 @@ const SingleCell = ({ const nodes = range(data.length).map(function (index) { return { - color: data[index].color, + color: SCORE_RANGES[data[index].result], tooltipContent: data[index].tooltipContent, path: data[index].path, radius: data[index].value, @@ -30,8 +31,8 @@ const SingleCell = ({ const simulation = () => { d3.forceSimulation(nodes) - .force('charge', d3.forceManyBody().strength(60)) - .force('y', d3.forceY().strength(0.45).y(0)) + .force('charge', d3.forceManyBody().strength(10)) + .force('y', d3.forceY().strength(0.3).y(0)) .force( 'collision', d3.forceCollide().radius(function (d) { diff --git a/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/ChartMobile.js b/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/ChartMobile.js new file mode 100644 index 000000000..d663829cd --- /dev/null +++ b/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/ChartMobile.js @@ -0,0 +1,172 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import chevronIcon from 'images/icons/white-chevron-down.svg'; +import chevronIconBlack from 'images/icon_chevron_dark/chevron_down_black-1.svg'; +import { SCORE_RANGES } from '../constants'; + +const Item = ({ title, children, className, isOpen, onOpen, icon }) => ( +
  • +
    + {title} + chevron +
    +
    + {children} +
    +
  • +); + +Item.propTypes = { + title: PropTypes.string.isRequired, + className: PropTypes.string, + children: PropTypes.node, + onOpen: PropTypes.func.isRequired, + isOpen: PropTypes.bool, + icon: PropTypes.string.isRequired +}; +Item.defaultProps = { + className: '', + children: null, + isOpen: false +}; + +const ResultItem = ({ result, i }) => { + const [isOpen, setIsOpen] = useState(false); + { + const color = Object.values(SCORE_RANGES)[i]; + const title = `${result.length} countries`; + + return ( +
  • +
    setIsOpen((prevState) => !prevState)} + > +
    + {!isOpen && {title}} +
    + + {isOpen && ( +
      + {result.length ? ( + result.map((country) => ( +
    • {country.country_name}
    • + )) + ) : ( +
    • No countries
    • + )} +
    + )} +
  • + ); + } +}; + +const ChartMobile = ({ data }) => { + const [openPillars, setOpenPillars] = useState([]); + const [openAreas, setOpenAreas] = useState([]); + + const handleOpenAreas = (key) => { + setOpenAreas((prevState) => { + if (prevState.includes(key)) { + return prevState.filter((area) => area !== key); + } + return [...prevState, key]; + }); + }; + + const handleOpenPillars = (key) => { + if (openPillars.includes(key)) { + setOpenPillars((prevState) => prevState.filter((pillar) => pillar !== key)); + setOpenAreas((prevState) => prevState.filter((area) => !area.includes(`${key}.`))); + return; + } + setOpenPillars((prevState) => [...prevState, key]); + }; + + return ( +
    +
      + {data.map((pillar) => ( + handleOpenPillars(pillar.pillar)} + icon={chevronIcon} + > +
        + {pillar.values.map((area, areaIndex) => { + const pillarAcronym = pillar.pillar + .split(' ') + .map((word) => word[0]) + .join(''); + const title = `${pillarAcronym} ${areaIndex + 1}. ${area.area}`; + return ( + handleOpenAreas(`${pillar.pillar}.${area.area}`)} + icon={chevronIconBlack} + > +
          + {area.values.map((result, i) => ( + + ))} +
        +
        + ); + })} +
      +
      + ))} +
    +
    + ); +}; + +ChartMobile.propTypes = { + data: PropTypes.arrayOf( + PropTypes.shape({ + pillar: PropTypes.string, + values: PropTypes.arrayOf( + PropTypes.shape({ + area: PropTypes.string, + values: PropTypes.array + }) + ) + }) + ) +}; + +ChartMobile.defaultProps = { + data: [] +}; + +export default ChartMobile; diff --git a/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/index.js b/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/index.js new file mode 100644 index 000000000..dc8cbf4b0 --- /dev/null +++ b/app/javascript/components/tpi/charts/ascor-bubble/chart-mobile/index.js @@ -0,0 +1,3 @@ +import ChartMobile from './ChartMobile'; + +export default ChartMobile; diff --git a/app/javascript/components/tpi/charts/ascor-bubble/constants.js b/app/javascript/components/tpi/charts/ascor-bubble/constants.js index 8ec9dfb14..f31d301d6 100644 --- a/app/javascript/components/tpi/charts/ascor-bubble/constants.js +++ b/app/javascript/components/tpi/charts/ascor-bubble/constants.js @@ -1,14 +1,7 @@ -export const SCORE_RANGES = [ - { - value: 'No', - color: '#F26E6E' - }, - { - value: 'Partial', - color: '#F9A400' - }, - { - value: 'Yes', - color: '#17B091' - } -]; +export const SCORE_RANGES = { + No: '#F26E6E', + Partial: '#F9A400', + Yes: '#17B091' +}; + +export const VALUES = { No: [], Partial: [], Yes: [] };