Skip to content
Snippets Groups Projects
Commit 1531cfca authored by Guilhem CARRON's avatar Guilhem CARRON
Browse files

Merge branch 'feat/US591-Total-analysis-graph' into 'dev'

feat(analysis): Add new pie chart section

See merge request web-et-numerique/llle_project/ecolyo!472
parents 8713da47 20ecfec1
Branches
Tags
2 merge requests!477V1.4.3,!472feat(analysis): Add new pie chart section
......@@ -23,6 +23,7 @@ import StyledSpinner from 'components/CommonKit/Spinner/StyledSpinner'
import AnalysisErrorModal from './AnalysisErrorModal'
import { DateTime } from 'luxon'
import StyledCard from 'components/CommonKit/Card/StyledCard'
import TotalAnalysisChart from './TotalAnalysisChart'
interface MonthlyAnalysisProps {
analysisDate: DateTime
......@@ -126,19 +127,12 @@ const MonthlyAnalysis: React.FC<MonthlyAnalysisProps> = ({
</div>
</div>
<div className="analysis-content">
<StyledCard>
<span className="analysis-header text-16-normal-uppercase">
{t('analysis.analysis_date')}
</span>
<div>
<PerformanceIndicatorContent
performanceIndicator={aggregatedPerformanceIndicators}
timeStep={timeStep}
fluidLackOfData={fluidLackOfData}
analysisDate={analysisDate}
/>
</div>
</StyledCard>
<div className="card total-card">
<TotalAnalysisChart
analysisDate={analysisDate}
fluidTypes={fluidTypes}
/>
</div>
</div>
<div className="analysis-content">
<StyledCard>
......
import React from 'react'
import { mount } from 'enzyme'
import { DateTime } from 'luxon'
import { globalStateData } from '../../../tests/__mocks__/globalStateData.mock'
import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'
import PieChart from './PieChart'
jest.mock('cozy-ui/transpiled/react/I18n', () => {
return {
useI18n: jest.fn(() => {
return {
t: (str: string) => str,
}
}),
}
})
const mockStore = configureStore([])
describe('PieChart component', () => {
it('should be rendered correctly', () => {
const store = mockStore({
ecolyo: {
global: globalStateData,
},
})
const wrapper = mount(
<Provider store={store}>
<PieChart
width={300}
height={300}
outerRadius={300}
innerRadius={300}
currentAnalysisDate={DateTime.fromISO('2021-07-01T00:00:00.000Z', {
zone: 'utc',
})}
totalValue={60}
dataArray={[10, 20, 30]}
/>
</Provider>
).getElement()
expect(wrapper).toMatchSnapshot()
})
it('should open estimation modal', () => {
const store = mockStore({
ecolyo: {
global: globalStateData,
},
})
const wrapper = mount(
<Provider store={store}>
<PieChart
width={300}
height={300}
outerRadius={300}
innerRadius={300}
currentAnalysisDate={DateTime.fromISO('2021-07-01T00:00:00.000Z', {
zone: 'utc',
})}
totalValue={60}
dataArray={[10, 20, 30]}
/>
</Provider>
)
expect(wrapper.find('.estimation-text').simulate('click'))
})
})
import React, { useCallback, useEffect, useRef, useState } from 'react'
import './totalAnalysisChart.scss'
import * as d3 from 'd3'
import { useI18n } from 'cozy-ui/transpiled/react/I18n'
import { formatNumberValues } from 'utils/utils'
import { DateTime } from 'luxon'
import { convertDateToMonthString } from 'utils/date'
import EstimatedConsumptionModal from 'components/ConsumptionVisualizer/EstimatedConsumptionModal'
interface PieProps {
innerRadius: number
outerRadius: number
dataArray: number[]
width: number
height: number
totalValue: number
currentAnalysisDate: DateTime
}
const PieChart: React.FC<PieProps> = ({
innerRadius,
outerRadius,
dataArray,
width,
height,
totalValue,
currentAnalysisDate,
}: PieProps) => {
const ref = useRef(null)
const { t } = useI18n()
const createPie = d3.pie().sort(null)
const arcWidth = outerRadius - innerRadius
const [openEstimationModal, setOpenEstimationModal] = useState<boolean>(false)
const toggleEstimationModal = useCallback(() => {
setOpenEstimationModal(prev => !prev)
}, [])
useEffect(() => {
const data = createPie(dataArray)
const group = d3.select(ref.current)
const groupWithData = group.selectAll('g.arc').data(data)
const colors = ['#D87B39', '#3A98EC', '#45D1B8']
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createArc: any = d3
.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
groupWithData.exit().remove()
const groupWithUpdate = groupWithData
.enter()
.append('g')
.attr('class', 'arc')
.attr('filter', 'url(#glow)')
const path = groupWithUpdate
.append('path')
.merge(groupWithData.select('path.arc'))
path
.attr('class', 'arc')
.attr('d', createArc)
.attr('fill', (d, i) => colors[i])
}, [createPie, dataArray, innerRadius, outerRadius])
return (
<div
className="pie-container"
style={{
width: width,
height: height,
}}
>
<svg width={width} height={height}>
<defs>
<filter id="glow" height="300%" width="300%" x="-75%" y="-75%">
<feGaussianBlur stdDeviation="10" result="coloredBlur" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<g ref={ref} transform={`translate(${outerRadius} ${outerRadius})`} />
</svg>
<div
className="pie-center"
style={{
width: width - arcWidth,
height: height - arcWidth,
top: arcWidth / 2,
left: arcWidth - arcWidth / 2,
}}
>
<div className="text-36-bold">
{formatNumberValues(totalValue)}
<span className="euro-unit">{`${t('FLUID.MULTIFLUID.UNIT')}`}</span>
</div>
<div className="text-16-normal date">
{t('analysis_pie.month') +
convertDateToMonthString(currentAnalysisDate).substring(3)}
</div>
<div
className="text-14-normal estimation-text"
onClick={toggleEstimationModal}
>
<span className="estimated">{t('analysis_pie.estimation')}</span>
<span className="estimated">{t('analysis_pie.estimation2')}</span>
</div>
<div
className="circle"
style={{
width: width - arcWidth * 2,
height: height - arcWidth * 2,
top: arcWidth / 2,
left: arcWidth - arcWidth / 2,
}}
></div>
</div>
<EstimatedConsumptionModal
open={openEstimationModal}
handleCloseClick={toggleEstimationModal}
/>
</div>
)
}
export default PieChart
import React from 'react'
import { mount } from 'enzyme'
import { DateTime } from 'luxon'
import { globalStateData } from '../../../tests/__mocks__/globalStateData.mock'
import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'
import TotalAnalysisChart from './TotalAnalysisChart'
import { FluidType } from 'enum/fluid.enum'
import { graphMonthData } from '../../../tests/__mocks__/datachartData.mock'
import { act } from 'react-dom/test-utils'
jest.mock('cozy-ui/transpiled/react/I18n', () => {
return {
useI18n: jest.fn(() => {
return {
t: (str: string) => str,
}
}),
}
})
const mockgetGraphData = jest.fn()
jest.mock('services/consumption.service', () => {
return jest.fn(() => {
return {
getGraphData: mockgetGraphData,
}
})
})
const mockStore = configureStore([])
describe('TotalAnalysisChart component', () => {
it('should be rendered correctly', () => {
const store = mockStore({
ecolyo: {
global: globalStateData,
},
})
const wrapper = mount(
<Provider store={store}>
<TotalAnalysisChart
analysisDate={DateTime.fromISO('2021-07-01T00:00:00.000Z', {
zone: 'utc',
})}
fluidTypes={[FluidType.ELECTRICITY]}
/>
</Provider>
).getElement()
expect(wrapper).toMatchSnapshot()
})
it('should render several fluids and display month data', async () => {
const store = mockStore({
ecolyo: {
global: globalStateData,
},
})
mockgetGraphData.mockResolvedValueOnce(graphMonthData)
const wrapper = mount(
<Provider store={store}>
<TotalAnalysisChart
analysisDate={DateTime.fromISO('2021-07-01T00:00:00.000Z', {
zone: 'utc',
})}
fluidTypes={[FluidType.ELECTRICITY, FluidType.WATER, FluidType.GAS]}
/>
</Provider>
)
await act(async () => {
await new Promise(resolve => setTimeout(resolve))
wrapper.update()
})
expect(wrapper.find('.fluidconso').exists()).toBeTruthy()
})
it('should render empty price', async () => {
const store = mockStore({
ecolyo: {
global: globalStateData,
},
})
const emptyData = {
actualData: [
{
date: DateTime.fromISO('2020-09-01T00:00:00.000Z', {
zone: 'utc',
}),
value: 69.18029999999999,
valueDetail: [-1],
},
],
}
mockgetGraphData.mockResolvedValueOnce(emptyData)
const wrapper = mount(
<Provider store={store}>
<TotalAnalysisChart
analysisDate={DateTime.fromISO('2021-07-01T00:00:00.000Z', {
zone: 'utc',
})}
fluidTypes={[FluidType.ELECTRICITY]}
/>
</Provider>
)
await act(async () => {
await new Promise(resolve => setTimeout(resolve))
wrapper.update()
})
expect(wrapper.find('.fluidconso').text()).toBe('--- €')
})
})
import React, { useState, useEffect } from 'react'
import './analysisView.scss'
import { DateTime } from 'luxon'
import { useClient } from 'cozy-client'
import ConsumptionDataManager from 'services/consumption.service'
import { FluidType } from 'enum/fluid.enum'
import { TimeStep } from 'enum/timeStep.enum'
import { TimePeriod } from 'models'
import PieChart from './PieChart'
import './totalAnalysisChart.scss'
import { getNavPicto } from 'utils/picto'
import Icon from 'cozy-ui/transpiled/react/Icon'
import { formatNumberValues } from 'utils/utils'
import { useI18n } from 'cozy-ui/transpiled/react/I18n'
interface TotalAnalysisChartProps {
analysisDate: DateTime
fluidTypes: FluidType[]
}
const TotalAnalysisChart: React.FC<TotalAnalysisChartProps> = ({
analysisDate,
fluidTypes,
}: TotalAnalysisChartProps) => {
const [dataLoadArray, setDataLoadArray] = useState<number[] | null>(null)
const [totalLoadValue, setTotalLoadValue] = useState<number>(0)
const client = useClient()
const { t } = useI18n()
const arcWidth = 30
const radius = Math.min(375, innerWidth - 100)
const outerRadius = radius / 2
const innerRadius = outerRadius - arcWidth
useEffect(() => {
let subscribed = true
async function getTotalData() {
const timePeriod: TimePeriod = {
startDate: analysisDate.minus({ month: 1 }).startOf('month'),
endDate: analysisDate.minus({ month: 1 }).endOf('month'),
}
const consumptionService = new ConsumptionDataManager(client)
const monthTotalData = await consumptionService.getGraphData(
timePeriod,
TimeStep.MONTH,
fluidTypes,
undefined,
true
)
if (monthTotalData && monthTotalData.actualData) {
setDataLoadArray(monthTotalData.actualData[0].valueDetail)
setTotalLoadValue(monthTotalData.actualData[0].value)
}
}
if (subscribed) {
getTotalData()
}
return () => {
subscribed = false
}
}, [analysisDate, client, fluidTypes])
return (
<div className="totalAnalysis-container">
{fluidTypes.length >= 2 && (
<div className="text-24-normal title">{t('analysis_pie.total')}</div>
)}
{dataLoadArray && (
<PieChart
dataArray={dataLoadArray}
totalValue={totalLoadValue}
width={radius}
height={radius}
innerRadius={innerRadius}
outerRadius={outerRadius}
currentAnalysisDate={analysisDate
.minus({ month: 1 })
.startOf('month')}
/>
)}
{dataLoadArray && (
<div className="total-card-container">
{dataLoadArray.map((load, index) => {
return (
<div key={index} className="total-card">
<div className="text-20-bold fluidconso">
{load !== -1 ? `${formatNumberValues(load)} €` : '--- €'}
</div>
<Icon
className="euro-fluid-icon"
icon={getNavPicto(index, true, true)}
size={38}
/>
<div className="text-16-normal">
{t('FLUID.' + FluidType[index] + '.LABEL')}
</div>
</div>
)
})}
</div>
)}
</div>
)
}
export default TotalAnalysisChart
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PieChart component should be rendered correctly 1`] = `
<Provider
store={
Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}
>
<PieChart
currentAnalysisDate={"2021-07-01T00:00:00.000Z"}
dataArray={
Array [
10,
20,
30,
]
}
height={300}
innerRadius={300}
outerRadius={300}
totalValue={60}
width={300}
/>
</Provider>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TotalAnalysisChart component should be rendered correctly 1`] = `
<Provider
store={
Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}
>
<TotalAnalysisChart
analysisDate={"2021-07-01T00:00:00.000Z"}
fluidTypes={
Array [
0,
]
}
/>
</Provider>
`;
......@@ -40,6 +40,9 @@
margin: 0;
}
}
.total-card {
padding: 0;
}
}
.analysis-container-spinner {
position: absolute;
......
@import '../../styles/base/color';
@import '../../styles/base/breakpoint';
.totalAnalysis-container {
display: flex;
justify-content: center;
flex-direction: column;
color: white;
.title {
text-align: center;
color: $grey-bright;
margin: 1.5rem 0;
}
.pie-container {
text-align: center;
position: relative;
margin: auto;
}
svg {
margin: auto;
overflow: visible;
}
.pie-center {
box-sizing: border-box;
position: absolute;
border-radius: 50%;
background: $dark-light-2;
z-index: 5;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.euro-unit {
margin-left: 0.6rem;
}
.date {
text-transform: uppercase;
color: $soft-grey;
display: inline-block;
margin: 0.5rem 0;
}
.estimated {
display: block;
text-decoration: underline;
}
.circle {
box-sizing: border-box;
border: 1px solid $grey-dark;
position: absolute;
border-radius: 50%;
background: transparent;
z-index: 6;
padding: 1rem;
@media screen and (max-width: 345px) {
display: none;
}
}
}
.total-card-container {
display: flex;
margin: auto;
justify-content: center;
margin-top: 1rem;
.total-card {
margin: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
svg {
margin: 0.7rem 0;
}
}
}
.text-36-bold {
@media screen and (max-width: 345px) {
font-size: 1.6rem;
}
}
.date {
@media screen and (max-width: 345px) {
font-size: 1rem;
}
}
.text-20-bold {
@media screen and (max-width: 345px) {
font-size: 1rem;
}
}
}
......@@ -97,6 +97,12 @@
"button_goto_konnector": "Aller aux connecteurs"
}
},
"analysis_pie": {
"total": "Conso totale",
"month": "Au mois de ",
"estimation": "Comment sont estimés",
"estimation2": "les prix ?"
},
"auth": {
"enedisgrandlyon": {
"connect": {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment