import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
import { getGraph } from '../../../helpers/storage';
import { METRICS, HISTORY_METRICS_LIST } from '../utils';
-import { GRAPHS_METRICS } from '../../projectActivity/utils';
+import { GRAPHS_METRICS_DISPLAYED } from '../../projectActivity/utils';
import type { Component, History, MeasuresList, Period } from '../types';
import '../styles.css';
}
loadHistory(component: Component) {
- const metrics = uniq(HISTORY_METRICS_LIST.concat(GRAPHS_METRICS[getGraph()]));
+ const metrics = uniq(HISTORY_METRICS_LIST.concat(GRAPHS_METRICS_DISPLAYED[getGraph()]));
return getAllTimeMachineData(component.key, metrics).then(r => {
if (this.mounted) {
const history: History = {};
import { map } from 'lodash';
import { Link } from 'react-router';
import { AutoSizer } from 'react-virtualized';
-import { generateSeries, GRAPHS_METRICS } from '../../projectActivity/utils';
+import { generateSeries, GRAPHS_METRICS_DISPLAYED } from '../../projectActivity/utils';
import { getGraph } from '../../../helpers/storage';
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
const measureHistory = map(history, (item, key) => ({
metric: key,
history: item.filter(p => p.value != null)
- })).filter(item => GRAPHS_METRICS[graph].indexOf(item.metric) >= 0);
+ })).filter(item => GRAPHS_METRICS_DISPLAYED[graph].indexOf(item.metric) >= 0);
return generateSeries(measureHistory, graph, metricsType);
};
getMetricType = (metrics: Array<Metric>, graph: string) => {
- const metricKey = GRAPHS_METRICS[graph][0];
+ const metricKey = GRAPHS_METRICS_DISPLAYED[graph][0];
const metric = metrics.find(metric => metric.key === metricKey);
return metric ? metric.type : 'INT';
};
Object {
"date": 2017-05-16T05:09:59.000Z,
"events": Array [
- Object {
- "category": "QUALITY_PROFILE",
- "key": "AVwQF7zXl-nNFgFWOJ3W",
- "name": "Changes in 'Default - SonarSource conventions' (Java)",
- },
Object {
"category": "VERSION",
"key": "AVyM9oI1HjR_PLDzRciU",
"name": "1.0",
},
+ Object {
+ "category": "QUALITY_PROFILE",
+ "key": "AVwQF7zXl-nNFgFWOJ3W",
+ "name": "Changes in 'Default - SonarSource conventions' (Java)",
+ },
],
"key": "AVwQF7kwl-nNFgFWOJ3V",
},
*/
// @flow
import React from 'react';
+import { sortBy } from 'lodash';
import Event from './Event';
import type { Event as EventType } from '../types';
};
export default function Events(props: Props) {
+ const sortedEvents = sortBy(
+ props.events,
+ // versions last
+ event => (event.category === 'VERSION' ? 1 : 0),
+ // then the rest sorted by category
+ 'category'
+ );
+
return (
<div className="project-activity-events">
- {props.events.map(event => (
+ {sortedEvents.map(event => (
<Event
analysis={props.analysis}
canAdmin={props.canAdmin}
--- /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.
+ */
+// @flow
+import React from 'react';
+import BubblePopup from '../../../components/common/BubblePopup';
+import FormattedDate from '../../../components/ui/FormattedDate';
+import GraphsTooltipsContent from './GraphsTooltipsContent';
+import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage';
+import GraphsTooltipsContentDuplication from './GraphsTooltipsContentDuplication';
+import GraphsTooltipsContentOverview from './GraphsTooltipsContentOverview';
+import type { MeasureHistory } from '../types';
+import type { Serie } from '../../../components/charts/AdvancedTimeline';
+
+type Props = {
+ formatValue: (number | string) => string,
+ graph: string,
+ graphWidth: number,
+ measuresHistory: Array<MeasureHistory>,
+ selectedDate: Date,
+ series: Array<Serie & { translatedName: string }>,
+ tooltipIdx: number,
+ tooltipPos: number
+};
+
+const TOOLTIP_WIDTH = 250;
+
+export default class GraphsTooltips extends React.PureComponent {
+ props: Props;
+
+ render() {
+ const { measuresHistory, tooltipIdx } = this.props;
+ const top = 50;
+ let left = this.props.tooltipPos + 60;
+ let customClass;
+ if (left > this.props.graphWidth - TOOLTIP_WIDTH - 50) {
+ left -= TOOLTIP_WIDTH;
+ customClass = 'bubble-popup-right';
+ }
+ return (
+ <BubblePopup customClass={customClass} position={{ top, left, width: TOOLTIP_WIDTH }}>
+ <div className="project-activity-graph-tooltip">
+ <div className="project-activity-graph-tooltip-title spacer-bottom">
+ <FormattedDate date={this.props.selectedDate} format="LL" />
+ </div>
+ <table className="width-100">
+ <tbody>
+ {this.props.series.map(serie => {
+ const point = serie.data[tooltipIdx];
+ if (!point || (!point.y && point.y !== 0)) {
+ return null;
+ }
+ return this.props.graph === 'overview'
+ ? <GraphsTooltipsContentOverview
+ key={serie.name}
+ measuresHistory={measuresHistory}
+ serie={serie}
+ tooltipIdx={tooltipIdx}
+ value={this.props.formatValue(point.y)}
+ />
+ : <GraphsTooltipsContent
+ key={serie.name}
+ serie={serie}
+ value={this.props.formatValue(point.y)}
+ />;
+ })}
+ </tbody>
+ {this.props.graph === 'coverage' &&
+ <GraphsTooltipsContentCoverage
+ measuresHistory={measuresHistory}
+ tooltipIdx={tooltipIdx}
+ />}
+ {this.props.graph === 'duplications' &&
+ <GraphsTooltipsContentDuplication
+ measuresHistory={measuresHistory}
+ tooltipIdx={tooltipIdx}
+ />}
+ </table>
+ </div>
+ </BubblePopup>
+ );
+ }
+}
--- /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.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
+import type { Serie } from '../../../components/charts/AdvancedTimeline';
+
+type Props = {
+ serie: Serie & { translatedName: string },
+ value: string
+};
+
+export default function GraphsTooltipsContent({ serie, value }: Props) {
+ return (
+ <tr key={serie.name} className="project-activity-graph-tooltip-line">
+ <td className="thin">
+ <ChartLegendIcon
+ className={classNames(
+ 'spacer-right line-chart-legend',
+ 'line-chart-legend-' + serie.style
+ )}
+ />
+ </td>
+ <td className="project-activity-graph-tooltip-value text-right spacer-right thin">
+ {value}
+ </td>
+ <td>{serie.translatedName}</td>
+ </tr>
+ );
+}
--- /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.
+ */
+// @flow
+import React from 'react';
+import { formatMeasure } from '../../../helpers/measures';
+import { translate } from '../../../helpers/l10n';
+import type { MeasureHistory } from '../types';
+
+type Props = {
+ measuresHistory: Array<MeasureHistory>,
+ tooltipIdx: number
+};
+
+export default function GraphsTooltipsContentCoverage({ measuresHistory, tooltipIdx }: Props) {
+ const uncovered = measuresHistory.find(measure => measure.metric === 'uncovered_lines');
+ const coverage = measuresHistory.find(measure => measure.metric === 'coverage');
+ if (!uncovered || !uncovered.history[tooltipIdx] || !coverage || !coverage.history[tooltipIdx]) {
+ return null;
+ }
+ const uncoveredValue = uncovered.history[tooltipIdx].value;
+ const coverageValue = coverage.history[tooltipIdx].value;
+ return (
+ <tbody>
+ <tr><td className="project-activity-graph-tooltip-separator" colSpan="3"><hr /></td></tr>
+ {uncoveredValue &&
+ <tr className="project-activity-graph-tooltip-line">
+ <td
+ colSpan="2"
+ className="project-activity-graph-tooltip-value text-right spacer-right thin">
+ {formatMeasure(uncoveredValue, 'SHORT_INT')}
+ </td>
+ <td>{translate('metric.uncovered_lines.name')}</td>
+ </tr>}
+ {coverageValue &&
+ <tr className="project-activity-graph-tooltip-line">
+ <td
+ colSpan="2"
+ className="project-activity-graph-tooltip-value text-right spacer-right thin">
+ {formatMeasure(coverageValue, 'PERCENT')}
+ </td>
+ <td>{translate('metric.coverage.name')}</td>
+ </tr>}
+ </tbody>
+ );
+}
--- /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.
+ */
+// @flow
+import React from 'react';
+import { formatMeasure } from '../../../helpers/measures';
+import { translate } from '../../../helpers/l10n';
+import type { MeasureHistory } from '../types';
+
+type Props = {
+ measuresHistory: Array<MeasureHistory>,
+ tooltipIdx: number
+};
+
+export default function GraphsTooltipsContentDuplication({ measuresHistory, tooltipIdx }: Props) {
+ const duplicationDensity = measuresHistory.find(
+ measure => measure.metric === 'duplicated_lines_density'
+ );
+ if (!duplicationDensity || !duplicationDensity.history[tooltipIdx]) {
+ return null;
+ }
+ const duplicationDensityValue = duplicationDensity.history[tooltipIdx].value;
+ if (!duplicationDensityValue) {
+ return null;
+ }
+ return (
+ <tbody>
+ <tr><td className="project-activity-graph-tooltip-separator" colSpan="3"><hr /></td></tr>
+ <tr className="project-activity-graph-tooltip-line">
+ <td
+ colSpan="2"
+ className="project-activity-graph-tooltip-value text-right spacer-right thin">
+ {formatMeasure(duplicationDensityValue, 'PERCENT')}
+ </td>
+ <td>{translate('metric.duplicated_lines_density.name')}</td>
+ </tr>
+ </tbody>
+ );
+}
--- /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.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
+import Rating from '../../../components/ui/Rating';
+import type { Serie } from '../../../components/charts/AdvancedTimeline';
+import type { MeasureHistory } from '../types';
+
+type Props = {
+ measuresHistory: Array<MeasureHistory>,
+ serie: Serie & { translatedName: string },
+ tooltipIdx: number,
+ value: string
+};
+
+const METRIC_RATING = {
+ bugs: 'reliability_rating',
+ vulnerabilities: 'security_rating',
+ code_smells: 'sqale_rating'
+};
+
+export default function GraphsTooltipsContentOverview(props: Props) {
+ const rating = props.measuresHistory.find(
+ measure => measure.metric === METRIC_RATING[props.serie.name]
+ );
+ if (!rating || !rating.history[props.tooltipIdx]) {
+ return null;
+ }
+ const ratingValue = rating.history[props.tooltipIdx].value;
+ return (
+ <tr key={props.serie.name} className="project-activity-graph-tooltip-overview-line">
+ <td className="thin">
+ <ChartLegendIcon
+ className={classNames(
+ 'spacer-right line-chart-legend',
+ 'line-chart-legend-' + props.serie.style
+ )}
+ />
+ </td>
+ <td className="text-right spacer-right thin">
+ <span className="project-activity-graph-tooltip-value">{props.value}</span>
+ {ratingValue && <Rating className="spacer-left" small={true} value={ratingValue} />}
+ </td>
+ <td>{props.serie.translatedName}</td>
+ </tr>
+ );
+}
import ProjectActivityPageHeader from './ProjectActivityPageHeader';
import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
import ProjectActivityGraphs from './ProjectActivityGraphs';
-import { GRAPHS_METRICS, activityQueryChanged } from '../utils';
+import { GRAPHS_METRICS_DISPLAYED, activityQueryChanged } from '../utils';
import { translate } from '../../../helpers/l10n';
import './projectActivity.css';
import type { Analysis, MeasureHistory, Metric, Query } from '../types';
};
getMetricType = () => {
- const metricKey = GRAPHS_METRICS[this.props.query.graph][0];
+ const metricKey = GRAPHS_METRICS_DISPLAYED[this.props.query.graph][0];
const metric = this.props.metrics.find(metric => metric.key === metricKey);
return metric ? metric.type : 'INT';
};
};
type State = {
+ selectedDate?: ?Date,
graphStartDate: ?Date,
graphEndDate: ?Date,
series: Array<Serie>
nextProps.query.graph,
nextProps.metricsType
);
-
const newDates = this.getStateZoomDates(this.props, nextProps, series);
if (newDates) {
this.setState({ series, ...newDates });
}
};
+ updateSelectedDate = (selectedDate: ?Date) => this.setState({ selectedDate });
+
updateGraphZoom = (graphStartDate: ?Date, graphEndDate: ?Date) => {
if (graphEndDate != null && graphStartDate != null) {
const msDiff = Math.abs(graphEndDate.valueOf() - graphStartDate.valueOf());
<StaticGraphs
analyses={this.props.analyses}
eventFilter={query.category}
+ graph={query.graph}
graphEndDate={this.state.graphEndDate}
graphStartDate={this.state.graphStartDate}
leakPeriodDate={leakPeriodDate}
loading={loading}
+ measuresHistory={this.props.measuresHistory}
metricsType={metricsType}
project={this.props.project}
+ selectedDate={this.state.selectedDate}
series={series}
- showAreas={['coverage', 'duplications'].includes(query.graph)}
updateGraphZoom={this.updateGraphZoom}
+ updateSelectedDate={this.updateSelectedDate}
/>
<GraphsZoom
graphEndDate={this.state.graphEndDate}
import { some, sortBy } from 'lodash';
import { AutoSizer } from 'react-virtualized';
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
+import GraphsTooltips from './GraphsTooltips';
import StaticGraphsLegend from './StaticGraphsLegend';
import { formatMeasure, getShortType } from '../../../helpers/measures';
import { EVENT_TYPES } from '../utils';
import { translate } from '../../../helpers/l10n';
-import type { Analysis } from '../types';
+import type { Analysis, MeasureHistory } from '../types';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
type Props = {
analyses: Array<Analysis>,
eventFilter: string,
+ graph: string,
graphEndDate: ?Date,
graphStartDate: ?Date,
leakPeriodDate: Date,
loading: boolean,
+ measuresHistory: Array<MeasureHistory>,
metricsType: string,
+ selectedDate?: ?Date => void,
series: Array<Serie>,
- showAreas?: boolean,
- updateGraphZoom: (from: ?Date, to: ?Date) => void
+ updateGraphZoom: (from: ?Date, to: ?Date) => void,
+ updateSelectedDate: (selectedDate: ?Date) => void
+};
+
+type State = {
+ tooltipIdx: ?number,
+ tooltipXPos: ?number
};
export default class StaticGraphs extends React.PureComponent {
props: Props;
+ state: State = {
+ tooltipIdx: null,
+ tooltipXPos: null
+ };
- formatYTick = tick => formatMeasure(tick, getShortType(this.props.metricsType));
+ formatValue = tick => formatMeasure(tick, getShortType(this.props.metricsType));
getEvents = () => {
const { analyses, eventFilter } = this.props;
hasSeriesData = () => some(this.props.series, serie => serie.data && serie.data.length > 2);
+ updateTooltipPos = (tooltipXPos: ?number, tooltipIdx: ?number) =>
+ this.setState({ tooltipXPos, tooltipIdx });
+
render() {
const { loading } = this.props;
);
}
- const { series } = this.props;
+ const { graph, selectedDate, series } = this.props;
return (
<div className="project-activity-graph-container">
<StaticGraphsLegend series={series} />
<div className="project-activity-graph">
<AutoSizer>
{({ height, width }) => (
- <AdvancedTimeline
- endDate={this.props.graphEndDate}
- events={this.getEvents()}
- height={height}
- width={width}
- interpolate="linear"
- formatYTick={this.formatYTick}
- leakPeriodDate={this.props.leakPeriodDate}
- metricType={this.props.metricsType}
- series={series}
- showAreas={this.props.showAreas}
- startDate={this.props.graphStartDate}
- updateZoom={this.props.updateGraphZoom}
- />
+ <div>
+ <AdvancedTimeline
+ endDate={this.props.graphEndDate}
+ events={this.getEvents()}
+ height={height}
+ width={width}
+ interpolate="linear"
+ formatYTick={this.formatValue}
+ leakPeriodDate={this.props.leakPeriodDate}
+ metricType={this.props.metricsType}
+ selectedDate={selectedDate}
+ series={series}
+ showAreas={['coverage', 'duplications'].includes(graph)}
+ startDate={this.props.graphStartDate}
+ updateSelectedDate={this.props.updateSelectedDate}
+ updateTooltipPos={this.updateTooltipPos}
+ updateZoom={this.props.updateGraphZoom}
+ />
+ {selectedDate != null &&
+ this.state.tooltipXPos != null &&
+ <GraphsTooltips
+ formatValue={this.formatValue}
+ graph={graph}
+ graphWidth={width}
+ measuresHistory={this.props.measuresHistory}
+ selectedDate={selectedDate}
+ series={series}
+ tooltipIdx={this.state.tooltipIdx}
+ tooltipPos={this.state.tooltipXPos}
+ />}
+ </div>
)}
</AutoSizer>
</div>
--- /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 GraphsTooltips from '../GraphsTooltips';
+
+const SERIES_OVERVIEW = [
+ {
+ name: 'code_smells',
+ translatedName: 'Code Smells',
+ style: 1,
+ data: [
+ {
+ x: '2011-10-01T22:01:00.000Z',
+ y: 18
+ },
+ {
+ x: '2011-10-25T10:27:41.000Z',
+ y: 15
+ }
+ ]
+ },
+ {
+ name: 'bugs',
+ translatedName: 'Bugs',
+ style: 0,
+ data: [
+ {
+ x: '2011-10-01T22:01:00.000Z',
+ y: 3
+ },
+ {
+ x: '2011-10-25T10:27:41.000Z',
+ y: 0
+ }
+ ]
+ },
+ {
+ name: 'vulnerabilities',
+ translatedName: 'Vulnerabilities',
+ style: 2,
+ data: [
+ {
+ x: '2011-10-01T22:01:00.000Z',
+ y: 0
+ },
+ {
+ x: '2011-10-25T10:27:41.000Z',
+ y: 1
+ }
+ ]
+ }
+];
+
+const DEFAULT_PROPS = {
+ formatValue: val => 'Formated.' + val,
+ graph: 'overview',
+ graphWidth: 500,
+ measuresHistory: [],
+ selectedDate: new Date('2011-10-01T22:01:00.000Z'),
+ series: SERIES_OVERVIEW,
+ tooltipIdx: 0,
+ tooltipPos: 666
+};
+
+it('should render correctly for overview graphs', () => {
+ expect(shallow(<GraphsTooltips {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should render correctly for random graphs', () => {
+ expect(
+ shallow(
+ <GraphsTooltips
+ {...DEFAULT_PROPS}
+ graph="random"
+ selectedDate={new Date('2011-10-25T10:27:41.000Z')}
+ tooltipIdx={1}
+ />
+ )
+ ).toMatchSnapshot();
+});
--- /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 GraphsTooltipsContent from '../GraphsTooltipsContent';
+
+const DEFAULT_PROPS = {
+ serie: {
+ name: 'code_smells',
+ translatedName: 'Code Smells',
+ style: 1
+ },
+ value: '1.2k'
+};
+
+it('should render correctly', () => {
+ expect(shallow(<GraphsTooltipsContent {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
--- /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 GraphsTooltipsContentCoverage from '../GraphsTooltipsContentCoverage';
+
+const MEASURES_COVERAGE = [
+ {
+ metric: 'coverage',
+ history: [
+ {
+ date: '2011-10-01T22:01:00.000Z'
+ },
+ {
+ date: '2011-10-25T10:27:41.000Z',
+ value: '80.3'
+ }
+ ]
+ },
+ {
+ metric: 'lines_to_cover',
+ history: [
+ {
+ date: '2011-10-01T22:01:00.000Z',
+ value: '60545'
+ },
+ {
+ date: '2011-10-25T10:27:41.000Z',
+ value: '65215'
+ }
+ ]
+ },
+ {
+ metric: 'uncovered_lines',
+ history: [
+ {
+ date: '2011-10-01T22:01:00.000Z',
+ value: '40564'
+ },
+ {
+ date: '2011-10-25T10:27:41.000Z',
+ value: '10245'
+ }
+ ]
+ }
+];
+
+const DEFAULT_PROPS = {
+ measuresHistory: MEASURES_COVERAGE,
+ tooltipIdx: 1
+};
+
+it('should render correctly', () => {
+ expect(shallow(<GraphsTooltipsContentCoverage {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should render correctly when data is missing', () => {
+ expect(
+ shallow(<GraphsTooltipsContentCoverage {...DEFAULT_PROPS} tooltipIdx={0} />)
+ ).toMatchSnapshot();
+});
--- /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 GraphsTooltipsContentDuplication from '../GraphsTooltipsContentDuplication';
+
+const MEASURES_DUPLICATION = [
+ {
+ metric: 'duplicated_lines_density',
+ history: [
+ {
+ date: '2011-10-01T22:01:00.000Z'
+ },
+ {
+ date: '2011-10-25T10:27:41.000Z',
+ value: '10245'
+ }
+ ]
+ }
+];
+
+const DEFAULT_PROPS = {
+ measuresHistory: MEASURES_DUPLICATION,
+ tooltipIdx: 1
+};
+
+it('should render correctly', () => {
+ expect(shallow(<GraphsTooltipsContentDuplication {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should render null when data is missing', () => {
+ expect(
+ shallow(<GraphsTooltipsContentDuplication {...DEFAULT_PROPS} tooltipIdx={0} />)
+ ).toMatchSnapshot();
+});
--- /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 GraphsTooltipsContentOverview from '../GraphsTooltipsContentOverview';
+
+const MEASURES_OVERVIEW = [
+ {
+ metric: 'bugs',
+ history: [
+ {
+ date: '2011-10-01T22:01:00.000Z',
+ value: '500'
+ },
+ {
+ date: '2011-10-25T10:27:41.000Z',
+ value: '1.2k'
+ }
+ ]
+ },
+ {
+ metric: 'reliability_rating',
+ history: [
+ {
+ date: '2011-10-01T22:01:00.000Z'
+ },
+ {
+ date: '2011-10-25T10:27:41.000Z',
+ value: '5.0'
+ }
+ ]
+ }
+];
+
+const DEFAULT_PROPS = {
+ measuresHistory: MEASURES_OVERVIEW,
+ serie: {
+ name: 'bugs',
+ translatedName: 'Bugs',
+ style: 2
+ },
+ tooltipIdx: 1,
+ value: '1.2k'
+};
+
+it('should render correctly', () => {
+ expect(shallow(<GraphsTooltipsContentOverview {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should render correctly when rating data is missing', () => {
+ expect(
+ shallow(<GraphsTooltipsContentOverview {...DEFAULT_PROPS} tooltipIdx={0} value="500" />)
+ ).toMatchSnapshot();
+});
const DEFAULT_PROPS = {
analyses: ANALYSES,
eventFilter: '',
- filteredSeries: SERIES,
+ graph: 'overview',
+ graphEndDate: null,
+ graphStartDate: null,
leakPeriodDate: '2017-05-16T13:50:02+0200',
loading: false,
+ measuresHistory: [],
+ metricsType: 'INT',
+ selectedDate: null,
series: SERIES,
- metricsType: 'INT'
+ updateGraphZoom: () => {},
+ updateSelectedDate: () => {}
};
it('should show a loading view', () => {
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly for overview graphs 1`] = `
+<BubblePopup
+ customClass="bubble-popup-right"
+ position={
+ Object {
+ "left": 476,
+ "top": 50,
+ "width": 250,
+ }
+ }
+>
+ <div
+ className="project-activity-graph-tooltip"
+ >
+ <div
+ className="project-activity-graph-tooltip-title spacer-bottom"
+ >
+ <FormattedDate
+ date={2011-10-01T22:01:00.000Z}
+ format="LL"
+ />
+ </div>
+ <table
+ className="width-100"
+ >
+ <tbody>
+ <GraphsTooltipsContentOverview
+ measuresHistory={Array []}
+ serie={
+ Object {
+ "data": Array [
+ Object {
+ "x": "2011-10-01T22:01:00.000Z",
+ "y": 18,
+ },
+ Object {
+ "x": "2011-10-25T10:27:41.000Z",
+ "y": 15,
+ },
+ ],
+ "name": "code_smells",
+ "style": 1,
+ "translatedName": "Code Smells",
+ }
+ }
+ tooltipIdx={0}
+ value="Formated.18"
+ />
+ <GraphsTooltipsContentOverview
+ measuresHistory={Array []}
+ serie={
+ Object {
+ "data": Array [
+ Object {
+ "x": "2011-10-01T22:01:00.000Z",
+ "y": 3,
+ },
+ Object {
+ "x": "2011-10-25T10:27:41.000Z",
+ "y": 0,
+ },
+ ],
+ "name": "bugs",
+ "style": 0,
+ "translatedName": "Bugs",
+ }
+ }
+ tooltipIdx={0}
+ value="Formated.3"
+ />
+ <GraphsTooltipsContentOverview
+ measuresHistory={Array []}
+ serie={
+ Object {
+ "data": Array [
+ Object {
+ "x": "2011-10-01T22:01:00.000Z",
+ "y": 0,
+ },
+ Object {
+ "x": "2011-10-25T10:27:41.000Z",
+ "y": 1,
+ },
+ ],
+ "name": "vulnerabilities",
+ "style": 2,
+ "translatedName": "Vulnerabilities",
+ }
+ }
+ tooltipIdx={0}
+ value="Formated.0"
+ />
+ </tbody>
+ </table>
+ </div>
+</BubblePopup>
+`;
+
+exports[`should render correctly for random graphs 1`] = `
+<BubblePopup
+ customClass="bubble-popup-right"
+ position={
+ Object {
+ "left": 476,
+ "top": 50,
+ "width": 250,
+ }
+ }
+>
+ <div
+ className="project-activity-graph-tooltip"
+ >
+ <div
+ className="project-activity-graph-tooltip-title spacer-bottom"
+ >
+ <FormattedDate
+ date={2011-10-25T10:27:41.000Z}
+ format="LL"
+ />
+ </div>
+ <table
+ className="width-100"
+ >
+ <tbody>
+ <GraphsTooltipsContent
+ serie={
+ Object {
+ "data": Array [
+ Object {
+ "x": "2011-10-01T22:01:00.000Z",
+ "y": 18,
+ },
+ Object {
+ "x": "2011-10-25T10:27:41.000Z",
+ "y": 15,
+ },
+ ],
+ "name": "code_smells",
+ "style": 1,
+ "translatedName": "Code Smells",
+ }
+ }
+ value="Formated.15"
+ />
+ <GraphsTooltipsContent
+ serie={
+ Object {
+ "data": Array [
+ Object {
+ "x": "2011-10-01T22:01:00.000Z",
+ "y": 3,
+ },
+ Object {
+ "x": "2011-10-25T10:27:41.000Z",
+ "y": 0,
+ },
+ ],
+ "name": "bugs",
+ "style": 0,
+ "translatedName": "Bugs",
+ }
+ }
+ value="Formated.0"
+ />
+ <GraphsTooltipsContent
+ serie={
+ Object {
+ "data": Array [
+ Object {
+ "x": "2011-10-01T22:01:00.000Z",
+ "y": 0,
+ },
+ Object {
+ "x": "2011-10-25T10:27:41.000Z",
+ "y": 1,
+ },
+ ],
+ "name": "vulnerabilities",
+ "style": 2,
+ "translatedName": "Vulnerabilities",
+ }
+ }
+ value="Formated.1"
+ />
+ </tbody>
+ </table>
+ </div>
+</BubblePopup>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tr
+ className="project-activity-graph-tooltip-line"
+>
+ <td
+ className="thin"
+ >
+ <ChartLegendIcon
+ className="spacer-right line-chart-legend line-chart-legend-1"
+ />
+ </td>
+ <td
+ className="project-activity-graph-tooltip-value text-right spacer-right thin"
+ >
+ 1.2k
+ </td>
+ <td>
+ Code Smells
+ </td>
+</tr>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tbody>
+ <tr>
+ <td
+ className="project-activity-graph-tooltip-separator"
+ colSpan="3"
+ >
+ <hr />
+ </td>
+ </tr>
+ <tr
+ className="project-activity-graph-tooltip-line"
+ >
+ <td
+ className="project-activity-graph-tooltip-value text-right spacer-right thin"
+ colSpan="2"
+ >
+ 10k
+ </td>
+ <td>
+ metric.uncovered_lines.name
+ </td>
+ </tr>
+ <tr
+ className="project-activity-graph-tooltip-line"
+ >
+ <td
+ className="project-activity-graph-tooltip-value text-right spacer-right thin"
+ colSpan="2"
+ >
+ 80.3%
+ </td>
+ <td>
+ metric.coverage.name
+ </td>
+ </tr>
+</tbody>
+`;
+
+exports[`should render correctly when data is missing 1`] = `
+<tbody>
+ <tr>
+ <td
+ className="project-activity-graph-tooltip-separator"
+ colSpan="3"
+ >
+ <hr />
+ </td>
+ </tr>
+ <tr
+ className="project-activity-graph-tooltip-line"
+ >
+ <td
+ className="project-activity-graph-tooltip-value text-right spacer-right thin"
+ colSpan="2"
+ >
+ 41k
+ </td>
+ <td>
+ metric.uncovered_lines.name
+ </td>
+ </tr>
+</tbody>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tbody>
+ <tr>
+ <td
+ className="project-activity-graph-tooltip-separator"
+ colSpan="3"
+ >
+ <hr />
+ </td>
+ </tr>
+ <tr
+ className="project-activity-graph-tooltip-line"
+ >
+ <td
+ className="project-activity-graph-tooltip-value text-right spacer-right thin"
+ colSpan="2"
+ >
+ 10,245.0%
+ </td>
+ <td>
+ metric.duplicated_lines_density.name
+ </td>
+ </tr>
+</tbody>
+`;
+
+exports[`should render null when data is missing 1`] = `null`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tr
+ className="project-activity-graph-tooltip-overview-line"
+>
+ <td
+ className="thin"
+ >
+ <ChartLegendIcon
+ className="spacer-right line-chart-legend line-chart-legend-2"
+ />
+ </td>
+ <td
+ className="text-right spacer-right thin"
+ >
+ <span
+ className="project-activity-graph-tooltip-value"
+ >
+ 1.2k
+ </span>
+ <Rating
+ className="spacer-left"
+ muted={false}
+ small={true}
+ value="5.0"
+ />
+ </td>
+ <td>
+ Bugs
+ </td>
+</tr>
+`;
+
+exports[`should render correctly when rating data is missing 1`] = `
+<tr
+ className="project-activity-graph-tooltip-overview-line"
+>
+ <td
+ className="thin"
+ >
+ <ChartLegendIcon
+ className="spacer-right line-chart-legend line-chart-legend-2"
+ />
+ </td>
+ <td
+ className="text-right spacer-right thin"
+ >
+ <span
+ className="project-activity-graph-tooltip-value"
+ >
+ 500
+ </span>
+ </td>
+ <td>
+ Bugs
+ </td>
+</tr>
+`;
]
}
eventFilter=""
+ graph="overview"
graphEndDate={2016-10-27T14:33:50.000Z}
graphStartDate={2016-10-26T10:17:29.000Z}
leakPeriodDate="2017-05-16T13:50:02+0200"
loading={false}
+ measuresHistory={
+ Array [
+ Object {
+ "history": Array [
+ Object {
+ "date": 2016-10-26T10:17:29.000Z,
+ "value": "2286",
+ },
+ Object {
+ "date": 2016-10-27T10:21:15.000Z,
+ "value": "1749",
+ },
+ Object {
+ "date": 2016-10-27T14:33:50.000Z,
+ "value": "500",
+ },
+ ],
+ "metric": "code_smells",
+ },
+ ]
+ }
metricsType="INT"
project="org.sonarsource.sonarqube:sonarqube"
series={
},
]
}
- showAreas={false}
updateGraphZoom={[Function]}
+ updateSelectedDate={[Function]}
/>
<GraphsZoom
graphEndDate={2016-10-27T14:33:50.000Z}
text-align: center;
}
+.project-activity-graph-tooltip {
+ padding: 8px;
+ pointer-events: none;
+}
+
+.project-activity-graph-tooltip-line {
+ height: 20px;
+ padding-bottom: 4px;
+}
+
+.project-activity-graph-tooltip-overview-line {
+ height: 26px;
+ padding-bottom: 4px;
+}
+
+.project-activity-graph-tooltip-separator {
+ padding-left: 16px;
+ padding-right: 16px;
+}
+
+.project-activity-graph-tooltip-separator hr {
+ margin-top: 8px;
+ margin-bottom: 8px;
+}
+
+.project-activity-graph-tooltip-title, .project-activity-graph-tooltip-value {
+ font-weight: bold;
+}
+
.project-activity-days-list {}
.project-activity-day {
padding: 4px;
border-top: 1px solid #e6e6e6;
border-bottom: 1px solid #e6e6e6;
+ cursor: pointer;
}
.project-activity-analysis:hover {
*/
// @flow
import moment from 'moment';
-import { sortBy } from 'lodash';
import {
cleanQuery,
parseAsDate,
export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'];
export const GRAPH_TYPES = ['overview', 'coverage', 'duplications'];
-export const GRAPHS_METRICS = {
+export const GRAPHS_METRICS_DISPLAYED = {
overview: ['bugs', 'code_smells', 'vulnerabilities'],
coverage: ['uncovered_lines', 'lines_to_cover'],
duplications: ['duplicated_lines', 'ncloc']
};
+export const GRAPHS_METRICS = {
+ overview: GRAPHS_METRICS_DISPLAYED['overview'].concat([
+ 'reliability_rating',
+ 'security_rating',
+ 'sqale_rating'
+ ]),
+ coverage: GRAPHS_METRICS_DISPLAYED['coverage'].concat(['coverage']),
+ duplications: GRAPHS_METRICS_DISPLAYED['duplications'].concat(['duplicated_lines_density'])
+};
export const activityQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
prevQuery.category !== nextQuery.category ||
graph: string,
dataType: string
): Array<Serie> =>
- measuresHistory.map(measure => {
- if (measure.metric === 'uncovered_lines') {
- return generateCoveredLinesMetric(
- measure,
- measuresHistory,
- GRAPHS_METRICS[graph].indexOf(measure.metric)
- );
- }
- return {
- name: measure.metric,
- translatedName: translate('metric', measure.metric, 'name'),
- style: GRAPHS_METRICS[graph].indexOf(measure.metric),
- data: measure.history.map(analysis => ({
- x: analysis.date,
- y: dataType === 'LEVEL' ? analysis.value : Number(analysis.value)
- }))
- };
- });
+ measuresHistory
+ .filter(measure => GRAPHS_METRICS_DISPLAYED[graph].indexOf(measure.metric) >= 0)
+ .map(measure => {
+ if (measure.metric === 'uncovered_lines') {
+ return generateCoveredLinesMetric(
+ measure,
+ measuresHistory,
+ GRAPHS_METRICS_DISPLAYED[graph].indexOf(measure.metric)
+ );
+ }
+ return {
+ name: measure.metric,
+ translatedName: translate('metric', measure.metric, 'name'),
+ style: GRAPHS_METRICS_DISPLAYED[graph].indexOf(measure.metric),
+ data: measure.history.map(analysis => ({
+ x: analysis.date,
+ y: dataType === 'LEVEL' ? analysis.value : Number(analysis.value)
+ }))
+ };
+ });
export const getAnalysesByVersionByDay = (
analyses: Array<Analysis>
if (!currentVersion.byDay[day]) {
currentVersion.byDay[day] = [];
}
- const sortedEvents = sortBy(
- analysis.events,
- // versions last
- event => (event.category === 'VERSION' ? 1 : 0),
- // then the rest sorted by category
- 'category'
- );
- currentVersion.byDay[day].push({ ...analysis, events: sortedEvents });
+ currentVersion.byDay[day].push(analysis);
- const lastEvent = sortedEvents[sortedEvents.length - 1];
- if (lastEvent && lastEvent.category === 'VERSION') {
- currentVersion.version = lastEvent.name;
- currentVersion.key = lastEvent.key;
+ const versionEvent = analysis.events.find(event => event.category === 'VERSION');
+ if (versionEvent && versionEvent.category === 'VERSION') {
+ currentVersion.version = versionEvent.name;
+ currentVersion.key = versionEvent.key;
acc.push({ version: undefined, key: undefined, byDay: {} });
}
return acc;
// @flow
import React from 'react';
import classNames from 'classnames';
-import { flatten, sortBy } from 'lodash';
-import { extent, max } from 'd3-array';
+import { throttle, flatten, sortBy } from 'lodash';
+import { bisector, extent, max } from 'd3-array';
import { scaleLinear, scalePoint, scaleTime } from 'd3-scale';
import { line as d3Line, area, curveBasis } from 'd3-shape';
height: number,
width: number,
leakPeriodDate?: Date,
+ metricType: string,
padding: Array<number>,
+ selectedDate?: Date,
series: Array<Serie>,
showAreas?: boolean,
showEventMarkers?: boolean,
startDate: ?Date,
+ updateSelectedDate?: (selectedDate: ?Date) => void,
+ updateTooltipPos?: (tooltipXPos: ?number, tooltipIdx: ?number) => void,
updateZoom?: (start: ?Date, endDate: ?Date) => void,
zoomSpeed: number
};
+type State = {
+ maxXRange: Array<number>,
+ mouseOver?: boolean,
+ mouseOverlayPos?: { [string]: number },
+ selectedDateXPos: ?number,
+ selectedDateIdx: ?number,
+ yScale: Scale,
+ xScale: Scale
+};
+
export default class AdvancedTimeline extends React.PureComponent {
props: Props;
+ state: State;
static defaultProps = {
eventSize: 8,
zoomSpeed: 1
};
+ constructor(props: Props) {
+ super(props);
+ const scales = this.getScales(props);
+ this.state = { ...scales, ...this.getSelectedDatePos(scales.xScale, props.selectedDate) };
+ this.updateSelectedDate = throttle(this.updateSelectedDate, 40);
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ let scales;
+ if (
+ nextProps.metricType !== this.props.metricType ||
+ nextProps.startDate !== this.props.startDate ||
+ nextProps.endDate !== this.props.endDate ||
+ nextProps.width !== this.props.width ||
+ nextProps.padding !== this.props.padding ||
+ nextProps.height !== this.props.height ||
+ nextProps.series !== this.props.series
+ ) {
+ scales = this.getScales(nextProps);
+ }
+
+ if (scales || nextProps.selectedDate !== this.props.selectedDate) {
+ const xScale = scales ? scales.xScale : this.state.xScale;
+ const selectedDatePos = this.getSelectedDatePos(xScale, nextProps.selectedDate);
+ this.setState({ ...scales, ...selectedDatePos });
+ if (nextProps.updateTooltipPos) {
+ nextProps.updateTooltipPos(
+ selectedDatePos.selectedDateXPos,
+ selectedDatePos.selectedDateIdx
+ );
+ }
+ }
+ }
+
getRatingScale = (availableHeight: number) =>
scalePoint().domain([5, 4, 3, 2, 1]).range([availableHeight, 0]);
getLevelScale = (availableHeight: number) =>
scalePoint().domain(['ERROR', 'WARN', 'OK']).range([availableHeight, 0]);
- getYScale = (availableHeight: number, flatData: Array<Point>) => {
- if (this.props.metricType === 'RATING') {
+ getYScale = (props: Props, availableHeight: number, flatData: Array<Point>) => {
+ if (props.metricType === 'RATING') {
return this.getRatingScale(availableHeight);
- } else if (this.props.metricType === 'LEVEL') {
+ } else if (props.metricType === 'LEVEL') {
return this.getLevelScale(availableHeight);
} else {
return scaleLinear().range([availableHeight, 0]).domain([0, max(flatData, d => d.y)]).nice();
}
};
- getXScale = (availableWidth: number, flatData: Array<Point>) => {
+ getXScale = (props: Props, availableWidth: number, flatData: Array<Point>) => {
const dateRange = extent(flatData, d => d.x);
- const start = this.props.startDate ? this.props.startDate : dateRange[0];
- const end = this.props.endDate ? this.props.endDate : dateRange[1];
+ const start = props.startDate ? props.startDate : dateRange[0];
+ const end = props.endDate ? props.endDate : dateRange[1];
const xScale = scaleTime().domain(sortBy([start, end])).range([0, availableWidth]).clamp(false);
return {
xScale,
};
};
- getScales = () => {
- const availableWidth = this.props.width - this.props.padding[1] - this.props.padding[3];
- const availableHeight = this.props.height - this.props.padding[0] - this.props.padding[2];
- const flatData = flatten(this.props.series.map((serie: Serie) => serie.data));
+ getScales = (props: Props) => {
+ const availableWidth = props.width - props.padding[1] - props.padding[3];
+ const availableHeight = props.height - props.padding[0] - props.padding[2];
+ const flatData = flatten(props.series.map((serie: Serie) => serie.data));
return {
- ...this.getXScale(availableWidth, flatData),
- yScale: this.getYScale(availableHeight, flatData)
+ ...this.getXScale(props, availableWidth, flatData),
+ yScale: this.getYScale(props, availableHeight, flatData)
};
};
+ getSelectedDatePos = (xScale: Scale, selectedDate: ?Date) => {
+ const firstSerie = this.props.series[0];
+ if (selectedDate && firstSerie) {
+ const idx = firstSerie.data.findIndex(
+ // $FlowFixMe selectedDate can't be null there
+ p => p.x.valueOf() === selectedDate.valueOf()
+ );
+ if (
+ idx >= 0 &&
+ this.props.series.some(serie => serie.data[idx].y || serie.data[idx].y === 0)
+ ) {
+ return {
+ selectedDateXPos: xScale(selectedDate),
+ selectedDateIdx: idx
+ };
+ }
+ }
+ return { selectedDateXPos: null, selectedDateIdx: null };
+ };
+
getEventMarker = (size: number) => {
const half = size / 2;
return `M${half} 0 L${size} ${half} L ${half} ${size} L0 ${half} L${half} 0 L${size} ${half}`;
};
- handleWheel = (xScale: Scale, maxXRange: Array<number>) => (
- evt: WheelEvent & { target: HTMLElement }
- ) => {
+ getMouseOverlayPos = (target: HTMLElement) => {
+ if (this.state.mouseOverlayPos) {
+ return this.state.mouseOverlayPos;
+ }
+ const pos = target.getBoundingClientRect();
+ this.setState({ mouseOverlayPos: pos });
+ return pos;
+ };
+
+ handleWheel = (evt: WheelEvent & { target: HTMLElement }) => {
evt.preventDefault();
- const parentBbox = evt.target.getBoundingClientRect();
- const mouseXPos = (evt.clientX - parentBbox.left) / parentBbox.width;
+ const { maxXRange, xScale } = this.state;
+ const parentBbox = this.getMouseOverlayPos(evt.target);
+ const mouseXPos = (evt.pageX - parentBbox.left) / parentBbox.width;
const xRange = xScale.range();
const speed = evt.deltaMode ? 25 / evt.deltaMode * this.props.zoomSpeed : this.props.zoomSpeed;
const leftPos = xRange[0] - Math.round(speed * evt.deltaY * mouseXPos);
this.props.updateZoom(startDate, endDate);
};
- renderHorizontalGrid = (xScale: Scale, yScale: Scale) => {
+ handleMouseMove = (evt: MouseEvent & { target: HTMLElement }) => {
+ const parentBbox = this.getMouseOverlayPos(evt.target);
+ this.updateSelectedDate(evt.pageX - parentBbox.left);
+ };
+
+ handleMouseEnter = () => this.setState({ mouseOver: true });
+
+ handleMouseOut = (evt: Event & { relatedTarget: HTMLElement }) => {
+ const { updateSelectedDate } = this.props;
+ const targetClass = evt.relatedTarget && typeof evt.relatedTarget.className === 'string'
+ ? evt.relatedTarget.className
+ : '';
+ if (
+ !updateSelectedDate ||
+ targetClass.includes('bubble-popup') ||
+ targetClass.includes('graph-tooltip')
+ ) {
+ return;
+ }
+ this.setState({ mouseOver: false });
+ updateSelectedDate(null);
+ };
+
+ updateSelectedDate = (xPos: number) => {
+ const { updateSelectedDate } = this.props;
+ const firstSerie = this.props.series[0];
+ if (this.state.mouseOver && firstSerie && updateSelectedDate) {
+ const date = this.state.xScale.invert(xPos);
+ const bisectX = bisector(d => d.x).right;
+ let idx = bisectX(firstSerie.data, date);
+ if (idx >= 0) {
+ const previousPoint = firstSerie.data[idx - 1];
+ const nextPoint = firstSerie.data[idx];
+ if (!nextPoint || (previousPoint && date - previousPoint.x <= nextPoint.x - date)) {
+ idx--;
+ }
+ updateSelectedDate(firstSerie.data[idx].x);
+ }
+ }
+ };
+
+ renderHorizontalGrid = () => {
const { formatYTick } = this.props;
+ const { xScale, yScale } = this.state;
const hasTicks = typeof yScale.ticks === 'function';
const ticks = hasTicks ? yScale.ticks(4) : yScale.domain();
);
};
- renderXAxisTicks = (xScale: Scale, yScale: Scale) => {
+ renderXAxisTicks = () => {
+ const { xScale, yScale } = this.state;
const format = xScale.tickFormat(7);
const ticks = xScale.ticks(7);
const y = yScale.range()[0];
);
};
- renderLeak = (xScale: Scale, yScale: Scale) => {
- const yRange = yScale.range();
- const xRange = xScale.range();
- const leakWidth = xRange[xRange.length - 1] - xScale(this.props.leakPeriodDate);
+ renderLeak = () => {
+ const yRange = this.state.yScale.range();
+ const xRange = this.state.xScale.range();
+ const leakWidth = xRange[xRange.length - 1] - this.state.xScale(this.props.leakPeriodDate);
if (leakWidth < 0) {
return null;
}
return (
<rect
- x={xScale(this.props.leakPeriodDate)}
+ x={this.state.xScale(this.props.leakPeriodDate)}
y={yRange[yRange.length - 1]}
width={leakWidth}
height={yRange[0] - yRange[yRange.length - 1]}
);
};
- renderLines = (xScale: Scale, yScale: Scale) => {
+ renderLines = () => {
const lineGenerator = d3Line()
.defined(d => d.y || d.y === 0)
- .x(d => xScale(d.x))
- .y(d => yScale(d.y));
+ .x(d => this.state.xScale(d.x))
+ .y(d => this.state.yScale(d.y));
if (this.props.basisCurve) {
lineGenerator.curve(curveBasis);
}
return (
<g>
- {this.props.series.map((serie, idx) => (
+ {this.props.series.map(serie => (
<path
- key={`${idx}-${serie.name}`}
+ key={serie.name}
className={classNames('line-chart-path', 'line-chart-path-' + serie.style)}
d={lineGenerator(serie.data)}
/>
);
};
- renderAreas = (xScale: Scale, yScale: Scale) => {
+ renderAreas = () => {
const areaGenerator = area()
.defined(d => d.y || d.y === 0)
- .x(d => xScale(d.x))
- .y1(d => yScale(d.y))
- .y0(yScale(0));
+ .x(d => this.state.xScale(d.x))
+ .y1(d => this.state.yScale(d.y))
+ .y0(this.state.yScale(0));
if (this.props.basisCurve) {
areaGenerator.curve(curveBasis);
}
return (
<g>
- {this.props.series.map((serie, idx) => (
+ {this.props.series.map(serie => (
<path
- key={`${idx}-${serie.name}`}
+ key={serie.name}
className={classNames('line-chart-area', 'line-chart-area-' + serie.style)}
d={areaGenerator(serie.data)}
/>
);
};
- renderEvents = (xScale: Scale, yScale: Scale) => {
+ renderEvents = () => {
const { events, eventSize } = this.props;
if (!events || !eventSize) {
return null;
}
+ const { xScale, yScale } = this.state;
const inRangeEvents = events.filter(
event => event.date >= xScale.domain()[0] && event.date <= xScale.domain()[1]
);
);
};
- renderClipPath = (xScale: Scale, yScale: Scale) => {
+ renderSelectedDate = () => {
+ const { selectedDateIdx, selectedDateXPos, yScale } = this.state;
+ const firstSerie = this.props.series[0];
+ if (selectedDateIdx == null || selectedDateXPos == null || !firstSerie) {
+ return null;
+ }
+
+ return (
+ <g>
+ <line
+ className="line-tooltip"
+ x1={selectedDateXPos}
+ x2={selectedDateXPos}
+ y1={yScale.range()[0]}
+ y2={yScale.range()[1]}
+ />
+ {this.props.series.map(serie => {
+ const point = serie.data[selectedDateIdx];
+ if (!point || (!point.y && point.y !== 0)) {
+ return null;
+ }
+ return (
+ <circle
+ key={serie.name}
+ cx={selectedDateXPos}
+ cy={yScale(point.y)}
+ r="4"
+ className={classNames('line-chart-dot', 'line-chart-dot-' + serie.style)}
+ />
+ );
+ })}
+ </g>
+ );
+ };
+
+ renderClipPath = () => {
return (
<defs>
<clipPath id="chart-clip">
- <rect width={xScale.range()[1]} height={yScale.range()[0] + 10} />
+ <rect width={this.state.xScale.range()[1]} height={this.state.yScale.range()[0] + 10} />
</clipPath>
</defs>
);
};
- renderZoomOverlay = (xScale: Scale, yScale: Scale, maxXRange: Array<number>) => {
+ renderMouseEventsOverlay = (zoomEnabled: boolean) => {
+ const mouseEvents = {};
+ if (zoomEnabled) {
+ mouseEvents.onWheel = this.handleWheel;
+ }
+ if (this.props.updateSelectedDate) {
+ mouseEvents.onMouseEnter = this.handleMouseEnter;
+ mouseEvents.onMouseMove = this.handleMouseMove;
+ mouseEvents.onMouseOut = this.handleMouseOut;
+ }
return (
<rect
- className="chart-wheel-zoom-overlay"
- width={xScale.range()[1]}
- height={yScale.range()[0]}
- onWheel={this.handleWheel(xScale, maxXRange)}
+ className="chart-mouse-events-overlay"
+ width={this.state.xScale.range()[1]}
+ height={this.state.yScale.range()[0]}
+ {...mouseEvents}
/>
);
};
if (!this.props.width || !this.props.height) {
return <div />;
}
-
- const { maxXRange, xScale, yScale } = this.getScales();
const zoomEnabled = !this.props.disableZoom && this.props.updateZoom != null;
const isZoomed = this.props.startDate || this.props.endDate;
return (
className={classNames('line-chart', { 'chart-zoomed': isZoomed })}
width={this.props.width}
height={this.props.height}>
- {zoomEnabled && this.renderClipPath(xScale, yScale)}
+ {zoomEnabled && this.renderClipPath()}
<g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
- {this.props.leakPeriodDate != null && this.renderLeak(xScale, yScale)}
- {!this.props.hideGrid && this.renderHorizontalGrid(xScale, yScale)}
- {!this.props.hideXAxis && this.renderXAxisTicks(xScale, yScale)}
- {this.props.showAreas && this.renderAreas(xScale, yScale)}
- {this.renderLines(xScale, yScale)}
- {zoomEnabled && this.renderZoomOverlay(xScale, yScale, maxXRange)}
- {this.props.showEventMarkers && this.renderEvents(xScale, yScale)}
+ {this.props.leakPeriodDate != null && this.renderLeak()}
+ {!this.props.hideGrid && this.renderHorizontalGrid()}
+ {!this.props.hideXAxis && this.renderXAxisTicks()}
+ {this.props.showAreas && this.renderAreas()}
+ {this.renderLines()}
+ {this.props.showEventMarkers && this.renderEvents()}
+ {this.renderSelectedDate()}
+ {this.renderMouseEventsOverlay(zoomEnabled)}
</g>
</svg>
);
export default class Rating extends React.PureComponent {
static propTypes = {
+ className: React.PropTypes.string,
value: (props, propName, componentName) => {
// allow both numbers and strings
const numberValue = Number(props[propName]);
render() {
const formatted = formatMeasure(this.props.value, 'RATING');
- const className = classNames('rating', 'rating-' + formatted, {
- 'rating-small': this.props.small,
- 'rating-muted': this.props.muted
- });
+ const className = classNames(
+ 'rating',
+ 'rating-' + formatted,
+ {
+ 'rating-small': this.props.small,
+ 'rating-muted': this.props.muted
+ },
+ this.props.className
+ );
return <span className={className}>{formatted}</span>;
}
}
.bubble-popup-bottom-right {
.bubble-popup-bottom;
margin-left: 0;
- margin-right: -8px;
+ margin-right: -@popupArrowSize;
.bubble-popup-arrow {
left: auto;
right: 15px;
+ border-right-width: 0;
+ border-left-color: barBorderColor;
+ }
+}
+
+.bubble-popup-right {
+ margin-left: -@popupArrowSize;
+
+ .bubble-popup-arrow {
+ right: -@popupArrowSize;
+ left: auto;
+ border-right-width: 0;
+ border-left-width: @popupArrowSize;
+ border-left-color: @barBorderColor;
+ border-right-color: transparent;
+
+ &:after {
+ left: auto;
+ right: 1px;
+ bottom: -@popupArrowSize;
+ border-right-width: 0;
+ border-left-width: @popupArrowSize;
+ border-left-color: @white;
+ border-right-color: transparent;
+ }
}
}
&.line-chart-path-2 {
stroke: @serieColor2;
}
-
- &:hover {
- z-index: 120;
- }
}
.line-chart-area {
}
}
+.line-chart-dot {
+ fill: @defaultSerieColor;
+
+ &.line-chart-dot-1 {
+ fill: @serieColor1;
+ }
+
+ &.line-chart-dot-2 {
+ fill: @serieColor2;
+ }
+}
+
.line-chart-point {
fill: #fff;
stroke: @defaultSerieColor;
text-anchor: middle;
}
-.chart-wheel-zoom-overlay {
+.chart-mouse-events-overlay {
fill: none;
stroke: none;
pointer-events: all;
stroke: none;
}
}
+
+/*
+ * Charts tooltips
+ */
+
+.line-tooltip {
+ fill: none;
+ stroke: @secondFontColor;
+ stroke-width: 1px;
+ shape-rendering: crispEdges;
+}