aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js')
-rw-r--r--server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js217
1 files changed, 217 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
new file mode 100644
index 00000000000..a7cfa6b848c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
@@ -0,0 +1,217 @@
+/*
+ * 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 { flatten } from 'lodash';
+import { extent, max } from 'd3-array';
+import { scaleLinear, scalePoint, scaleTime } from 'd3-scale';
+import { line as d3Line, curveBasis } from 'd3-shape';
+
+type Point = { x: Date, y: number | string };
+
+type Serie = { name: string, data: Array<Point> };
+
+type Event = { className?: string, name: string, date: Date };
+
+type Scale = Function;
+
+type Props = {
+ basisCurve?: boolean,
+ events?: Array<Event>,
+ eventSize?: number,
+ formatYTick: number => string,
+ formatValue: number => string,
+ height: number,
+ width: number,
+ leakPeriodDate: Date,
+ padding: Array<number>,
+ series: Array<Serie>
+};
+
+export default class AdvancedTimeline extends React.PureComponent {
+ props: Props;
+
+ static defaultProps = {
+ eventSize: 8,
+ padding: [10, 10, 10, 10]
+ };
+
+ 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') {
+ return this.getRatingScale(availableHeight);
+ } else if (this.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>) =>
+ scaleTime().domain(extent(flatData, d => d.x)).range([0, availableWidth]).clamp(true);
+
+ 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));
+ return {
+ xScale: this.getXScale(availableWidth, flatData),
+ yScale: this.getYScale(availableHeight, flatData)
+ };
+ };
+
+ 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}`;
+ };
+
+ renderHorizontalGrid = (xScale: Scale, yScale: Scale) => {
+ const hasTicks = typeof yScale.ticks === 'function';
+ const ticks = hasTicks ? yScale.ticks(4) : yScale.domain();
+
+ if (!ticks.length) {
+ ticks.push(yScale.domain()[1]);
+ }
+
+ return (
+ <g>
+ {ticks.map(tick => (
+ <g key={tick}>
+ <text
+ className="line-chart-tick line-chart-tick-x"
+ dx="-1em"
+ dy="0.3em"
+ textAnchor="end"
+ x={xScale.range()[0]}
+ y={yScale(tick)}>
+ {this.props.formatYTick(tick)}
+ </text>
+ <line
+ className="line-chart-grid"
+ x1={xScale.range()[0]}
+ x2={xScale.range()[1]}
+ y1={yScale(tick)}
+ y2={yScale(tick)}
+ />
+ </g>
+ ))}
+ </g>
+ );
+ };
+
+ renderTicks = (xScale: Scale, yScale: Scale) => {
+ const format = xScale.tickFormat(7);
+ const ticks = xScale.ticks(7);
+ const y = yScale.range()[0];
+ return (
+ <g>
+ {ticks.slice(0, -1).map((tick, index) => {
+ const nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
+ const x = (xScale(tick) + xScale(nextTick)) / 2;
+ return (
+ <text key={index} className="line-chart-tick" x={x} y={y} dy="2em">
+ {format(tick)}
+ </text>
+ );
+ })}
+ </g>
+ );
+ };
+
+ renderLeak = (xScale: Scale, yScale: Scale) => {
+ if (!this.props.leakPeriodDate) {
+ return null;
+ }
+ const yScaleRange = yScale.range();
+ return (
+ <rect
+ x={xScale(this.props.leakPeriodDate)}
+ y={yScaleRange[yScaleRange.length - 1]}
+ width={xScale.range()[1] - xScale(this.props.leakPeriodDate)}
+ height={yScaleRange[0] - yScaleRange[yScaleRange.length - 1]}
+ fill="#fbf3d5"
+ />
+ );
+ };
+
+ renderLines = (xScale: Scale, yScale: Scale) => {
+ const line = d3Line().x(d => xScale(d.x)).y(d => yScale(d.y));
+ if (this.props.basisCurve) {
+ line.curve(curveBasis);
+ }
+ return (
+ <g>
+ {this.props.series.map((serie, idx) => (
+ <path
+ key={`${idx}-${serie.name}`}
+ className={classNames('line-chart-path', 'line-chart-path-' + idx)}
+ d={line(serie.data)}
+ />
+ ))}
+ </g>
+ );
+ };
+
+ renderEvents = (xScale: Scale, yScale: Scale) => {
+ const { events, eventSize } = this.props;
+ if (!events || !eventSize) {
+ return null;
+ }
+
+ const offset = eventSize / 2;
+ return (
+ <g>
+ {events.map((event, idx) => (
+ <path
+ d={this.getEventMarker(eventSize)}
+ className={classNames('line-chart-event', event.className)}
+ key={`${idx}-${event.date.getTime()}`}
+ transform={`translate(${xScale(event.date) - offset}, ${yScale.range()[0] - offset})`}
+ />
+ ))}
+ </g>
+ );
+ };
+
+ render() {
+ if (!this.props.width || !this.props.height) {
+ return <div />;
+ }
+
+ const { xScale, yScale } = this.getScales();
+ return (
+ <svg className="line-chart" width={this.props.width} height={this.props.height}>
+ <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+ {this.renderLeak(xScale, yScale)}
+ {this.renderHorizontalGrid(xScale, yScale)}
+ {this.renderTicks(xScale, yScale)}
+ {this.renderLines(xScale, yScale)}
+ {this.renderEvents(xScale, yScale)}
+ </g>
+ </svg>
+ );
+ }
+}