--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { AutoSizer } from 'react-virtualized';
+import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
+import GraphsTooltips from './GraphsTooltips';
+import GraphsLegendCustom from './GraphsLegendCustom';
+import GraphsLegendStatic from './GraphsLegendStatic';
+import { formatMeasure, getShortType } from '../../../helpers/measures';
+import type { Event, MeasureHistory } from '../types';
+import type { Serie } from '../../../components/charts/AdvancedTimeline';
+
+type Props = {
+ events: Array<Event>,
+ graph: string,
+ graphEndDate: ?Date,
+ graphStartDate: ?Date,
+ leakPeriodDate: Date,
+ isCustom: boolean,
+ measuresHistory: Array<MeasureHistory>,
+ metricsType: string,
+ removeCustomMetric: (metric: string) => void,
+ showAreas: boolean,
+ series: Array<Serie>,
+ selectedDate?: ?Date,
+ updateGraphZoom: (from: ?Date, to: ?Date) => void,
+ updateSelectedDate: (selectedDate: ?Date) => void,
+ updateTooltip: (selectedDate: ?Date) => void
+};
+
+type State = {
+ tooltipIdx: ?number,
+ tooltipXPos: ?number
+};
+
+export default class GraphHistory extends React.PureComponent {
+ props: Props;
+ state: State = {
+ tooltipIdx: null,
+ tooltipXPos: null
+ };
+
+ formatValue = (tick: string | number) =>
+ formatMeasure(tick, getShortType(this.props.metricsType));
+
+ updateTooltip = (selectedDate: ?Date, tooltipXPos: ?number, tooltipIdx: ?number) => {
+ this.props.updateTooltip(selectedDate);
+ this.setState({ tooltipXPos, tooltipIdx });
+ };
+
+ render() {
+ const { graph, selectedDate, series } = this.props;
+ const { tooltipIdx, tooltipXPos } = this.state;
+
+ return (
+ <div className="project-activity-graph-container">
+ {this.props.isCustom
+ ? <GraphsLegendCustom series={series} removeMetric={this.props.removeCustomMetric} />
+ : <GraphsLegendStatic series={series} />}
+ <div className="project-activity-graph">
+ <AutoSizer>
+ {({ height, width }) =>
+ <div>
+ <AdvancedTimeline
+ endDate={this.props.graphEndDate}
+ height={height}
+ width={width}
+ interpolate="linear"
+ formatYTick={this.formatValue}
+ leakPeriodDate={this.props.leakPeriodDate}
+ metricType={this.props.metricsType}
+ selectedDate={selectedDate}
+ series={series}
+ showAreas={this.props.showAreas}
+ startDate={this.props.graphStartDate}
+ updateSelectedDate={this.props.updateSelectedDate}
+ updateTooltip={this.updateTooltip}
+ updateZoom={this.props.updateGraphZoom}
+ />
+ {selectedDate != null &&
+ tooltipXPos != null &&
+ <GraphsTooltips
+ events={this.props.events}
+ formatValue={this.formatValue}
+ graph={graph}
+ graphWidth={width}
+ measuresHistory={this.props.measuresHistory}
+ selectedDate={selectedDate}
+ series={series}
+ tooltipIdx={tooltipIdx}
+ tooltipPos={tooltipXPos}
+ />}
+ </div>}
+ </AutoSizer>
+ </div>
+ </div>
+ );
+ }
+}
*/
import React from 'react';
import moment from 'moment';
-import { sortBy } from 'lodash';
-import { AutoSizer } from 'react-virtualized';
-import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
-import GraphsTooltips from './GraphsTooltips';
-import GraphsLegendCustom from './GraphsLegendCustom';
-import GraphsLegendStatic from './GraphsLegendStatic';
-import { formatMeasure, getShortType } from '../../../helpers/measures';
-import { EVENT_TYPES, isCustomGraph } from '../utils';
+import { isEqual, sortBy } from 'lodash';
+import GraphHistory from './GraphHistory';
+import { EVENT_TYPES, getSeriesMetricType, hasHistoryData, isCustomGraph } from '../utils';
+import { translate } from '../../../helpers/l10n';
import type { Analysis, MeasureHistory } from '../types';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
analyses: Array<Analysis>,
eventFilter: string,
graph: string,
+ graphs: Array<Array<Serie>>,
graphEndDate: ?Date,
graphStartDate: ?Date,
leakPeriodDate: Date,
+ loading: boolean,
measuresHistory: Array<MeasureHistory>,
- metricsType: string,
removeCustomMetric: (metric: string) => void,
selectedDate: ?Date,
series: Array<Serie>,
};
type State = {
- selectedDate?: ?Date,
- tooltipIdx: ?number,
- tooltipXPos: ?number
+ selectedDate?: ?Date
};
export default class GraphsHistory extends React.PureComponent {
props: Props;
- state: State = {
- tooltipIdx: null,
- tooltipXPos: null
- };
+ state: State;
- formatValue = (tick: string | number) =>
- formatMeasure(tick, getShortType(this.props.metricsType));
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ selectedDate: props.selectedDate
+ };
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (!isEqual(nextProps.selectedDate, this.props.selectedDate)) {
+ this.setState({ selectedDate: nextProps.selectedDate });
+ }
+ }
getEvents = () => {
const { analyses, eventFilter } = this.props;
return [];
};
- updateTooltip = (selectedDate: ?Date, tooltipXPos: ?number, tooltipIdx: ?number) =>
- this.setState({ selectedDate, tooltipXPos, tooltipIdx });
+ updateTooltip = (selectedDate: ?Date) => this.setState({ selectedDate });
render() {
const { graph, series } = this.props;
const isCustom = isCustomGraph(graph);
- const { selectedDate, tooltipIdx, tooltipXPos } = this.state;
- return (
- <div className="project-activity-graph-container">
- {isCustom
- ? <GraphsLegendCustom series={series} removeMetric={this.props.removeCustomMetric} />
- : <GraphsLegendStatic series={series} />}
- <div className="project-activity-graph">
- <AutoSizer>
- {({ height, width }) =>
- <div>
- <AdvancedTimeline
- endDate={this.props.graphEndDate}
- height={height}
- width={width}
- interpolate="linear"
- formatYTick={this.formatValue}
- leakPeriodDate={this.props.leakPeriodDate}
- metricType={this.props.metricsType}
- selectedDate={this.props.selectedDate}
- series={series}
- showAreas={['coverage', 'duplications'].includes(graph)}
- startDate={this.props.graphStartDate}
- updateSelectedDate={this.props.updateSelectedDate}
- updateTooltip={this.updateTooltip}
- updateZoom={this.props.updateGraphZoom}
- />
- {selectedDate != null &&
- tooltipXPos != null &&
- <GraphsTooltips
- events={this.getSelectedDateEvents()}
- formatValue={this.formatValue}
- graph={graph}
- graphWidth={width}
- measuresHistory={this.props.measuresHistory}
- selectedDate={selectedDate}
- series={series}
- tooltipIdx={tooltipIdx}
- tooltipPos={tooltipXPos}
- />}
- </div>}
- </AutoSizer>
+
+ if (this.props.loading) {
+ return (
+ <div className="project-activity-graph-container">
+ <div className="text-center">
+ <i className="spinner" />
+ </div>
</div>
+ );
+ }
+
+ if (!hasHistoryData(series)) {
+ return (
+ <div className="project-activity-graph-container">
+ <div className="note text-center">
+ {translate(
+ isCustom
+ ? 'project_activity.graphs.custom.no_history'
+ : 'component_measures.no_history'
+ )}
+ </div>
+ </div>
+ );
+ }
+ const events = this.getSelectedDateEvents();
+ const showAreas = ['coverage', 'duplications'].includes(graph);
+ return (
+ <div className="project-activity-graphs">
+ {this.props.graphs.map((series, idx) =>
+ <GraphHistory
+ key={idx}
+ events={events}
+ graph={graph}
+ graphEndDate={this.props.graphEndDate}
+ graphStartDate={this.props.graphStartDate}
+ isCustom={isCustom}
+ leakPeriodDate={this.props.leakPeriodDate}
+ measuresHistory={this.props.measuresHistory}
+ metricsType={getSeriesMetricType(series)}
+ removeCustomMetric={this.props.removeCustomMetric}
+ selectedDate={this.state.selectedDate}
+ series={series}
+ showAreas={showAreas}
+ updateGraphZoom={this.props.updateGraphZoom}
+ updateSelectedDate={this.props.updateSelectedDate}
+ updateTooltip={this.updateTooltip}
+ />
+ )}
</div>
);
}
generateSeries,
getDisplayedHistoryMetrics,
getSeriesMetricType,
- hasHistoryData,
historyQueryChanged
} from '../utils';
-import { translate } from '../../../helpers/l10n';
import type { RawQuery } from '../../../helpers/query';
import type { Analysis, MeasureHistory, Metric, Query } from '../types';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
}
};
- renderGraphs() {
- const { leakPeriodDate, loading, query } = this.props;
- const { graphEndDate, graphs, graphStartDate, series } = this.state;
- const isCustom = isCustomGraph(query.graph);
-
- if (loading) {
- return (
- <div className="project-activity-graph-container">
- <div className="text-center">
- <i className="spinner" />
- </div>
- </div>
- );
- }
-
- if (!hasHistoryData(series)) {
- return (
- <div className="project-activity-graph-container">
- <div className="note text-center">
- {translate(
- isCustom
- ? 'project_activity.graphs.custom.no_history'
- : 'component_measures.no_history'
- )}
- </div>
- </div>
- );
- }
-
- return graphs.map((series, idx) =>
- <GraphsHistory
- key={idx}
- analyses={this.props.analyses}
- eventFilter={query.category}
- graph={query.graph}
- graphEndDate={graphEndDate}
- graphStartDate={graphStartDate}
- leakPeriodDate={leakPeriodDate}
- measuresHistory={this.props.measuresHistory}
- metricsType={getSeriesMetricType(series)}
- removeCustomMetric={this.removeCustomMetric}
- selectedDate={this.props.query.selectedDate}
- series={series}
- updateGraphZoom={this.updateGraphZoom}
- updateSelectedDate={this.updateSelectedDate}
- />
- );
- }
-
render() {
const { leakPeriodDate, loading, metrics, query } = this.props;
const { graphEndDate, graphStartDate, series } = this.state;
selectedMetrics={this.props.query.customMetrics}
updateGraph={this.updateGraph}
/>
- {this.renderGraphs()}
+ <GraphsHistory
+ analyses={this.props.analyses}
+ eventFilter={query.category}
+ graph={query.graph}
+ graphs={this.state.graphs}
+ graphEndDate={graphEndDate}
+ graphStartDate={graphStartDate}
+ leakPeriodDate={leakPeriodDate}
+ loading={loading}
+ measuresHistory={this.props.measuresHistory}
+ removeCustomMetric={this.removeCustomMetric}
+ selectedDate={this.props.query.selectedDate}
+ series={series}
+ updateGraphZoom={this.updateGraphZoom}
+ updateSelectedDate={this.updateSelectedDate}
+ />
<GraphsZoom
graphEndDate={graphEndDate}
graphStartDate={graphStartDate}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import GraphHistory from '../GraphHistory';
+
+const SERIES = [
+ {
+ name: 'bugs',
+ translatedName: 'metric.bugs.name',
+ data: [
+ { x: new Date('2016-10-27T16:33:50+0200'), y: 5 },
+ { x: new Date('2016-10-27T12:21:15+0200'), y: 16 },
+ { x: new Date('2016-10-26T12:17:29+0200'), y: 12 }
+ ]
+ }
+];
+
+const DEFAULT_PROPS = {
+ events: [],
+ graph: 'overview',
+ graphEndDate: null,
+ graphStartDate: null,
+ leakPeriodDate: '2017-05-16T13:50:02+0200',
+ isCustom: false,
+ measuresHistory: [],
+ metrics: [],
+ metricsType: 'INT',
+ removeCustomMetric: () => {},
+ showAreas: true,
+ selectedDate: null,
+ series: SERIES,
+ updateGraphZoom: () => {},
+ updateSelectedDate: () => {},
+ updateTooltip: () => {}
+};
+
+it('should correctly render a graph', () => {
+ expect(shallow(<GraphHistory {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
analyses: ANALYSES,
eventFilter: '',
graph: 'overview',
+ graphs: [SERIES],
graphEndDate: null,
graphStartDate: null,
leakPeriodDate: '2017-05-16T13:50:02+0200',
+ loading: false,
measuresHistory: [],
- metricsType: 'INT',
removeCustomMetric: () => {},
selectedDate: null,
series: SERIES,
expect(shallow(<GraphsHistory {...DEFAULT_PROPS} />)).toMatchSnapshot();
});
+it('should correctly render multiple graphs', () => {
+ expect(shallow(<GraphsHistory {...DEFAULT_PROPS} graphs={[SERIES, SERIES]} />)).toMatchSnapshot();
+});
+
it('should correctly filter events', () => {
expect(shallow(<GraphsHistory {...DEFAULT_PROPS} />).instance().getEvents()).toMatchSnapshot();
expect(
shallow(<GraphsHistory {...DEFAULT_PROPS} eventFilter="OTHER" />).instance().getEvents()
).toMatchSnapshot();
});
+
+it('should show a loading view instead of the graph', () => {
+ expect(
+ shallow(<GraphsHistory {...DEFAULT_PROPS} loading={true} />).find('.spinner')
+ ).toHaveLength(1);
+});
+
+it('should show that there is no history data', () => {
+ expect(shallow(<GraphsHistory {...DEFAULT_PROPS} series={[]} />)).toMatchSnapshot();
+ expect(
+ shallow(
+ <GraphsHistory
+ {...DEFAULT_PROPS}
+ series={[
+ {
+ name: 'bugs',
+ translatedName: 'metric.bugs.name',
+ data: [{ x: new Date('2016-10-27T16:33:50+0200'), y: undefined }]
+ }
+ ]}
+ />
+ )
+ ).toMatchSnapshot();
+});
);
expect(wrapper.state()).toMatchSnapshot();
});
-
-it('should show a loading view instead of the graph', () => {
- expect(
- shallow(<ProjectActivityGraphs {...DEFAULT_PROPS} loading={true} />).find('.spinner')
- ).toHaveLength(1);
-});
-
-it('should show that there is no history data', () => {
- expect(
- shallow(
- <ProjectActivityGraphs
- {...DEFAULT_PROPS}
- measuresHistory={[{ metric: 'code_smells', history: [] }]}
- />
- )
- ).toMatchSnapshot();
- expect(
- shallow(
- <ProjectActivityGraphs
- {...DEFAULT_PROPS}
- measuresHistory={[
- {
- metric: 'code_smells',
- history: [{ date: new Date('2016-10-26T12:17:29+0200'), value: undefined }]
- }
- ]}
- query={{
- category: '',
- graph: 'custom',
- project: 'org.sonarsource.sonarqube:sonarqube',
- customMetrics: ['code_smells']
- }}
- />
- )
- ).toMatchSnapshot();
-});
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly render a graph 1`] = `
+<div
+ className="project-activity-graph-container"
+>
+ <GraphsLegendStatic
+ series={
+ Array [
+ Object {
+ "data": Array [
+ Object {
+ "x": 2016-10-27T14:33:50.000Z,
+ "y": 5,
+ },
+ Object {
+ "x": 2016-10-27T10:21:15.000Z,
+ "y": 16,
+ },
+ Object {
+ "x": 2016-10-26T10:17:29.000Z,
+ "y": 12,
+ },
+ ],
+ "name": "bugs",
+ "translatedName": "metric.bugs.name",
+ },
+ ]
+ }
+ />
+ <div
+ className="project-activity-graph"
+ >
+ <AutoSizer
+ onResize={[Function]}
+ />
+ </div>
+</div>
+`;
exports[`should correctly render a graph 1`] = `
<div
- className="project-activity-graph-container"
+ className="project-activity-graphs"
+>
+ <GraphHistory
+ events={Array []}
+ graph="overview"
+ graphEndDate={null}
+ graphStartDate={null}
+ isCustom={false}
+ leakPeriodDate="2017-05-16T13:50:02+0200"
+ measuresHistory={Array []}
+ removeCustomMetric={[Function]}
+ selectedDate={null}
+ series={
+ Array [
+ Object {
+ "data": Array [
+ Object {
+ "x": 2016-10-27T14:33:50.000Z,
+ "y": 5,
+ },
+ Object {
+ "x": 2016-10-27T10:21:15.000Z,
+ "y": 16,
+ },
+ Object {
+ "x": 2016-10-26T10:17:29.000Z,
+ "y": 12,
+ },
+ ],
+ "name": "bugs",
+ "translatedName": "metric.bugs.name",
+ },
+ ]
+ }
+ showAreas={false}
+ updateGraphZoom={[Function]}
+ updateSelectedDate={[Function]}
+ updateTooltip={[Function]}
+ />
+</div>
+`;
+
+exports[`should correctly render multiple graphs 1`] = `
+<div
+ className="project-activity-graphs"
>
- <GraphsLegendStatic
+ <GraphHistory
+ events={Array []}
+ graph="overview"
+ graphEndDate={null}
+ graphStartDate={null}
+ isCustom={false}
+ leakPeriodDate="2017-05-16T13:50:02+0200"
+ measuresHistory={Array []}
+ removeCustomMetric={[Function]}
+ selectedDate={null}
series={
Array [
Object {
},
]
}
+ showAreas={false}
+ updateGraphZoom={[Function]}
+ updateSelectedDate={[Function]}
+ updateTooltip={[Function]}
/>
+ <GraphHistory
+ events={Array []}
+ graph="overview"
+ graphEndDate={null}
+ graphStartDate={null}
+ isCustom={false}
+ leakPeriodDate="2017-05-16T13:50:02+0200"
+ measuresHistory={Array []}
+ removeCustomMetric={[Function]}
+ selectedDate={null}
+ series={
+ Array [
+ Object {
+ "data": Array [
+ Object {
+ "x": 2016-10-27T14:33:50.000Z,
+ "y": 5,
+ },
+ Object {
+ "x": 2016-10-27T10:21:15.000Z,
+ "y": 16,
+ },
+ Object {
+ "x": 2016-10-26T10:17:29.000Z,
+ "y": 12,
+ },
+ ],
+ "name": "bugs",
+ "translatedName": "metric.bugs.name",
+ },
+ ]
+ }
+ showAreas={false}
+ updateGraphZoom={[Function]}
+ updateSelectedDate={[Function]}
+ updateTooltip={[Function]}
+ />
+</div>
+`;
+
+exports[`should show that there is no history data 1`] = `
+<div
+ className="project-activity-graph-container"
+>
+ <div
+ className="note text-center"
+ >
+ component_measures.no_history
+ </div>
+</div>
+`;
+
+exports[`should show that there is no history data 2`] = `
+<div
+ className="project-activity-graph-container"
+>
<div
- className="project-activity-graph"
+ className="note text-center"
>
- <AutoSizer
- onResize={[Function]}
- />
+ component_measures.no_history
</div>
</div>
`;
graph="overview"
graphEndDate={null}
graphStartDate={null}
+ graphs={
+ Array [
+ Array [
+ Object {
+ "data": Array [
+ Object {
+ "x": 2016-10-26T10:17:29.000Z,
+ "y": 2286,
+ },
+ Object {
+ "x": 2016-10-27T10:21:15.000Z,
+ "y": 1749,
+ },
+ Object {
+ "x": 2016-10-27T14:33:50.000Z,
+ "y": 500,
+ },
+ ],
+ "name": "code_smells",
+ "translatedName": "Code Smells",
+ "type": "INT",
+ },
+ ],
+ ]
+ }
leakPeriodDate="2017-05-16T13:50:02+0200"
+ loading={false}
measuresHistory={
Array [
Object {
},
]
}
- metricsType="INT"
removeCustomMetric={[Function]}
series={
Array [
],
}
`;
-
-exports[`should show that there is no history data 1`] = `
-<div
- className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"
->
- <ProjectActivityGraphsHeader
- addCustomMetric={[Function]}
- graph="overview"
- metrics={
- Array [
- Object {
- "key": "code_smells",
- "name": "Code Smells",
- "type": "INT",
- },
- ]
- }
- metricsTypeFilter={null}
- updateGraph={[Function]}
- />
- <div
- className="project-activity-graph-container"
- >
- <div
- className="note text-center"
- >
- component_measures.no_history
- </div>
- </div>
- <GraphsZoom
- graphEndDate={null}
- graphStartDate={null}
- leakPeriodDate="2017-05-16T13:50:02+0200"
- loading={false}
- metricsType="INT"
- series={
- Array [
- Object {
- "data": Array [],
- "name": "code_smells",
- "translatedName": "Code Smells",
- "type": "INT",
- },
- ]
- }
- showAreas={false}
- updateGraphZoom={[Function]}
- />
-</div>
-`;
-
-exports[`should show that there is no history data 2`] = `
-<div
- className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"
->
- <ProjectActivityGraphsHeader
- addCustomMetric={[Function]}
- graph="custom"
- metrics={
- Array [
- Object {
- "key": "code_smells",
- "name": "Code Smells",
- "type": "INT",
- },
- ]
- }
- metricsTypeFilter={null}
- selectedMetrics={
- Array [
- "code_smells",
- ]
- }
- updateGraph={[Function]}
- />
- <div
- className="project-activity-graph-container"
- >
- <div
- className="note text-center"
- >
- project_activity.graphs.custom.no_history
- </div>
- </div>
- <GraphsZoom
- graphEndDate={null}
- graphStartDate={null}
- leakPeriodDate="2017-05-16T13:50:02+0200"
- loading={false}
- metricsType="INT"
- series={
- Array [
- Object {
- "data": Array [
- Object {
- "x": 2016-10-26T10:17:29.000Z,
- "y": NaN,
- },
- ],
- "name": "code_smells",
- "translatedName": "Code Smells",
- "type": "INT",
- },
- ]
- }
- showAreas={false}
- updateGraphZoom={[Function]}
- />
-</div>
-`;
padding-top: 52px;
}
+.project-activity-graphs {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ justify-content: center;
+}
+
.project-activity-graph-container {
padding: 10px 0;
flex-grow: 1;
// $FlowFixMe selectedDate can't be null there
p => p.x.valueOf() === selectedDate.valueOf()
);
- const xRange = xScale.range();
+ const xRange = xScale.range().sort();
const xPos = xScale(selectedDate);
if (idx >= 0 && xPos >= xRange[0] && xPos <= xRange[1]) {
return {