Skip to content

Reduce copy/pasted code between chart types

There's a lot of copy/pasted code across various chart types. For instance, there are only ~30 lines which differ between src/components/charts/line/line.vue and src/components/charts/area/area.vue, whereas these files are nearly 400 lines long. In other words, more than 90% of these files are the same.

Diff between src/components/charts/line/line.vue and src/components/charts/area/area.vue
diff --git a/src/components/charts/area/area.vue b/src/components/charts/line/line.vue
index 98fc06524..080074d91 100644
--- a/src/components/charts/area/area.vue
+++ b/src/components/charts/line/line.vue
@@ -1,12 +1,14 @@
 <!-- eslint-disable vue/multi-word-component-names -->
 <script>
 /**
- * Area charts as of %12.10 support annotations.
+ * Line charts as of %12.10 support annotations.
  * Annotations is composed of a dotted line and an arrow
  * at the bottom. The dotted line is constructed
- * with markLine and arrows with markPoint.
+ * with markLine and arrows with markPoint. Most of this
+ * logic is in GitLab and should be eventually moved to
+ * GitLab UI https://gitlab.com/gitlab-org/gitlab/-/issues/213390.
  *
- * Similar to how custom tooltips are displayed when area chart
+ * Similar to how custom tooltips are displayed when line chart
  * is hovered, a tooltip should be displayed when the annotation
  * arrow is hovered. This component adds event listeners
  * to figure out if mouse is hovered on charts to show tooltips.
@@ -17,13 +19,14 @@
  */
 
 import merge from 'lodash/merge';
+import { gray200 } from '../../../../scss_to_js/scss_variables'; // eslint-disable-line import/no-unresolved
 import {
   defaultChartOptions,
   grid,
   getThresholdConfig,
   generateAnnotationSeries,
   dataZoomAdjustments,
-  defaultAreaOpacity,
+  symbolSize,
   mergeSeriesToOptions,
   mergeAnnotationAxisToOptions,
   lineStyle,
@@ -81,11 +84,6 @@ export default {
       required: false,
       default: true,
     },
-    formatAnnotationsTooltipText: {
-      type: Function,
-      required: false,
-      default: null,
-    },
     formatTooltipText: {
       type: Function,
       required: false,
@@ -119,6 +117,11 @@ export default {
         return [LEGEND_LAYOUT_INLINE, LEGEND_LAYOUT_TABLE].indexOf(layout) !== -1;
       },
     },
+    showLegend: {
+      type: Boolean,
+      required: false,
+      default: true,
+    },
     /**
      * Sets the chart's height in pixels. Set to `"auto"` to use the height of the container.
      */
@@ -157,15 +160,11 @@ export default {
     series() {
       const dataSeries = this.data.map((series, index) => {
         const defaultColor = colorFromDefaultPalette(index);
-        const getColor = (type) =>
-          series[type] && series[type].color ? series[type].color : defaultColor;
+        const getColor = (type) => series[type]?.color ?? defaultColor;
+
         return merge(
           {
-            areaStyle: {
-              opacity: defaultAreaOpacity,
-              color: getColor('areaStyle'),
-            },
-            showSymbol: false,
+            showSymbol: true,
             lineStyle: {
               color: getColor('lineStyle'),
             },
@@ -173,6 +172,7 @@ export default {
               color: getColor('itemStyle'),
             },
           },
+          symbolSize,
           lineStyle,
           series,
           getThresholdConfig(this.thresholds)
@@ -189,21 +189,20 @@ export default {
       return generateAnnotationSeries(this.annotations);
     },
     options() {
-      const defaultAreaChartOptions = {
+      const defaultLineChartOptions = {
         xAxis: {
           axisPointer: {
             show: true,
-            lineStyle: {
-              type: 'solid',
-            },
             label: {
               formatter: this.onLabelChange,
             },
           },
-        },
-        yAxis: {
           axisTick: {
-            show: false,
+            alignWithLabel: true,
+            show: true,
+            lineStyle: {
+              color: gray200,
+            },
           },
         },
         legend: {
@@ -213,12 +212,12 @@ export default {
       const mergedOptions = merge(
         {},
         defaultChartOptions,
-        defaultAreaChartOptions,
+        defaultLineChartOptions,
         this.option,
         dataZoomAdjustments(this.option.dataZoom)
       );
       // All chart options can be merged but series
-      // needs to be handled specially.
+      // needs to be handled specially
       return mergeSeriesToOptions(
         mergeAnnotationAxisToOptions(mergedOptions, this.hasAnnotations),
         this.series
@@ -243,6 +242,9 @@ export default {
     legendStyle() {
       return { paddingLeft: `${grid.left}px` };
     },
+    hasLegend() {
+      return this.compiledOptions && this.showLegend;
+    },
     seriesInfo() {
       return this.compiledOptions.series.reduce((acc, series, index) => {
         if (series.type === 'line') {
@@ -264,8 +266,8 @@ export default {
     this.chart.getDom().removeEventListener('mousemove', this.debouncedShowHideTooltip);
     this.chart.getDom().removeEventListener('mouseout', this.debouncedShowHideTooltip);
 
-    this.chart.off('mouseout', this.hideAnnotationsTooltip);
-    this.chart.off('mouseover', this.onChartMouseOver);
+    this.chart.off('mouseout', this.onChartDataPointMouseOut);
+    this.chart.off('mouseover', this.onChartDataPointMouseOver);
   },
   methods: {
     defaultFormatTooltipText(params) {
@@ -381,16 +383,16 @@ export default {
     >
       <template #title>
         <slot v-if="formatTooltipText" name="tooltip-title"></slot>
-        <template v-else>
+        <div v-else>
           {{ dataTooltipTitle }}
           <template v-if="options.xAxis.name">({{ options.xAxis.name }})</template>
-        </template>
+        </div>
       </template>
       <slot v-if="formatTooltipText" name="tooltip-content"></slot>
       <tooltip-default-format v-else :tooltip-content="dataTooltipContent" />
     </chart-tooltip>
     <chart-legend
-      v-if="compiledOptions"
+      v-if="hasLegend"
       :chart="chart"
       :style="legendStyle"
       :series-info="seriesInfo"

Here's a diff-charts.fish that spits out all the pairwise diffs of the various chart types. Using that, it's clear that several of the chart components share a lot of code. The <template> often differs by just a small amount, usually in the tooltip body.

This duplication can lead to maintenance issues, like bugs that affect one chart type but not another. An example of this is gitlab-org/gitlab-ui!2908 (merged).

Edited by Mark Florian