Skip to content

Commit 2eef350

Browse files
Further Time Tacking Improvement (#4121)
* duration of time tracking view is now in mins and secs * refactored time tracking view and added spinning wheel when loading * moved time tracking chart into an own file; fixed wrong positioned tooltip bug * fixed flow errors * displaying chart now at full size always * fixed tooltip positioning * Update CHANGELOG.md * added fixed height to chart again to avoid bugs; fixed bug that caused the tooltip not to be rendered * adding mispositioned tooltip at window borders fix * fixed the tooltip edgecase positioning bug
1 parent dc7a17c commit 2eef350

File tree

6 files changed

+174
-43
lines changed

6 files changed

+174
-43
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md).
3636
- Added an two additional buttons to the dropdown menu of the tree hierarchy view. On Click, one collapses the other expands all subgroups. [#4143](https://github.com/scalableminds/webknossos/pull/4143)
3737

3838
### Changed
39+
- The tooltip of the timeline chart in the Time Tracking view now displays the duration in minutes:seconds. [#4121](https://github.com/scalableminds/webknossos/pull/4121)
3940
- Reactivated and renamed the "Quality" setting to "Hardware Utilization". Using a higher value will render data in higher quality, but puts more stress on the user's hardware and bandwidth. [#4142](https://github.com/scalableminds/webknossos/pull/4142)
4041

42+
4143
### Fixed
4244
- Fixed that team managers couldn't view time tracking details of other users anymore. [#4125](https://github.com/scalableminds/webknossos/pull/4125)
45+
- Fixed the positioning of the tooltip of the timeline chart in the Time Tracking view. [#4121](https://github.com/scalableminds/webknossos/pull/4121)
4346
- Fixed a rendering problem which caused a red viewport on some Windows machines. [#4133](https://github.com/scalableminds/webknossos/pull/4133)
4447

4548
### Removed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// @flow
2+
import { Chart } from "react-google-charts";
3+
import * as React from "react";
4+
import { getWindowBounds } from "libs/utils";
5+
6+
export type ColumnDefinition = {
7+
id?: string,
8+
type: string,
9+
role?: string,
10+
p?: Object,
11+
};
12+
13+
export type RowContent = [string, string, string, Date, Date];
14+
15+
export type DateRange = [moment$Moment, moment$Moment];
16+
17+
type Props = {
18+
columns: Array<ColumnDefinition>,
19+
rows: Array<RowContent>,
20+
timeAxisFormat: string,
21+
dateRange: DateRange,
22+
};
23+
24+
export default class TimeTrackingChart extends React.PureComponent<Props> {
25+
additionalCSS: ?HTMLStyleElement = null;
26+
chartScrollElement: ?HTMLElement = null;
27+
28+
componentWillUnmount() {
29+
if (this.chartScrollElement) {
30+
this.chartScrollElement.removeEventListener("mousemove", this.adjustTooltipPosition);
31+
}
32+
}
33+
34+
/* We need to adjust the tooltips position manually because it is not positioned correctly when scrolling down.
35+
* This fix was suggested by
36+
* https://stackoverflow.com/questions/52755733/google-charts-tooltips-have-wrong-position-when-inside-a-scrolling-container.
37+
* The fix is modified so that it sets the tooltip directly next to the mouse. This is done by using the total
38+
* coordinates of the mouse of the whole window (clientX/Y) and manipulating the style of the tooltip directly. */
39+
applyTooltipPositioningFix = () => {
40+
// TimeLineGraph is the name of the chart given by the library.
41+
this.chartScrollElement = document.querySelector(
42+
"#TimeLineGraph > div > div:first-child > div",
43+
);
44+
if (this.chartScrollElement) {
45+
this.chartScrollElement.addEventListener("mousemove", this.adjustTooltipPosition);
46+
}
47+
};
48+
49+
adjustTooltipPosition = (event: MouseEvent) => {
50+
const tooltip = document.getElementsByClassName("google-visualization-tooltip")[0];
51+
if (tooltip != null) {
52+
const { clientX, clientY } = event;
53+
const [clientWidth, clientHeight] = getWindowBounds();
54+
const { offsetHeight, offsetWidth } = tooltip;
55+
if (clientY + offsetHeight >= clientHeight) {
56+
// The tooltip needs to be displayed above the mouse.
57+
tooltip.style.top = `${clientY - offsetHeight}px`;
58+
} else {
59+
// The tooltip can be displayed below the mouse.
60+
tooltip.style.top = `${clientY}px`;
61+
}
62+
if (clientX + offsetWidth >= clientWidth) {
63+
// The tooltip needs to be displayed on the left side of the mouse.
64+
tooltip.style.left = `${clientX - offsetWidth - 5}px`;
65+
} else {
66+
// The tooltip needs to be displayed on the right side of the mouse.
67+
tooltip.style.left = `${clientX + 15}px`;
68+
}
69+
// We need to make the tooltip visible again after adjusting the position.
70+
// It is initially invisible as it is mispositioned by the library and would then "beam" to the corrected
71+
// position. We want to avoid that "beaming" behaviour.
72+
tooltip.style.visibility = "visible";
73+
}
74+
};
75+
76+
render() {
77+
const { columns, rows, timeAxisFormat, dateRange } = this.props;
78+
79+
const { applyTooltipPositioningFix } = this;
80+
81+
return (
82+
<Chart
83+
chartType="Timeline"
84+
columns={columns}
85+
rows={rows}
86+
options={{
87+
timeline: { singleColor: "#108ee9" },
88+
// Workaround for google-charts bug, see https://github.com/scalableminds/webknossos/pull/3772
89+
hAxis: {
90+
format: timeAxisFormat,
91+
minValue: dateRange[0].toDate(),
92+
maxValue: dateRange[1].toDate(),
93+
},
94+
allowHtml: true,
95+
}}
96+
graph_id="TimeLineGraph"
97+
chartPackages={["timeline"]}
98+
width="100%"
99+
height="600px"
100+
legend_toggle
101+
chartEvents={[
102+
{
103+
eventName: "ready",
104+
callback() {
105+
// After the whole chart is drawn, we can now apply the position fixing workaround.
106+
applyTooltipPositioningFix();
107+
},
108+
},
109+
]}
110+
/>
111+
);
112+
}
113+
}

frontend/javascripts/admin/time/time_line_view.js

+26-33
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// @flow
2-
import { Chart } from "react-google-charts";
3-
import { Select, Card, Form, Row, Col, DatePicker } from "antd";
2+
import { Select, Card, Form, Row, Col, DatePicker, Spin } from "antd";
43
import * as React from "react";
54
import ReactDOMServer from "react-dom/server";
65
import { connect } from "react-redux";
@@ -9,12 +8,17 @@ import moment from "moment";
98
import FormattedDate from "components/formatted_date";
109
import { type OxalisState } from "oxalis/store";
1110
import type { APIUser, APITimeTracking } from "admin/api_flow_types";
12-
import { formatMilliseconds, formatDurationToHoursAndMinutes } from "libs/format_utils";
11+
import { formatMilliseconds, formatDurationToMinutesAndSeconds } from "libs/format_utils";
1312
import { isUserAdmin } from "libs/utils";
1413
import { getEditableUsers, getTimeTrackingForUser } from "admin/admin_rest_api";
1514
import Toast from "libs/toast";
1615
import messages from "messages";
1716
import { enforceActiveUser } from "oxalis/model/accessors/user_accessor";
17+
import TimeTrackingChart, {
18+
type DateRange,
19+
type ColumnDefinition,
20+
type RowContent,
21+
} from "./time_line_chart_view";
1822

1923
const FormItem = Form.Item;
2024
const { Option } = Select;
@@ -29,8 +33,6 @@ type TimeTrackingStats = {
2933
averageTimePerTask: number,
3034
};
3135

32-
type DateRange = [moment$Moment, moment$Moment];
33-
3436
type StateProps = {|
3537
activeUser: APIUser,
3638
|};
@@ -43,6 +45,7 @@ type State = {
4345
dateRange: DateRange,
4446
timeTrackingData: Array<APITimeTracking>,
4547
stats: TimeTrackingStats,
48+
isLoading: boolean,
4649
};
4750

4851
class TimeLineView extends React.PureComponent<Props, State> {
@@ -56,6 +59,7 @@ class TimeLineView extends React.PureComponent<Props, State> {
5659
numberTasks: 0,
5760
averageTimePerTask: 0,
5861
},
62+
isLoading: false,
5963
};
6064

6165
componentDidMount() {
@@ -73,6 +77,7 @@ class TimeLineView extends React.PureComponent<Props, State> {
7377
}
7478

7579
async fetchTimeTrackingData() {
80+
this.setState({ isLoading: true });
7681
if (this.state.user != null) {
7782
/* eslint-disable react/no-access-state-in-setstate */
7883
const timeTrackingData = await getTimeTrackingForUser(
@@ -84,6 +89,7 @@ class TimeLineView extends React.PureComponent<Props, State> {
8489
this.setState({ timeTrackingData }, this.calculateStats);
8590
/* eslint-enable react/no-access-state-in-setstate */
8691
}
92+
this.setState({ isLoading: false });
8793
}
8894

8995
calculateStats() {
@@ -136,10 +142,10 @@ class TimeLineView extends React.PureComponent<Props, State> {
136142
getTooltipForEntry(taskId: string, start: Date, end: Date) {
137143
const isSameDay = start.getUTCDate() === end.getUTCDate();
138144
const duration = end - start;
139-
const durationAsString = formatDurationToHoursAndMinutes(duration);
145+
const durationAsString = formatDurationToMinutesAndSeconds(duration);
140146
const dayFormatForMomentJs = "DD, MMM, YYYY";
141147
const tooltip = (
142-
<div className="google-charts-tooltip">
148+
<div>
143149
<div className="highlighted">
144150
Task ID: {taskId}
145151
<div className="striped-border" />
@@ -168,7 +174,7 @@ class TimeLineView extends React.PureComponent<Props, State> {
168174
</td>
169175
</tr>
170176
<tr>
171-
<td className="highlighted">Duration:</td>
177+
<td className="highlighted">Duration (min:sec):</td>
172178
<td>{durationAsString}</td>
173179
</tr>
174180
</tbody>
@@ -179,7 +185,7 @@ class TimeLineView extends React.PureComponent<Props, State> {
179185
}
180186

181187
render() {
182-
const columns = [
188+
const columns: Array<ColumnDefinition> = [
183189
{ id: "AnnotationId", type: "string" },
184190
// This label columns is somehow needed to make the custom tooltip work.
185191
// See https://developers.google.com/chart/interactive/docs/gallery/timeline#customizing-tooltips.
@@ -189,9 +195,9 @@ class TimeLineView extends React.PureComponent<Props, State> {
189195
{ id: "End", type: "date" },
190196
];
191197

192-
const { dateRange } = this.state;
193-
const timeTrackingRowGrouped = []; // shows each time span grouped by annotation id
194-
const timeTrackingRowTotal = []; // show all times spans in a single row
198+
const { dateRange, isLoading, timeTrackingData } = this.state;
199+
const timeTrackingRowGrouped: Array<RowContent> = []; // shows each time span grouped by annotation id
200+
const timeTrackingRowTotal: Array<RowContent> = []; // show all times spans in a single row
195201

196202
const totalSumColumnLabel = "Sum Tracking Time";
197203

@@ -292,35 +298,22 @@ class TimeLineView extends React.PureComponent<Props, State> {
292298
</Row>
293299
</Card>
294300

295-
<div style={{ marginTop: 20 }}>
296-
{this.state.timeTrackingData.length > 0 ? (
297-
<Chart
298-
chartType="Timeline"
301+
<div style={{ marginTop: 20 }} />
302+
<Spin size="large" spinning={isLoading}>
303+
{timeTrackingData.length > 0 ? (
304+
<TimeTrackingChart
299305
columns={columns}
300306
rows={rows}
301-
options={{
302-
timeline: { singleColor: "#108ee9" },
303-
// Workaround for google-charts bug, see https://github.com/scalableminds/webknossos/pull/3772
304-
hAxis: {
305-
format: timeAxisFormat,
306-
minValue: dateRange[0].toDate(),
307-
maxValue: dateRange[1].toDate(),
308-
},
309-
allowHtml: true,
310-
tooltip: { isHtml: true },
311-
}}
312-
graph_id="TimeLineGraph"
313-
chartPackages={["timeline"]}
314-
width="100%"
315-
height="600px"
316-
legend_toggle
307+
timeAxisFormat={timeAxisFormat}
308+
dateRange={dateRange}
309+
timeTrackingData={timeTrackingData}
317310
/>
318311
) : (
319312
<div style={{ textAlign: "center" }}>
320313
No Time Tracking Data for the Selected User or Date Range.
321314
</div>
322315
)}
323-
</div>
316+
</Spin>
324317
</div>
325318
);
326319
}

frontend/javascripts/libs/format_utils.js

+5-10
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,13 @@ export function formatSeconds(durationSeconds: number): string {
9898
return timeString;
9999
}
100100
101-
export function formatDurationToHoursAndMinutes(
102-
durationInMillisecons: number,
103-
minMinutes: number = 1,
104-
) {
101+
export function formatDurationToMinutesAndSeconds(durationInMillisecons: number) {
105102
// Moment does not provide a format method for durations, so we have to do it manually.
106103
const duration = moment.duration(durationInMillisecons);
107-
const minutesAsString = `${duration.minutes() < 10 ? 0 : ""}${Math.max(
108-
duration.minutes(),
109-
minMinutes,
110-
)}`;
111-
const hoursAsString = `${duration.hours() < 10 ? 0 : ""}${duration.hours()}`;
112-
return `${hoursAsString}:${minutesAsString}`;
104+
const minuteDuration = duration.minutes() + 60 * duration.hours();
105+
const minutesAsString = `${minuteDuration < 10 ? 0 : ""}${minuteDuration}`;
106+
const hoursAsSeconds = `${duration.seconds() < 10 ? 0 : ""}${duration.seconds()}`;
107+
return `${minutesAsString}:${hoursAsSeconds}`;
113108
}
114109

115110
export function formatHash(id: string): string {

frontend/javascripts/libs/utils.js

+23
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,29 @@ export function getIsInIframe() {
667667
}
668668
}
669669

670+
export function getWindowBounds(): [number, number] {
671+
// Function taken from https://stackoverflow.com/questions/3333329/javascript-get-browser-height.
672+
let width = 0;
673+
let height = 0;
674+
if (typeof window.innerWidth === "number") {
675+
// Non-IE
676+
width = window.innerWidth;
677+
height = window.innerHeight;
678+
} else if (
679+
document.documentElement &&
680+
(document.documentElement.clientWidth || document.documentElement.clientHeight)
681+
) {
682+
// IE 6+ in 'standards compliant mode'
683+
width = document.documentElement.clientWidth;
684+
height = document.documentElement.clientHeight;
685+
} else if (document.body && (document.body.clientWidth || document.body.clientHeight)) {
686+
// IE 4 compatible
687+
width = document.body.clientWidth;
688+
height = document.body.clientHeight;
689+
}
690+
return [width, height];
691+
}
692+
670693
export function disableViewportMetatag() {
671694
const viewport = document.querySelector("meta[name=viewport]");
672695
if (!viewport) {

frontend/stylesheets/_google-charts-overwrites.less

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ div.google-visualization-tooltip {
55
font-weight: normal;
66
font-size: 13px;
77
border-radius: 6px;
8+
position: fixed;
9+
// We need to set the visibility to hidden as the tooltip is initially mispositioned.
10+
// After adjusting the position, we set the visibility to visible again (@see TimeLineChart).
11+
visibility: hidden;
812
.highlighted {
913
font-weight: bold;
1014
}

0 commit comments

Comments
 (0)