Skip to content

Commit

Permalink
Merge pull request #52 from MayGo/add-line-charts
Browse files Browse the repository at this point in the history
Add line charts
  • Loading branch information
MayGo authored Jun 16, 2019
2 parents c9160c7 + 8c547b7 commit 7f469ee
Show file tree
Hide file tree
Showing 14 changed files with 418 additions and 185 deletions.
52 changes: 52 additions & 0 deletions client/src/SummaryContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import moment from 'moment';
import * as React from 'react';
import { TrackItemService } from './services/TrackItemService';
import {
summariseLog,
summariseOnline,
summariseTimeOnline,
} from './components/SummaryCalendar/SummaryCalendar.util';

export const SummaryContext = React.createContext<any>({});

export const SummaryProvider = ({ children }) => {
const [isLoading, setIsLoading] = React.useState<any>(false);
const [selectedDate, setSelectedDate] = React.useState<any>(moment());
const [selectedMode, setSelectedMode] = React.useState<any>('month');

const [logSummary, setLogSummary] = React.useState<any>([]);
const [onlineSummary, setOnlineSummary] = React.useState<any>([]);
const [onlineTimesSummary, setOnlineTimesSummary] = React.useState<any>([]);

const loadData = React.useCallback(async () => {
setIsLoading(true);
const beginDate = moment(selectedDate).startOf(selectedMode);
const endDate = moment(selectedDate).endOf(selectedMode);

TrackItemService.findAllItems(beginDate, endDate).then(
({ appItems, statusItems, logItems }) => {
setLogSummary(summariseLog(logItems, selectedMode));
setOnlineSummary(summariseOnline(statusItems, selectedMode));
setOnlineTimesSummary(summariseTimeOnline(statusItems, selectedMode));
setIsLoading(false);
},
);
}, [selectedDate, selectedMode]);

const defaultContext = {
selectedDate,
setSelectedDate,
selectedMode,
setSelectedMode,
logSummary,
onlineSummary,
onlineTimesSummary,
isLoading,
};

React.useEffect(() => {
loadData();
}, [selectedDate, selectedMode]); // eslint-disable-line react-hooks/exhaustive-deps

return <SummaryContext.Provider value={defaultContext}>{children}</SummaryContext.Provider>;
};
77 changes: 77 additions & 0 deletions client/src/components/LineCharts/LineChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react';
import moment from 'moment';
import { VictoryBar, VictoryChart, VictoryAxis, VictoryTooltip } from 'victory';
import { convertDate, TIME_FORMAT, COLORS } from '../../constants';
import { chartTheme } from '../Timeline/ChartTheme';
import useWindowSize from '@rehooks/window-size';
import { SummaryContext } from '../../SummaryContext';
import {
addToTimeDuration,
formatToTimeEveryOther,
formatToDay,
toTimeDuration,
} from './LineChart.util';
import { diffAndFormatShort } from '../../utils';

const scale = { x: 'time', y: 'time' };
const padding = { left: 50, top: 0, bottom: 20, right: 10 };
const domainPadding = { y: 10, x: 10 };
const labelComponent = () => (
<VictoryTooltip
style={chartTheme.tooltip.style}
cornerRadius={chartTheme.tooltip.cornerRadius}
pointerLength={chartTheme.tooltip.pointerLength}
flyoutStyle={chartTheme.tooltip.flyoutStyle}
renderInPortal
horizontal={false}
/>
);
export const LineChart = () => {
const { innerWidth: chartWidth } = useWindowSize();
const { onlineTimesSummary } = React.useContext(SummaryContext);

return (
<VictoryChart
theme={chartTheme}
scale={scale}
width={chartWidth}
height={500}
domainPadding={domainPadding}
padding={padding}
horizontal
>
<VictoryAxis
orientation="bottom"
tickCount={24}
tickFormat={formatToTimeEveryOther}
dependentAxis
/>
<VictoryAxis orientation="left" name="time-axis" tickFormat={formatToDay} />
<VictoryBar
y={d => toTimeDuration(convertDate(d.beginDate), convertDate(d.beginDate))}
y0={d => toTimeDuration(convertDate(d.beginDate), convertDate(d.endDate))}
x={d => convertDate(d.beginDate).startOf('day')}
barWidth={10}
data={onlineTimesSummary}
labelComponent={labelComponent()}
labels={d =>
`Start time: ${convertDate(d.beginDate).format(
TIME_FORMAT,
)}\r\nEnd time: ${convertDate(d.endDate).format(
TIME_FORMAT,
)}\r\nDuration: ${diffAndFormatShort(d.beginDate, d.endDate)}`
}
/>
<VictoryBar
y={d => toTimeDuration(convertDate(d.beginDate), convertDate(d.beginDate))}
y0={d => addToTimeDuration(convertDate(d.beginDate), d.online)}
x={d => convertDate(d.beginDate).startOf('day')}
barWidth={10}
style={{ data: { fill: COLORS.green } }}
data={onlineTimesSummary}
labelComponent={labelComponent()}
labels={d => `Worked: ${moment.duration(d.online).format()}`}
/>
</VictoryChart>
);
};
45 changes: 45 additions & 0 deletions client/src/components/LineCharts/LineChart.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import moment from 'moment';
import { convertDate } from '../../constants';

