Skip to content

Commit

Permalink
feat: Add DynamicRadar component. (#207)
Browse files Browse the repository at this point in the history
This component consists of 3 sub components:
 - Radar
 - SimpleTable
 - ControlBar
  • Loading branch information
tyn1998 authored Aug 13, 2021
1 parent b928c11 commit 6b41263
Show file tree
Hide file tree
Showing 4 changed files with 394 additions and 0 deletions.
96 changes: 96 additions & 0 deletions src/components/DynamicRadar/ControlBar/ControlBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useState } from 'react';
import { Stack, Slider, initializeIcons, IconButton } from '@fluentui/react';
import {
IStyleFunctionOrObject,
ISliderStyleProps,
ISliderStyles,
} from '@fluentui/react';

initializeIcons();

interface ControlBarProps {
theme: 'light' | 'dark';
play: Function;
pause: Function;
setIndex: Function;
sliderValue: number;
sliderMax: number;
}

const ControlBar: React.FC<ControlBarProps> = (props) => {
const { theme, play, pause, setIndex, sliderValue, sliderMax } = props;
const [ifPlay, setIfPlay] = useState(false);

const sliderStyle: IStyleFunctionOrObject<ISliderStyleProps, ISliderStyles> =
{
thumb: {
background: theme == 'light' ? 'darkblue' : '#58a6ff',
borderRadius: 0,
height: '14px',
top: '-5px',
width: '6px',
borderWidth: '0px',
},
activeSection: {
background: theme == 'light' ? 'rgb(200, 198, 196)' : '#58a6ff',
},
inactiveSection: {
background: theme == 'light' ? 'rgb(237, 235, 233)' : 'white',
},
};

const previous = () => {
setIndex((idx: number) => (idx - 1 < 0 ? 0 : idx - 1));
};

const next = () => {
setIndex((idx: number) => (idx + 1 > sliderMax ? sliderMax : idx + 1));
};

return (
<div style={{ width: '91%', marginLeft: '9%' }}>
<Stack horizontal>
<IconButton
iconProps={{
iconName: ifPlay ? 'CirclePauseSolid' : 'MSNVideosSolid',
}}
styles={{
icon: { color: theme == 'light' ? 'darkblue' : '#58a6ff' },
}}
onClick={() => {
ifPlay ? pause() : play();
setIfPlay(!ifPlay);
}}
/>
<IconButton
iconProps={{ iconName: 'ChevronLeftSmall' }}
styles={{
icon: { color: theme == 'light' ? 'darkblue' : '#58a6ff' },
}}
onClick={previous}
/>
<Stack.Item grow align="center">
<Slider
styles={sliderStyle}
min={0}
max={sliderMax}
step={1}
value={sliderValue}
showValue={false}
snapToStep
onChange={(value) => setIndex(value)}
/>
</Stack.Item>
<IconButton
iconProps={{ iconName: 'ChevronRightSmall' }}
styles={{
icon: { color: theme == 'light' ? 'darkblue' : '#58a6ff' },
}}
onClick={next}
/>
</Stack>
</div>
);
};

export default ControlBar;
110 changes: 110 additions & 0 deletions src/components/DynamicRadar/DynamicRadar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useEffect, useState, useRef } from 'react';
import { Stack } from '@fluentui/react';
import Radar from './Radar/Radar';
import SimpleTable from './SimpleTable/SimpleTable';
import ControlBar from './ControlBar/ControlBar';

const LEFT_TOP_WIDTH_PERCENT = '70%';
const RIGHT_TOP_WIDTH_PERCENT = '30%';
const TOP_HEIGHT_PERCENT = 0.9;

interface DynamicRadarDataItem {
date: string;
values: number[];
}

interface DynamicRadarProps {
theme: 'light' | 'dark';
width: number;
height: number;
indicators: string[];
data: DynamicRadarDataItem[];
}

const DynamicRadar: React.FC<DynamicRadarProps> = (props) => {
const { theme, width, height, indicators, data } = props;
const [idx, setIdx] = useState(0);
const timer: { current: NodeJS.Timeout | null } = useRef(null);
const maxScales = findMaxScales(indicators, data);

const tick = () => {
setIdx((idx) => (idx + 1 > data.length - 1 ? 0 : idx + 1));
};

const play = () => {
clearInterval(timer.current as NodeJS.Timeout);
timer.current = setInterval(tick, 500);
};

const pause = () => {
clearInterval(timer.current as NodeJS.Timeout);
};

useEffect(() => {
console.log('DynamicRadar (re)rendered!');
});

return (
<Stack verticalAlign="space-evenly" styles={{ root: { width, height } }}>
<Stack horizontal>
<div
style={{
width: LEFT_TOP_WIDTH_PERCENT,
height: TOP_HEIGHT_PERCENT * height,
}}
>
<Radar
{...{
theme,
height: TOP_HEIGHT_PERCENT * height,
indicators,
maxScales,
values: data[idx].values,
}}
/>
</div>
<Stack
verticalAlign="space-evenly"
styles={{
root: {
width: RIGHT_TOP_WIDTH_PERCENT,
height: TOP_HEIGHT_PERCENT * height,
},
}}
>
<SimpleTable
{...{
theme: theme,
title: data[idx].date,
keys: indicators,
values: data[idx].values,
}}
/>
</Stack>
</Stack>
<ControlBar
theme={theme}
play={play}
pause={pause}
setIndex={setIdx}
sliderValue={idx}
sliderMax={data.length - 1}
/>
</Stack>
);
};

