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

feat: Add DynamicRadar component. #207

Merged
merged 1 commit into from
Aug 13, 2021
Merged
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
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;