export const generateTickValues = (date, ticks, unit, startOf) => {
const day = convertDate(date).startOf(startOf);
const dates = [...Array(ticks)].map((__, i) => {
return day.clone().add(i, unit);
});

return dates;
};

export const toTimeDuration = (from, to) => {
const start = moment(from).startOf('day');

return moment(moment(to).diff(start));
};
export const addToTimeDuration = (from, duration) => {
const start = moment(from).startOf('day');

return moment(moment(from).diff(start) + duration);
};

export const isOddHour = date => moment(date).get('hour') % 2;

export const formatToTime = t => moment.utc(t).format('HH:mm');

export const formatToTimeEveryOther = t => {
const hour = moment(t).startOf('hour');
return formatToTime(hour);
};

export const formatToDay = t => moment(t).format('DD');

export const timeTickValues = t => {
const ticks = 36;
const day = moment();
const dates = [...Array(ticks)].map((__, i) => {
return toTimeDuration(day, day.clone().add(i, 'hour'));
});

return dates;
};

export const dayTickValues = t => generateTickValues(t, 31, 'day', 'month');
6 changes: 6 additions & 0 deletions client/src/components/LineCharts/LineCharts.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import styled from 'styled-components';

export const Heading = styled.h3`
text-align: center;
color: rgba(0, 0, 0, 0.65);
`;
78 changes: 41 additions & 37 deletions client/src/components/SummaryCalendar/SummaryCalendar.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,40 @@
import { Flex } from '@rebass/grid';
import { Badge, Calendar, Spin } from 'antd';
import moment, { Moment } from 'moment';
import { Calendar, Spin, Icon } from 'antd';
import { Moment } from 'moment';
import * as React from 'react';
import useReactRouter from 'use-react-router';
import { TrackItemService } from '../../services/TrackItemService';
import { TimelineContext } from '../../TimelineContext';
import { SummaryContext } from '../../SummaryContext';
import { Spinner } from '../Timeline/Timeline.styles';
import { Item, TaskList } from './SummaryCalendar.styles';
import { summariseLog, summariseOnline } from './SummaryCalendar.util';
import { Logger } from '../../logger';
import { convertDate, TIME_FORMAT_SHORT } from '../../constants';
import { formatDuration } from './SummaryCalendar.util';

