]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19608 New UI for the Zoom widget of the Activity graph
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 20 Jun 2023 15:18:10 +0000 (17:18 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 26 Jun 2023 20:03:54 +0000 (20:03 +0000)
13 files changed:
server/sonar-web/design-system/src/components/__tests__/__snapshots__/HotspotRating-test.tsx.snap
server/sonar-web/design-system/src/components/__tests__/__snapshots__/LineFinding-test.tsx.snap
server/sonar-web/design-system/src/components/icons/DraggableIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/Icon.tsx
server/sonar-web/design-system/src/components/icons/index.ts
server/sonar-web/src/main/js/apps/overview/branches/__tests__/ActivityPanel-it.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsTooltips.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentCoverage.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentDuplication.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentEvents.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentIssues.tsx
server/sonar-web/src/main/js/components/charts/ZoomTimeLine.css [deleted file]
server/sonar-web/src/main/js/components/charts/ZoomTimeLine.tsx

index 454aec9ac9b3512e73d8da59cf469b8e4a9e165c..741beaababd34821f5df22d5598b9ea106b68686 100644 (file)
@@ -5,12 +5,12 @@ exports[`should render HotspotRating with HIGH rating 1`] = `
   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"
 >
@@ -32,12 +32,12 @@ exports[`should render HotspotRating with LOW rating 1`] = `
   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"
 >
@@ -62,12 +62,12 @@ exports[`should render HotspotRating with MEDIUM rating 1`] = `
   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"
 >
@@ -92,12 +92,12 @@ exports[`should render HotspotRating with default LOW rating 1`] = `
   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"
 >
index 9607fc62ac608f50b4f97354762359fa76284fe1..f68f4f6dec9b4dc8e15ea479adc23f7539fa0bf5 100644 (file)
@@ -123,12 +123,12 @@ exports[`should render correctly when issueType is provided 1`] = `
       <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"
       >
diff --git a/server/sonar-web/design-system/src/components/icons/DraggableIcon.tsx b/server/sonar-web/design-system/src/components/icons/DraggableIcon.tsx
new file mode 100644 (file)
index 0000000..54eef49
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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;
+`;
index 80e0e64be8850804dd81797b84a0875e4d8eebe1..b0527ae437081818de2db75fe14119bcdc4df2e3 100644 (file)
@@ -39,6 +39,12 @@ export interface IconProps extends Omit<Props, 'children'> {
   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,
@@ -54,7 +60,7 @@ export function CustomIcon(props: Props) {
       aria-label={ariaLabel}
       className={className}
       fill="none"
-      height={theme('height.icon')}
+      height={convertRemToPixel(theme('height.icon'))}
       role="img"
       style={{
         clipRule: 'evenodd',
@@ -67,7 +73,7 @@ export function CustomIcon(props: Props) {
       }}
       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}
index f6a20a74a7c3c29838f48c014c629d8f173e6f9a..1c00421fac7e88b20634285cbdea8ce5c38159f0 100644 (file)
@@ -31,6 +31,7 @@ export { CodeSmellIcon } from './CodeSmellIcon';
 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';
index 9be1f3a82fc7aa126f1a8cc1347d7e44ecf154a9..ee55acff3ac0ea216cccc1c0d781fc45a1b02b5a 100644 (file)
@@ -49,10 +49,19 @@ function renderActivityPanel(props: Partial<ActivityPanelProps> = {}) {
   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: {
index 552e050973e49332514d7f40a0c3c57c784d56ea..5f90bd2464da4952b99375e31f594cfa63f32dc7 100644 (file)
@@ -128,31 +128,33 @@ export class GraphsTooltipsClass extends React.PureComponent<Props> {
             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>
index 1752b2c128ef0494e90609d6e10c595a7e6e9939..2212f49e4e04d412d706d505bff1c4ceb50d2232 100644 (file)
@@ -20,7 +20,7 @@
 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 {
@@ -33,13 +33,13 @@ export default function GraphsTooltipsContentCoverage(props: GraphsTooltipsConte
   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}>
@@ -50,7 +50,7 @@ export default function GraphsTooltipsContentCoverage(props: GraphsTooltipsConte
       {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>
@@ -58,11 +58,11 @@ export default function GraphsTooltipsContentCoverage(props: GraphsTooltipsConte
       {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>
+    </>
   );
 }
index d04dcb45842658f4fd38f9c53185816e7b9efd48..55677538a5a0db880a38176c4bf45b56897e5c5d 100644 (file)
@@ -20,7 +20,7 @@
 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 {
@@ -36,7 +36,7 @@ export default function GraphsTooltipsContentDuplication(
   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;
@@ -44,7 +44,7 @@ export default function GraphsTooltipsContentDuplication(
     return null;
   }
   return (
-    <tbody>
+    <>
       {addSeparator && (
         <tr>
           <td className="activity-graph-tooltip-separator" colSpan={3}>
@@ -54,10 +54,10 @@ export default function GraphsTooltipsContentDuplication(
       )}
       <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>
+    </>
   );
 }
index d5462a4f6348b2073a3a8f99d5eea324dba20fad..b05222c4a9279a60879686c170c31d4ffac0d3c5 100644 (file)
@@ -28,7 +28,7 @@ interface Props {
 
 export default function GraphsTooltipsContentEvents({ addSeparator, events }: Props) {
   return (
-    <tbody>
+    <>
       <tr className="activity-graph-tooltip-line">
         <td colSpan={3}>
           {events.map((event) => (
@@ -45,6 +45,6 @@ export default function GraphsTooltipsContentEvents({ addSeparator, events }: Pr
           </td>
         </tr>
       )}
-    </tbody>
+    </>
   );
 }
index 094f49d4954572769cbf7770e1d5272cdd7a04ad..61ed99e34c4f554ed4412dc94bee92f5ba2b0673 100644 (file)
@@ -44,7 +44,7 @@ export default function GraphsTooltipsContentIssues(props: GraphsTooltipsContent
   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;
   }
 
diff --git a/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.css b/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.css
deleted file mode 100644 (file)
index d3585b5..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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;
-}
index 4adbf91952b438a4e1345e71825305cefca9cd32..9462ee13b5b3632affaa66551cec04d877457046 100644 (file)
  * 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;
@@ -40,15 +38,12 @@ export interface PropsWithoutTheme {
   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;
@@ -57,7 +52,7 @@ interface State {
 
 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],
   };
@@ -181,8 +176,7 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
 
   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]}
@@ -191,30 +185,8 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
     );
   };
 
-  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;
@@ -223,8 +195,7 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
     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)}
@@ -234,7 +205,7 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
   };
 
   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))
@@ -248,14 +219,7 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
     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>
     );
@@ -275,11 +239,7 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
     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>
     );
@@ -311,13 +271,19 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
       )}
       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>
   );
 
@@ -337,14 +303,13 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
       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]}
@@ -359,8 +324,7 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
             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}
@@ -392,7 +356,7 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { padding, showXTicks = true } = this.props as PropsWithDefaults;
+    const { padding } = this.props as PropsWithDefaults;
 
     if (!this.props.width || !this.props.height) {
       return <div />;
@@ -401,11 +365,10 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
     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])}
@@ -416,4 +379,48 @@ export class ZoomTimeLineClass extends React.PureComponent<Props, State> {
   }
 }
 
-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')};
+`;