Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Learning profile recharts poc #2831

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@coorpacademy-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"react-autosuggest": "^10.0.2",
"react-dropzone": "^9.0.0",
"react-tooltip": "4.5.1",
"recharts": "^2.12.2",
"uuid": "^9.0.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import React, {useCallback, useEffect, useState} from 'react';
import PropTypes from 'prop-types';
import {
Radar,
RadarChart,
PolarGrid,
PolarAngleAxis,
ResponsiveContainer,
Tooltip,
PolarRadiusAxis
} from 'recharts';
import {find, findIndex, get} from 'lodash/fp';
import classnames from 'classnames';
import style from './style.css';

const Gradient = ({type}) => (
<defs>
<linearGradient id={`${type}-gradient`} x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#0062ffff" />
<stop offset="100%" stopColor={type === 'fill' ? '#8000ff85' : '#8000FF'} />
</linearGradient>
</defs>
);

// TICK_POSITIONS
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TICK_POSITIONS
/* TICK_POSITIONS */

Utilise plutot le block comment pour délimiter tes sections
(et NE CRIE PAS STEUPLE 😅 )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂

const top = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: si ça rentre je rendrai ce block plus compact:

/* Ticks ** */
const top = {offset: {x: -100, y: -65}, alignment: 'center', margin: 'auto'};
const bottom = {/*...*/};
const right = {/*...*/};
const left = {/*...*/};

offset: {x: -100, y: -65},
alignment: 'center',
margin: 'auto'
};

const bottom = {
offset: {x: -100, y: 10},
alignment: 'center',
margin: 'auto'
};

const right = {
offset: {x: 30, y: -10},
alignment: 'start',
marginRigth: 'auto'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this ain't right 🙈

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😅

};

const left = {
offset: {x: -230, y: -10},
alignment: 'end',
marginLeft: 'auto'
};

const CHART_TYPES = {
hexagon: 'hexagon',
pentagon: 'pentagon',
quadrilateral: 'quadrilateral',
triangle: 'triangle'
};

const TICK_CUSTOM_STYLE = {
[CHART_TYPES.hexagon]: {
0: top,
1: right,
2: right,
3: bottom,
4: left,
5: left
},
[CHART_TYPES.pentagon]: {
0: top,
1: right,
2: right,
3: left,
4: left
},
[CHART_TYPES.quadrilateral]: {
0: top,
1: right,
2: bottom,
3: left
},
[CHART_TYPES.triangle]: {
0: top,
1: right,
2: left
}
};

const buildCustomTick = (index, x, y, currentValue, label, activeDot, chartType) => {
const isDotActive = activeDot === index;
const {
offset: {x: offsetX, y: offsetY},
alignment,
...rest
} = get([chartType, index], TICK_CUSTOM_STYLE);

return (
<g>
<foreignObject x={x + offsetX} y={y + offsetY} width="200" height="65">
<div
className={classnames(style.tickWrapper, isDotActive && style.tickWrapperFocus)}
style={{...rest, alignItems: alignment, textAlign: alignment}}
>
<span className={style.tickValue}>{currentValue}%</span>
<span className={style.tickLabel}>{label}</span>
</div>
</foreignObject>
</g>
);
};

const CustomDot = ({index, cx, cy, payload: {name}, onDotClick, activeDot}) => {
const defaultDotProps = {
cx,
cy,
r: 8,
stroke: 'url(#stroke-gradient)',
strokeWidth: 4,
strokeOpacity: 0.2,
fill: 'white',
onClick: () => onDotClick(name),
style: {cursor: 'pointer'}
};

const activeDotProps = {
...defaultDotProps,
r: 12,
strokeWidth: 6,
strokeOpacity: 0.5
};

const props = {
...defaultDotProps,
...(activeDot === index && activeDotProps)
};

return <circle {...props} />;
};