export const SummaryCalendar = () => {
const { setTimerange } = React.useContext(TimelineContext);
const { loadTimerange } = React.useContext(TimelineContext);
const {
selectedDate,
setSelectedDate,
selectedMode,
setSelectedMode,
logSummary,
onlineSummary,
onlineTimesSummary,
isLoading,
} = React.useContext(SummaryContext);

const { history } = useReactRouter();

const onDateSelect = (selectedDate: Moment | undefined) => {
const onDateSelect = (date: Moment | undefined) => {
const pathname = '/app/timeline';
if (selectedDate) {
setTimerange([selectedDate.startOf('day'), selectedDate.endOf('day')]);
if (date) {
loadTimerange([date.clone().startOf('day'), date.clone().endOf('day')]);
history.push(pathname);
} else {
Logger.error('No date');
}

// setPath
};
const [isLoading, setIsLoading] = React.useState<any>(false);
const [selectedDate, setSelectedDate] = React.useState<any>(moment());
const [selectedMode, setSelectedMode] = React.useState<any>('month');
const [logSummary, setLogSummary] = React.useState<any>([]);
const [onlineSummary, setOnlineSummary] = React.useState<any>([]);

React.useEffect(() => {
setIsLoading(true);
const beginDate = moment(selectedDate).startOf(selectedMode);
const endDate = moment(selectedDate).endOf(selectedMode);

TrackItemService.findAllItems(beginDate, endDate).then(
({ appItems, statusItems, logItems }) => {
setLogSummary(summariseLog(logItems, selectedMode));
setOnlineSummary(summariseOnline(statusItems, selectedMode));
setIsLoading(false);
},
);
}, [selectedDate, selectedMode]);

const changeSelectedDate = (date?: Moment, mode?: 'month' | 'year') => {
setSelectedDate(date);
Expand All @@ -53,18 +43,28 @@ export const SummaryCalendar = () => {

const getListData = day => {
const listData: any[] = [];
const worked = logSummary[day];
if (worked) {
const formattedDuration = moment.duration(worked).format();
listData.push({ type: 'warning', content: `Worked: ${formattedDuration}` });

const times = onlineTimesSummary[day];
if (times) {
listData.push({
type: 'coffee',
content: `Wake time ${convertDate(times.beginDate).format(TIME_FORMAT_SHORT)}`,
});
listData.push({
type: 'eye-invisible',
content: `Sleep time ${convertDate(times.endDate).format(TIME_FORMAT_SHORT)}`,
});
}

const online = onlineSummary[day];
if (online) {
const formattedDuration = moment.duration(online).format();
listData.push({ type: 'success', content: `Online: ${formattedDuration}` });
listData.push({ type: 'laptop', content: `Worked ${formatDuration(online)}` });
}

const worked = logSummary[day];
if (worked) {
listData.push({ type: 'tool', content: `Tasks ${formatDuration(worked)}` });
}
return listData || [];
};

Expand All @@ -75,7 +75,9 @@ export const SummaryCalendar = () => {
<TaskList>
{listData.map(item => (
<Item key={item.content}>
<Badge status={item.type} text={item.content} />
<Icon type={item.type} theme="outlined" />
{' '}
{item.content}
</Item>
))}
</TaskList>
Expand All @@ -90,7 +92,9 @@ export const SummaryCalendar = () => {
<TaskList>
{listData.map(item => (
<Item key={item.content}>
<Badge status={item.type} text={item.content} />
<Icon type={item.type} theme="outlined" />
{' '}
{item.content}
</Item>
))}
</TaskList>
Expand Down
45 changes: 44 additions & 1 deletion client/src/components/SummaryCalendar/SummaryCalendar.util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import _ from 'lodash';
import { convertDate } from '../../constants';
import {
convertDate,
TIME_FORMAT,
BREAKPOINT_TIME,
DURATION_FORMAT,
DURATION_SETTINGS,
} from '../../constants';
import moment from 'moment';

export const formatDuration = dur =>
moment.duration(dur).format(DURATION_FORMAT, DURATION_SETTINGS);

export const groupByField = mode => item =>
mode === 'month' ? convertDate(item.beginDate).date() : convertDate(item.beginDate).month();

export const groupByActualDay = item => {
const date = convertDate(item.beginDate);
const day = date.date();

if (date.format(TIME_FORMAT) < BREAKPOINT_TIME) {
return day === 1 ? day : day - 1;
}
return day;
};

export const summariseLog = (items, mode) => {
const data = {};

Expand All @@ -27,3 +47,26 @@ export const summariseOnline = (items, mode) => {
});
return data;
};

export const summariseTimeOnline = (items, mode) => {
if (mode === 'year') {
return [];
}
const data = _(items)
.filter(item => item.app === 'ONLINE')
.groupBy(groupByActualDay)
.map((value, key) => {
return {
beginDate: _.minBy(
value.filter(
item => convertDate(item.beginDate).format(TIME_FORMAT) > BREAKPOINT_TIME,
),
c => convertDate(c.beginDate),
).beginDate,
endDate: _.maxBy(value, c => convertDate(c.endDate)).endDate,
online: _.sumBy(value, c => convertDate(c.endDate).diff(convertDate(c.beginDate))),
};
})
.value();
return data;
};
Loading

0 comments on commit 7f469ee

Please sign in to comment.