Skip to content

Commit

Permalink
When showing measurements on multiple dates, also allow for having co…
Browse files Browse the repository at this point in the history
…lumns that show the delta between dates. The delta columns can be turned on and off via the Settings panel. Closes #7039.
  • Loading branch information
fniessink committed Feb 26, 2024
1 parent 6165ce8 commit 43f72bc
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 20 deletions.
6 changes: 6 additions & 0 deletions components/frontend/src/header_footer/SettingsPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ export function SettingsPanel({
<VisibleColumnMenuItem column="unit" {...visibleColumnMenuItemProps} />
<VisibleColumnMenuItem column="source" {...visibleColumnMenuItemProps} />
<VisibleColumnMenuItem column="time_left" {...visibleColumnMenuItemProps} />
<VisibleColumnMenuItem
column="delta"
disabled={oneDateColumn}
help="The delta columns can only be made visible when at least two dates are shown"
{...visibleColumnMenuItemProps}
/>
<VisibleColumnMenuItem
column="overrun"
disabled={oneDateColumn}
Expand Down
2 changes: 2 additions & 0 deletions components/frontend/src/sharedPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export const measurementSourceType = PropTypes.shape({
parse_error: PropTypes.string,
})

export const measurementValueType = PropTypes.oneOfType([PropTypes.number, PropTypes.string])

export const sourcePropType = PropTypes.shape({
entities: PropTypes.array,
entity_user_data: PropTypes.object,
Expand Down
29 changes: 27 additions & 2 deletions components/frontend/src/subject/SubjectTableHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,31 @@ const tagsHelp = <>
</p>
</>

function MeasurementHeaderCells({ columnDates, showDeltaColumns }) {
const cells = []
columnDates.forEach((date, index) => {
if (showDeltaColumns && index > 0) {
cells.push(
<UnsortableTableHeaderCell
key={`delta-${date}`}
help="The difference between the measurement values on the previous and next date.
A plus (+) sign indicates that the newer value is higher, a minus (-) sign that it is lower.
A green outline indicates that the newer value is better, a red outline that it is worse."
label="𝚫"
textAlign="right"
/>
)
}
cells.push(<UnsortableTableHeaderCell key={date} textAlign="right" label={date.toLocaleDateString()} />)
})
return cells
}
MeasurementHeaderCells.propTypes = {
columnDates: datesPropType,
showDeltaColumns: PropTypes.bool,
}


export function SubjectTableHeader(
{
columnDates,
Expand All @@ -238,7 +263,7 @@ export function SubjectTableHeader(
<Table.Header>
<Table.Row>
<SortableTableHeaderCell colSpan="2" column='name' label='Metric' help={metricHelp} {...sortProps} />
{nrDates > 1 && columnDates.map(date => <UnsortableTableHeaderCell key={date} textAlign="right" label={date.toLocaleDateString()} />)}
{nrDates > 1 && <MeasurementHeaderCells columnDates={columnDates} showDeltaColumns={!settings.hiddenColumns.includes("delta")} />}
{nrDates === 1 && !settings.hiddenColumns.includes("trend") && <UnsortableTableHeaderCell width="2" label="Trend (7 days)" help={trendHelp} />}
{nrDates === 1 && !settings.hiddenColumns.includes("status") && <SortableTableHeaderCell column='status' label='Status' textAlign='center' help={statusHelp(darkMode)} {...sortProps} />}
{nrDates === 1 && !settings.hiddenColumns.includes("measurement") && <SortableTableHeaderCell column='measurement' label='Measurement' textAlign="right" help={measurementHelp} {...sortProps} />}
Expand All @@ -257,5 +282,5 @@ export function SubjectTableHeader(
SubjectTableHeader.propTypes = {
columnDates: datesPropType,
handleSort: PropTypes.func,
settings: settingsPropType
settings: settingsPropType,
}
10 changes: 9 additions & 1 deletion components/frontend/src/subject/SubjectTableHeader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ it('shows the column dates and unit', () => {
const date1 = new Date("2022-02-02");
const date2 = new Date("2022-02-03");
renderSubjectTableHeader([date1, date2]);
[date1.toLocaleDateString(), date2.toLocaleDateString(), "Unit", "Sources", "Time left", "Overrun", "Comment", "Issues", "Tags"].forEach(
[date1.toLocaleDateString(), "𝚫", date2.toLocaleDateString(), "Unit", "Sources", "Time left", "Overrun", "Comment", "Issues", "Tags"].forEach(
header => expect(screen.getAllByText(header).length).toBe(1)
);
["Trend (7 days)", "Status", "Measurement", "Target"].forEach(
Expand All @@ -49,3 +49,11 @@ it('hides columns', () => {
header => expect(screen.queryAllByText(header).length).toBe(0)
);
})

it('hides the delta columns', () => {
history.push("?hidden_columns=delta")
const date1 = new Date("2022-02-02")
const date2 = new Date("2022-02-03");
renderSubjectTableHeader([date1, date2]);
expect(screen.queryAllByText("𝚫").length).toBe(0)
})
103 changes: 86 additions & 17 deletions components/frontend/src/subject/SubjectTableRow.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { Table } from '../semantic_ui_react_wrappers';
import { Icon } from 'semantic-ui-react';
import { Label, Table } from '../semantic_ui_react_wrappers';
import { DataModel } from "../context/DataModel";
import { DarkMode } from "../context/DarkMode";
import { IssueStatus } from '../issue/IssueStatus';
Expand All @@ -14,9 +15,10 @@ import { TimeLeft } from '../measurement/TimeLeft';
import { TrendSparkline } from '../measurement/TrendSparkline';
import { TableRowWithDetails } from '../widgets/TableRowWithDetails';
import { Tag } from '../widgets/Tag';
import { formatMetricScale, get_metric_name, getMetricTags, getMetricUnit } from '../utils';
import { formatMetricScale, get_metric_name, getMetricDirection, getMetricTags, getMetricUnit } from '../utils';
import {
datesPropType,
measurementValueType,
metricPropType,
optionalDatePropType,
reportPropType,
Expand All @@ -25,28 +27,95 @@ import {
stringsPropType,
} from '../sharedPropTypes';

function MeasurementCells({ dates, metric, metric_uuid, measurements }) {
function didValueIncrease(dateOrderAscending, metricValue, previousValue) {
return (dateOrderAscending && metricValue > previousValue) || (!dateOrderAscending && metricValue < previousValue)
}
didValueIncrease.propTypes = {
dateOrderAscending: PropTypes.bool,
metricValue: measurementValueType,
previousValue: measurementValueType,
}

function didValueImprove(didValueIncrease, direction) {
return (didValueIncrease && direction === ">") || (!didValueIncrease && direction === "<")
}
didValueImprove.propTypes = {
didValueIncrease: PropTypes.bool,
direction: PropTypes.oneOf(["<", ">"]),
}

function DeltaCell({ dateOrderAscending, index, metric, metricValue, previousValue, status }) {
const dataModel = useContext(DataModel);
let label = null;
if (index > 0 && previousValue !== "?" && metricValue !== "?" && previousValue !== metricValue) {
const direction = getMetricDirection(metric, dataModel)
const increase = didValueIncrease(dateOrderAscending, metricValue, previousValue)
const improved = didValueImprove(increase, direction)
let alt = "The measurement value ";
let color = null;
if (improved) {
alt += "improved"
color = "green"
} else {
alt += "worsened"
color = "red"
}
let delta = increase ? "+" : "-"
if (metric.scale !== "version_number") {
delta += `${Math.abs(metricValue - previousValue)}`
alt += ` with ${delta}`
}
label = <Label aria-label={alt} basic color={color}>{delta}</Label>
}
return (
<>
{
dates.map((date) => {
const iso_date_string = date.toISOString().split("T")[0];
const measurement = measurements?.find((m) => { return m.metric_uuid === metric_uuid && m.start.split("T")[0] <= iso_date_string && iso_date_string <= m.end.split("T")[0] })
let metric_value = measurement?.[metric.scale]?.value ?? "?";
const status = measurement?.[metric.scale]?.status ?? "unknown";
return (
<Table.Cell className={status} key={date} textAlign="right">{metric_value}{formatMetricScale(metric)}</Table.Cell>
)
})
}
</>
<Table.Cell className={status} singleLine textAlign="right">
{label}
</Table.Cell>
)
}
DeltaCell.propTypes = {
dateOrderAscending: PropTypes.bool,
metric: metricPropType,
index: PropTypes.number,
metricValue: measurementValueType,
previousValue: measurementValueType,
status: PropTypes.string,
}

function MeasurementCells({ dates, metric, metric_uuid, measurements, settings }) {
const showDeltaColumns = !settings.hiddenColumns.includes("delta")
const dateOrderAscending = settings.dateOrder.value === "ascending"
const cells = []
let previousValue = "?";
dates.forEach((date, index) => {
const isoDateString = date.toISOString().split("T")[0];
const measurement = measurements?.find((m) => { return m.metric_uuid === metric_uuid && m.start.split("T")[0] <= isoDateString && isoDateString <= m.end.split("T")[0] })
let metricValue = measurement?.[metric.scale]?.value ?? "?";
const status = measurement?.[metric.scale]?.status ?? "unknown";
if (showDeltaColumns && index > 0) {
cells.push(
<DeltaCell
dateOrderAscending={dateOrderAscending}
index={index}
key={`${date}-delta`}
metric={metric}
metricValue={metricValue}
previousValue={previousValue}
status={status}
/>
)
}
cells.push(<Table.Cell className={status} key={date} textAlign="right">{metricValue}{formatMetricScale(metric)}</Table.Cell>)
previousValue = metricValue === "?" ? previousValue : metricValue;
})
return cells
}
MeasurementCells.propTypes = {
dates: datesPropType,
measurements: PropTypes.array,
metric_uuid: PropTypes.string,
metric: metricPropType,
settings: settingsPropType,
}

function expandOrCollapseItem(expand, metric_uuid, expandedItems) {
Expand Down Expand Up @@ -111,7 +180,7 @@ export function SubjectTableRow(
style={style}
>
<Table.Cell style={style}>{metricName}</Table.Cell>
{nrDates > 1 && <MeasurementCells dates={dates} metric={metric} metric_uuid={metric_uuid} measurements={reversedMeasurements} />}
{nrDates > 1 && <MeasurementCells dates={dates} metric={metric} metric_uuid={metric_uuid} measurements={reversedMeasurements} settings={settings} />}
{nrDates === 1 && !settings.hiddenColumns.includes("trend") && <Table.Cell><TrendSparkline measurements={metric.recent_measurements} report_date={reportDate} scale={metric.scale} /></Table.Cell>}
{nrDates === 1 && !settings.hiddenColumns.includes("status") && <Table.Cell textAlign='center'><StatusIcon status={metric.status} status_start={metric.status_start} /></Table.Cell>}
{nrDates === 1 && !settings.hiddenColumns.includes("measurement") && <Table.Cell textAlign="right"><MeasurementValue metric={metric} reportDate={reportDate} /></Table.Cell>}
Expand Down
81 changes: 81 additions & 0 deletions components/frontend/src/subject/SubjectTableRow.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { render, screen } from "@testing-library/react";
import history from 'history/browser';
import { Table } from '../semantic_ui_react_wrappers';
import { DataModel } from "../context/DataModel";
import { SubjectTableRow } from './SubjectTableRow';
import { createTestableSettings, datamodel, report } from "../__fixtures__/fixtures";

beforeEach(() => {
history.push("")
})

function renderSubjectTableRow(direction, ascending, scale) {
const dates = [new Date("2024-01-03"), new Date("2024-01-02"), new Date("2024-01-01")]
const reverseMeasurements = [
{ metric_uuid: "metric_uuid", start: "2024-01-03T00:00", end: "2024-01-03T00:00", count: { value: 8, status: "target_met" }, version_number: { value: "0.8", status: "target_met" } },
{ metric_uuid: "metric_uuid", start: "2024-01-02T00:00", end: "2024-01-02T00:00", count: { value: 12, status: "target_met" }, version_number: { value: "1.2", status: "target_met" } },
{ metric_uuid: "metric_uuid", start: "2024-01-01T00:00", end: "2024-01-01T00:00", count: { value: 10, status: "target_met" }, version_number: { value: "1.0", status: "target_met" } },
]
if (ascending) {
dates.reverse()
}
render(
<DataModel.Provider value={datamodel}>
<Table>
<Table.Body>
<SubjectTableRow
dates={dates}
measurements={[]}
metric={{ type: "metric_type", direction: direction ?? "<", recent_measurements: [], scale: scale ?? "count" }}
metric_uuid="metric_uuid"
report={report}
reversedMeasurements={reverseMeasurements}
settings={createTestableSettings()}
/>
</Table.Body>
</Table>
</DataModel.Provider>
)
}

it('shows the delta column', () => {
history.push("?nr_dates=3&date_interval=1")
renderSubjectTableRow()
expect(screen.getAllByText("+2").length).toBe(1);
expect(screen.getAllByLabelText("The measurement value worsened with +2").length).toBe(1);
expect(screen.getAllByText("-4").length).toBe(1);
expect(screen.getAllByLabelText("The measurement value improved with -4").length).toBe(1);
})

it('hides the delta column', () => {
history.push("?nr_dates=2&hidden_columns=delta")
renderSubjectTableRow()
expect(screen.queryAllByText("+2").length).toBe(0);
})

it('takes the metric direction into account', () => {
history.push("?nr_dates=3&date_interval=1")
renderSubjectTableRow(">")
expect(screen.getAllByText("+2").length).toBe(1);
expect(screen.getAllByLabelText("The measurement value improved with +2").length).toBe(1);
expect(screen.getAllByText("-4").length).toBe(1);
expect(screen.getAllByLabelText("The measurement value worsened with -4").length).toBe(1);
})

it('takes the date order into account', () => {
history.push("?nr_dates=3&date_interval=1&date_order=ascending")
renderSubjectTableRow("<", true)
expect(screen.getAllByText("+2").length).toBe(1);
expect(screen.getAllByLabelText("The measurement value worsened with +2").length).toBe(1);
expect(screen.getAllByText("-4").length).toBe(1);
expect(screen.getAllByLabelText("The measurement value improved with -4").length).toBe(1);
})

it('shows the delta column for the version scale', () => {
history.push("?nr_dates=3&date_interval=1")
renderSubjectTableRow("<", false, "version_number")
expect(screen.getAllByText("+").length).toBe(1);
expect(screen.getAllByLabelText("The measurement value worsened").length).toBe(1);
expect(screen.getAllByText("-").length).toBe(1);
expect(screen.getAllByLabelText("The measurement value improved").length).toBe(1);
})
1 change: 1 addition & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ If your currently installed *Quality-time* version is v4.10.0 or older, please r
### Added

- If the documentation at Read-the-Docs has specific information on configuring a metric or source, point this out in Quality-time itself. Closes [#4445](https://github.com/ICTU/quality-time/issues/4445).
- When showing measurements on multiple dates, also show columns with the delta between dates. The delta columns can be turned on and off via the Settings panel. Closes [#7039](https://github.com/ICTU/quality-time/issues/7039).
- In the dashboard popups, show the percentage next to the number of metrics with a specific status. Closes [#7946](https://github.com/ICTU/quality-time/issues/7946).

## v5.8.0 - 2024-02-16
Expand Down

0 comments on commit 43f72bc

Please sign in to comment.