const findMaxScales = (indicators: string[], data: DynamicRadarDataItem[]) => {
let maxScales = new Array(indicators.length).fill(0);
data.forEach((item) => {
let values = item.values;
for (let i = 0; i < indicators.length; i++) {
if (values[i] > maxScales[i]) {
maxScales[i] = values[i];
}
}
});
return maxScales.map((v) => Math.ceil(v * 1.1));
};

export default DynamicRadar;
103 changes: 103 additions & 0 deletions src/components/DynamicRadar/Radar/Radar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { useEffect, useRef } from 'react';
import * as echarts from 'echarts';

interface RadarProps {
theme: 'light' | 'dark';
height: number;
indicators: string[];
maxScales: number[];
values: number[];
}

const Radar: React.FC<RadarProps> = (props) => {
const { theme, height, indicators, maxScales, values } = props;
const divEL = useRef(null);

const option = {
legend: undefined,
tooltip: undefined,
radar: {
shape: 'circle',
indicator: indicators.map((item: any, index: any) => {
return { name: item, max: maxScales[index] };
}),
center: ['50%', '50%'],
splitNumber: 3,
name: {
textStyle: {
color: theme === 'light' ? '#010409' : '#f0f6fc',
fontSize: 13,
textShadowColor: theme === 'light' ? '#010409' : '#f0f6fc',
textShadowBlur: 0.01,
textShadowOffsetX: 0.01,
textShadowOffsetY: 0.01,
},
},
splitArea: {
areaStyle: {
color: [theme == 'light' ? 'darkblue' : '#58a6ff'],
opacity: 1,
},
},
axisLine: {
lineStyle: {
width: 2,
color: 'white',
opacity: 0.4,
},
},
splitLine: {
lineStyle: {
color: 'white',
width: 1.5,
opacity: 0.4,
},
},
},
series: [
{
type: 'radar',
symbol: 'none',
itemStyle: {},
lineStyle: {
width: 1.5,
color: 'white',
opacity: 1,
shadowBlur: 4,
shadowColor: 'white',
shadowOffsetX: 0.5,
shadowOffsetY: 0.5,
},
areaStyle: {
color: 'white',
opacity: 0.8,
},
data: [
{
value: values,
},
],
},
],
animation: true,
animationDurationUpdate: 200,
};
useEffect(() => {
let chartDOM = divEL.current;
const instance = echarts.init(chartDOM as any);

return () => {
instance.dispose();
};
}, []);

useEffect(() => {
let chartDOM = divEL.current;
const instance = echarts.getInstanceByDom(chartDOM as any);
instance.setOption(option);
}, [indicators, values]);

return <div ref={divEL} style={{ width: '100%', height }}></div>;
};

export default Radar;
85 changes: 85 additions & 0 deletions src/components/DynamicRadar/SimpleTable/SimpleTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import { Stack } from '@fluentui/react';

interface SimpleTableProps {
theme: 'light' | 'dark';
title: string;
keys: string[];
values: number[];
}

const SimpleTable: React.FC<SimpleTableProps> = (props) => {
const { theme, title, keys, values } = props;

const tableContainerStyle: React.CSSProperties = {
width: '90%',
marginLeft: '5%',
marginRight: '5%',
};

const tableTitleStyle: React.CSSProperties = {
margin: 0,
marginBottom: 5,
color: theme == 'light' ? '#010409' : '#f0f6fc',
};

const tableLineStyle: React.CSSProperties = {
height: '2px',
backgroundColor: theme == 'light' ? 'darkblue' : '#58a6ff',
marginTop: '1px',
marginBottom: '1px',
};

const tableKeyStyle: React.CSSProperties = {
fontSize: 15,
color: theme == 'light' ? '#010409' : '#f0f6fc',
fontWeight: 'bold',
};

const tableValueStyle: React.CSSProperties = {
fontSize: 15,
color: theme == 'light' ? '#010409' : '#f0f6fc',
fontStyle: 'italic',
};

return (
<div style={tableContainerStyle}>
<Stack>
<Stack.Item align="center">
<h3 style={tableTitleStyle}>{title}</h3>
</Stack.Item>
</Stack>
<div style={tableLineStyle} />
{keys.map((item: any, index: any) => {
return (
<Stack
horizontal
horizontalAlign="space-between"
key={`keys-${index}`}
>
<div style={tableKeyStyle}>{item}</div>
<div style={tableValueStyle}>{numFormat(values[index], 1)}</div>
</Stack>
);
})}
<div style={tableLineStyle} />
</div>
);
};

const numFormat = (num: number, digits: number) => {
let si = [
{ value: 1, symbol: '' },
{ value: 1e3, symbol: 'k' },
];
let rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
let i;
for (i = si.length - 1; i > 0; i--) {
if (num >= si[i].value) {
break;
}
}
return (num / si[i].value).toFixed(digits).replace(rx, '$1') + si[i].symbol;
};

export default SimpleTable;

0 comments on commit 6b41263

Please sign in to comment.