aria-hidden="false"
aria-label="label"
fill="none"
- height="1rem"
+ height="16"
role="img"
style="clip-rule: evenodd; display: inline-block; fill-rule: evenodd; user-select: none; vertical-align: middle; stroke-linejoin: round; stroke-miterlimit: 1.414;"
version="1.1"
viewBox="0 0 16 16"
- width="1rem"
+ width="16"
xml:space="preserve"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
aria-hidden="false"
aria-label="label"
fill="none"
- height="1rem"
+ height="16"
role="img"
style="clip-rule: evenodd; display: inline-block; fill-rule: evenodd; user-select: none; vertical-align: middle; stroke-linejoin: round; stroke-miterlimit: 1.414;"
version="1.1"
viewBox="0 0 16 16"
- width="1rem"
+ width="16"
xml:space="preserve"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
aria-hidden="false"
aria-label="label"
fill="none"
- height="1rem"
+ height="16"
role="img"
style="clip-rule: evenodd; display: inline-block; fill-rule: evenodd; user-select: none; vertical-align: middle; stroke-linejoin: round; stroke-miterlimit: 1.414;"
version="1.1"
viewBox="0 0 16 16"
- width="1rem"
+ width="16"
xml:space="preserve"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
aria-hidden="false"
aria-label="label"
fill="none"
- height="1rem"
+ height="16"
role="img"
style="clip-rule: evenodd; display: inline-block; fill-rule: evenodd; user-select: none; vertical-align: middle; stroke-linejoin: round; stroke-miterlimit: 1.414;"
version="1.1"
viewBox="0 0 16 16"
- width="1rem"
+ width="16"
xml:space="preserve"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<svg
aria-hidden="true"
fill="none"
- height="1rem"
+ height="16"
role="img"
style="clip-rule: evenodd; display: inline-block; fill-rule: evenodd; user-select: none; vertical-align: middle; stroke-linejoin: round; stroke-miterlimit: 1.414;"
version="1.1"
viewBox="0 0 16 16"
- width="1rem"
+ width="16"
xml:space="preserve"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import { themeColor, themeContrast } from '../../helpers/theme';
+import { CustomIcon, IconProps } from './Icon';
+
+export function DraggableIcon({
+ fill = 'currentColor',
+ ...iconProps
+}: IconProps & { x: number; y: number }) {
+ const theme = useTheme();
+ const fillColor = themeColor(fill)({ theme });
+ const innerFillColor = themeContrast(fill)({ theme });
+
+ return (
+ <StyledCustomIcon {...iconProps}>
+ <circle cx="8" cy="8" fill={fillColor} r="8" />
+ <rect fill={innerFillColor} height="7" width="1" x="6" y="5" />
+ <rect fill={innerFillColor} height="7" width="1" x="9" y="5" />
+ </StyledCustomIcon>
+ );
+}
+
+const StyledCustomIcon = styled(CustomIcon)`
+ cursor: ew-resize;
+`;
width?: number;
}
+const PIXELS_IN_ONE_REM = 16;
+
+function convertRemToPixel(remString: string) {
+ return Number(remString.replace('rem', '')) * PIXELS_IN_ONE_REM;
+}
+
export function CustomIcon(props: Props) {
const {
'aria-label': ariaLabel,
aria-label={ariaLabel}
className={className}
fill="none"
- height={theme('height.icon')}
+ height={convertRemToPixel(theme('height.icon'))}
role="img"
style={{
clipRule: 'evenodd',
}}
version="1.1"
viewBox="0 0 16 16"
- width={theme('width.icon')}
+ width={convertRemToPixel(theme('width.icon'))}
xmlSpace="preserve"
xmlnsXlink="http://www.w3.org/1999/xlink"
{...iconProps}
export { CommentIcon } from './CommentIcon';
export { CopyIcon } from './CopyIcon';
export { DirectoryIcon } from './DirectoryIcon';
+export { DraggableIcon } from './DraggableIcon';
export { ExecutionFlowIcon } from './ExecutionFlowIcon';
export { FileIcon } from './FileIcon';
export { FilterIcon } from './FilterIcon';
const mockedAnalysis = [
mockAnalysis({
events: [
- mockAnalysisEvent(),
- mockAnalysisEvent({ category: ProjectAnalysisEventCategory.Version, name: 'v1.0' }),
- mockAnalysisEvent({ category: ProjectAnalysisEventCategory.Other, name: 'Other' }),
+ mockAnalysisEvent({ key: '1' }),
mockAnalysisEvent({
+ key: '2',
+ category: ProjectAnalysisEventCategory.Version,
+ name: 'v1.0',
+ }),
+ mockAnalysisEvent({
+ key: '3',
+ category: ProjectAnalysisEventCategory.Other,
+ name: 'Other',
+ }),
+ mockAnalysisEvent({
+ key: '4',
category: ApplicationAnalysisEventCategory.DefinitionChange,
name: 'DefinitionChange',
definitionChange: {
className="width-100"
style={{ color: themeColor('dropdownMenuSubTitle')({ theme }) }}
>
- {addSeparator && (
- <tr>
- <td className="activity-graph-tooltip-separator" colSpan={3}>
- <hr />
- </td>
- </tr>
- )}
- {events?.length > 0 && (
- <GraphsTooltipsContentEvents addSeparator={addSeparator} events={events} />
- )}
- <tbody>{tooltipContent}</tbody>
- {graph === GraphType.coverage && (
- <GraphsTooltipsContentCoverage
- addSeparator={addSeparator}
- measuresHistory={measuresHistory}
- tooltipIdx={tooltipIdx}
- />
- )}
- {graph === GraphType.duplications && (
- <GraphsTooltipsContentDuplication
- addSeparator={addSeparator}
- measuresHistory={measuresHistory}
- tooltipIdx={tooltipIdx}
- />
- )}
+ <tbody>
+ {addSeparator && (
+ <tr>
+ <td className="activity-graph-tooltip-separator" colSpan={3}>
+ <hr />
+ </td>
+ </tr>
+ )}
+ {events?.length > 0 && (
+ <GraphsTooltipsContentEvents addSeparator={addSeparator} events={events} />
+ )}
+ {tooltipContent}
+ {graph === GraphType.coverage && (
+ <GraphsTooltipsContentCoverage
+ addSeparator={addSeparator}
+ measuresHistory={measuresHistory}
+ tooltipIdx={tooltipIdx}
+ />
+ )}
+ {graph === GraphType.duplications && (
+ <GraphsTooltipsContentDuplication
+ addSeparator={addSeparator}
+ measuresHistory={measuresHistory}
+ tooltipIdx={tooltipIdx}
+ />
+ )}
+ </tbody>
</table>
</div>
</Popup>
import * as React from 'react';
import { translate } from '../../helpers/l10n';
import { formatMeasure } from '../../helpers/measures';
-import { MetricKey } from '../../types/metrics';
+import { MetricKey, MetricType } from '../../types/metrics';
import { MeasureHistory } from '../../types/project-activity';
export interface GraphsTooltipsContentCoverageProps {
const { addSeparator, measuresHistory, tooltipIdx } = props;
const uncovered = measuresHistory.find((measure) => measure.metric === MetricKey.uncovered_lines);
const coverage = measuresHistory.find((measure) => measure.metric === MetricKey.coverage);
- if (!uncovered || !uncovered.history[tooltipIdx] || !coverage || !coverage.history[tooltipIdx]) {
+ if (!uncovered?.history[tooltipIdx] || !coverage?.history[tooltipIdx]) {
return null;
}
const uncoveredValue = uncovered.history[tooltipIdx].value;
const coverageValue = coverage.history[tooltipIdx].value;
return (
- <tbody>
+ <>
{addSeparator && (
<tr>
<td className="activity-graph-tooltip-separator" colSpan={3}>
{uncoveredValue && (
<tr className="activity-graph-tooltip-line">
<td className="activity-graph-tooltip-value text-right spacer-right thin" colSpan={2}>
- {formatMeasure(uncoveredValue, 'SHORT_INT')}
+ {formatMeasure(uncoveredValue, MetricType.ShortInteger)}
</td>
<td>{translate('metric.uncovered_lines.name')}</td>
</tr>
{coverageValue && (
<tr className="activity-graph-tooltip-line">
<td className="activity-graph-tooltip-value text-right spacer-right thin" colSpan={2}>
- {formatMeasure(coverageValue, 'PERCENT')}
+ {formatMeasure(coverageValue, MetricType.Percent)}
</td>
<td>{translate('metric.coverage.name')}</td>
</tr>
)}
- </tbody>
+ </>
);
}
import * as React from 'react';
import { translate } from '../../helpers/l10n';
import { formatMeasure } from '../../helpers/measures';
-import { MetricKey } from '../../types/metrics';
+import { MetricKey, MetricType } from '../../types/metrics';
import { MeasureHistory } from '../../types/project-activity';
export interface GraphsTooltipsContentDuplicationProps {
const duplicationDensity = measuresHistory.find(
(measure) => measure.metric === MetricKey.duplicated_lines_density
);
- if (!duplicationDensity || !duplicationDensity.history[tooltipIdx]) {
+ if (!duplicationDensity?.history[tooltipIdx]) {
return null;
}
const duplicationDensityValue = duplicationDensity.history[tooltipIdx].value;
return null;
}
return (
- <tbody>
+ <>
{addSeparator && (
<tr>
<td className="activity-graph-tooltip-separator" colSpan={3}>
)}
<tr className="activity-graph-tooltip-line">
<td className="activity-graph-tooltip-value text-right spacer-right thin" colSpan={2}>
- {formatMeasure(duplicationDensityValue, 'PERCENT')}
+ {formatMeasure(duplicationDensityValue, MetricType.Percent)}
</td>
<td>{translate('metric.duplicated_lines_density.name')}</td>
</tr>
- </tbody>
+ </>
);
}
export default function GraphsTooltipsContentEvents({ addSeparator, events }: Props) {
return (
- <tbody>
+ <>
<tr className="activity-graph-tooltip-line">
<td colSpan={3}>
{events.map((event) => (
</td>
</tr>
)}
- </tbody>
+ </>
);
}
const { index, measuresHistory, name, tooltipIdx, translatedName, value } = props;
const rating = measuresHistory.find((measure) => measure.metric === METRIC_RATING[name]);
- if (!rating || !rating.history[tooltipIdx]) {
+ if (!rating?.history[tooltipIdx]) {
return null;
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.
- */
-.chart-zoom-tick {
- fill: var(--secondFontColor);
- font-size: 10px;
- text-anchor: middle;
- user-select: none;
-}
-
-.chart-zoom .zoom-overlay {
- fill: none;
- stroke: none;
- cursor: crosshair;
- pointer-events: all;
-}
-
-.chart-zoom .zoom-selection {
- fill: var(--secondFontColor);
- fill-opacity: 0.2;
- stroke: var(--secondFontColor);
- shape-rendering: crispEdges;
- cursor: move;
-}
-
-.chart-zoom .zoom-selection-handle {
- cursor: ew-resize;
- fill-opacity: 0;
- stroke: none;
-}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import classNames from 'classnames';
+import styled from '@emotion/styled';
import { extent, max } from 'd3-array';
import { ScaleTime, scaleLinear, scalePoint, scaleTime } from 'd3-scale';
import { area, curveBasis, line as d3Line } from 'd3-shape';
-import { ThemeProp, themeColor, withTheme } from 'design-system';
+import { CSSColor, DraggableIcon, themeColor } from 'design-system';
import { flatten, sortBy, throttle } from 'lodash';
import * as React from 'react';
import Draggable, { DraggableBounds, DraggableCore, DraggableData } from 'react-draggable';
import { MetricType } from '../../types/metrics';
import { Chart } from '../../types/types';
-import './LineChart.css';
-import './ZoomTimeLine.css';
-export interface PropsWithoutTheme {
+export interface Props {
basisCurve?: boolean;
endDate?: Date;
height: number;
padding?: number[];
series: Chart.Serie[];
showAreas?: boolean;
- showXTicks?: boolean;
startDate?: Date;
updateZoom: (start?: Date, endDate?: Date) => void;
width: number;
}
-export type Props = PropsWithoutTheme & ThemeProp;
-
-export type PropsWithDefaults = Props & typeof ZoomTimeLineClass.defaultProps;
+export type PropsWithDefaults = Props & typeof ZoomTimeLine.defaultProps;
interface State {
overlayLeftPos?: number;
type XScale = ScaleTime<number, number>;
-export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
+export class ZoomTimeLine extends React.PureComponent<Props, State> {
static defaultProps = {
padding: [0, 0, 18, 0],
};
renderBaseLine = (xScale: XScale, yScale: { range: () => number[] }) => {
return (
- <line
- className="line-chart-grid"
+ <StyledBaseLine
x1={xScale.range()[0]}
x2={xScale.range()[1]}
y1={yScale.range()[0]}
);
};
- renderTicks = (xScale: XScale, yScale: { range: () => number[] }) => {
- 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 (
- // eslint-disable-next-line react/no-array-index-key
- <text className="chart-zoom-tick" dy="1.3em" key={index} x={x} y={y}>
- {format(tick)}
- </text>
- );
- })}
- </g>
- );
- };
-
renderNewCode = (xScale: XScale, yScale: { range: () => number[] }) => {
- const { leakPeriodDate, theme } = this.props;
+ const { leakPeriodDate } = this.props;
if (!leakPeriodDate) {
return null;
const yRange = yScale.range();
return (
- <rect
- fill={themeColor('newCodeLegend')({ theme })}
+ <StyledNewCodeLegend
height={yRange[0] - yRange[yRange.length - 1]}
width={xScale.range()[1] - xScale(leakPeriodDate)}
x={xScale(leakPeriodDate)}
};
renderLines = (xScale: XScale, yScale: (y: string | number | undefined) => number) => {
- const { series, theme } = this.props;
+ const { series } = this.props;
const lineGenerator = d3Line<Chart.Point>()
.defined((d) => Boolean(d.y || d.y === 0))
return (
<g>
{series.map((serie, idx) => (
- <path
- className={classNames('line-chart-path', `line-chart-path-${idx}`)}
- d={lineGenerator(serie.data) ?? undefined}
- key={serie.name}
- stroke={themeColor(`graphLineColor.${idx}` as Parameters<typeof themeColor>[0])({
- theme,
- })}
- />
+ <StyledPath index={idx} d={lineGenerator(serie.data) ?? undefined} key={serie.name} />
))}
</g>
);
return (
<g>
{this.props.series.map((serie, idx) => (
- <path
- className={classNames('line-chart-area', 'line-chart-area-' + idx)}
- d={areaGenerator(serie.data) || undefined}
- key={serie.name}
- />
+ <StyledArea index={idx} d={areaGenerator(serie.data) ?? undefined} key={serie.name} />
))}
</g>
);
)}
position={{ x: options.xPos, y: 0 }}
>
- <rect
- className="zoom-selection-handle"
- height={options.yDim[0] - options.yDim[1] + 1}
- width={6}
- x={-3}
- y={options.yDim[1]}
- />
+ <g>
+ <ZoomHighlightHandle
+ height={options.yDim[0] - options.yDim[1] + 1}
+ width={2}
+ x={options.direction === 'right' ? 0 : -2}
+ y={options.yDim[1]}
+ />
+ <DraggableIcon
+ fill="graphZoomHandleColor"
+ x={options.direction === 'right' ? -7 : -9}
+ y={16}
+ />
+ </g>
</Draggable>
);
this.state.newZoomStart === endX;
return (
- <g className="chart-zoom">
+ <g>
<DraggableCore
onDrag={this.handleNewZoomDrag(xScale, xDim)}
onStart={this.handleNewZoomDragStart(xDim)}
onStop={this.handleNewZoomDragEnd(xScale, xDim)}
>
- <rect
- className="zoom-overlay"
+ <ZoomOverlay
height={yDim[0] - yDim[1]}
width={xDim[1] - xDim[0]}
x={xDim[0]}
onStop={this.handleSelectionDrag(xScale, zoomBoxWidth, xDim)}
position={{ x: xArray[0], y: 0 }}
>
- <rect
- className="zoom-selection"
+ <ZoomHighlight
height={yDim[0] - yDim[1] + 1}
onDoubleClick={this.handleDoubleClick(xScale, xDim)}
width={zoomBoxWidth}
};
render() {
- const { padding, showXTicks = true } = this.props as PropsWithDefaults;
+ const { padding } = this.props as PropsWithDefaults;
if (!this.props.width || !this.props.height) {
return <div />;
const { xScale, yScale } = this.getScales();
return (
- <svg className="line-chart " height={this.props.height} width={this.props.width}>
+ <svg height={this.props.height} width={this.props.width}>
<g transform={`translate(${padding[3]}, ${padding[0] + 2})`}>
{this.renderNewCode(xScale, yScale as Parameters<typeof this.renderNewCode>[1])}
{this.renderBaseLine(xScale, yScale as Parameters<typeof this.renderBaseLine>[1])}
- {showXTicks && this.renderTicks(xScale, yScale as Parameters<typeof this.renderTicks>[1])}
{this.props.showAreas &&
this.renderAreas(xScale, yScale as Parameters<typeof this.renderAreas>[1])}
{this.renderLines(xScale, yScale as Parameters<typeof this.renderLines>[1])}
}
}
-export const ZoomTimeLine = withTheme<PropsWithoutTheme>(ZoomTimeLineClass);
+const ZoomHighlight = styled.rect`
+ cursor: move;
+ fill: ${themeColor('graphZoomBackgroundColor')};
+ stroke: ${themeColor('graphZoomBorderColor')};
+ fill-opacity: 0.2;
+ shape-rendering: crispEdges;
+`;
+
+const ZoomHighlightHandle = styled.rect`
+ cursor: ew-resize;
+ fill-opacity: 1;
+ fill: ${themeColor('graphZoomHandleColor')};
+ stroke: none;
+`;
+
+const ZoomOverlay = styled.rect`
+ cursor: crosshair;
+ pointer-events: all;
+ fill: none;
+ stroke: none;
+`;
+
+const AREA_OPACITY = 0.15;
+
+const StyledArea = styled.path<{ index: number }>`
+ clip-path: url(#chart-clip);
+ fill: ${({ index }) => themeColor(`graphLineColor.${index}` as CSSColor, AREA_OPACITY)};
+ stroke-width: 0;
+`;
+
+const StyledPath = styled.path<{ index: number }>`
+ clip-path: url(#chart-clip);
+ fill: none;
+ stroke: ${({ index }) => themeColor(`graphLineColor.${index}` as CSSColor)};
+ stroke-width: 2px;
+`;
+
+const StyledNewCodeLegend = styled.rect`
+ fill: ${themeColor('newCodeLegend')};
+`;
+
+const StyledBaseLine = styled('line')`
+ shape-rendering: crispedges;
+ stroke: ${themeColor('graphGridColor')};
+`;