const LearningProfileRadarChart = ({data, onClick}, context) => {
const [isMobile, setIsMobile] = useState(false);
const [activeDot, setActiveDot] = useState(null);

const getIsMobile = useCallback(() => {
const userAgent = navigator.userAgent.toLowerCase();
const isMobile_ = /iphone|ipad|ipod|android|blackberry|windows phone/g.test(userAgent);
setIsMobile(isMobile_);
}, []);

useEffect(() => {
getIsMobile();
window.addEventListener('resize', getIsMobile);

return () => {
window.removeEventListener('resize', getIsMobile);
};
}, [getIsMobile]);

useEffect(() => {
const handleClick = () => {
setActiveDot(null);
};

activeDot !== null && window.addEventListener('click', handleClick);

return () => {
window.removeEventListener('click', handleClick);
};
}, [activeDot]);

function handleOnDotClick(label) {
const index = findIndex({subject: label}, data);
setActiveDot(index);
}

const getChartType = useCallback(() => {
switch (data.length) {
case 3:
return CHART_TYPES.triangle;
case 4:
return CHART_TYPES.quadrilateral;
case 5:
return CHART_TYPES.pentagon;
case 6:
return CHART_TYPES.hexagon;
}
}, [data]);
const chartType = getChartType();

function renderCustomTick({x, y, payload, index}) {
if (isMobile) return;

const {value: label} = payload;
const {value: currentValue} = find({subject: label}, data);

return buildCustomTick(index, x, y, currentValue, label, activeDot, chartType);
}

return (
<ResponsiveContainer width="100%" height="100%">
<RadarChart cx="50%" cy="50%" outerRadius="80%" data={data}>
<svg>
<Gradient type="fill" />
<Gradient type="stroke" />
</svg>
{/* possible to pass gridType="circle" */}
<PolarGrid strokeDasharray={15} strokeWidth={3} radialLines={false} />
<PolarAngleAxis dataKey="subject" tick={renderCustomTick} />
<PolarRadiusAxis tick={false} axisLine={false} domain={[0, 100]} />
<Radar
name="dataset-1"
dataKey="value"
stroke="url(#stroke-gradient)"
strokeWidth={6}
strokeOpacity={0.2}
fill="url(#fill-gradient)"
fillOpacity={0.2}
dot={<CustomDot onDotClick={handleOnDotClick} activeDot={activeDot} />}
activeDot={{stroke: '#0061FF', r: 6, strokeWidth: 4, fill: 'white'}}
/>
{isMobile ? <Tooltip cursor={false} /> : null}
</RadarChart>
</ResponsiveContainer>
);
};

LearningProfileRadarChart.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
subject: PropTypes.string,
value: PropTypes.number,
fullMark: PropTypes.number
})
),
onClick: PropTypes.func
};

CustomDot.propTypes = {
cx: PropTypes.number,
cy: PropTypes.number,
payload: PropTypes.shape({
name: PropTypes.string
}),
onDotClick: PropTypes.func,
index: PropTypes.number,
activeDot: PropTypes.number
};

Gradient.propTypes = {
type: PropTypes.string
};

export default LearningProfileRadarChart;
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

.tickWrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: end;
gap: 6px;
width: 100%;
height: 100%;
font-size: 14px;
font-family: 'Gilroy';
font-weight: 600;
padding: 0px 12px;
box-sizing: border-box;
}

.tickWrapperFocus {
background-color: #FAFAFA;
border-radius: 12px;
width: fit-content;
}

.tickValue {
color: #0061FF;
background: linear-gradient(to bottom, rgba(0, 97, 255, 0.1), rgba(128, 0, 255, 0.1));
padding: 4px;
border-radius: 12px;
}

.tickLabel {
text-overflow: ellipsis;
overflow: hidden;
white-space: normal;
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export default {
props: {
data: [
{
subject: 'Adaptability and resilience',
value: 15.6,
fullMark: 100
},
{
subject: 'Digital culture',
value: 43.8,
fullMark: 100
},
{
subject: 'Problem solving',
value: 56.4,
fullMark: 100
},
{
subject: 'Leadership',
value: 59.1,
fullMark: 100
},
{
subject: 'Time management',
value: 34.9,
fullMark: 100
},
{
subject: 'Sustainable thinking',
value: 82.3,
fullMark: 100
}
],
onClick: () => {
console.log('on click');
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export default {
props: {
data: [
{
subject: 'Adaptability and resilience',
value: 15.6,
fullMark: 100
},
{
subject: 'Digital culture',
value: 43.8,
fullMark: 100
},
{
subject: 'Problem solving',
value: 56.4,
fullMark: 100
},
{
subject: 'Leadership',
value: 59.1,
fullMark: 100
},
{
subject: 'Time management',
value: 34.9,
fullMark: 100
}
],
onClick: () => {
console.log('on click');
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export default {
props: {
data: [
{
subject: 'Adaptability and resilience',
value: 15.6,
fullMark: 100
},
{
subject: 'Digital culture',
value: 43.8,
fullMark: 100
},
{
subject: 'Problem solving',
value: 56.4,
fullMark: 100
},
{
subject: 'Leadership',
value: 59.1,
fullMark: 100
}
],
onClick: () => {
console.log('on click');
}
}
};
Loading