Repository: DistrictDataLabs/yellowbrick

Issue:
Change elbow plot fit time to use color palette instead of hardcoded "g"
https://github.com/DistrictDataLabs/yellowbrick/blob/2d4e23648b33ad42e2e95b559e35ca6de2db5bb6/yellowbrick/cluster/elbow.py#L406

Would be nice if the fit time line, as well as the elblow line - (black/dark-grey? not sure where/if this is hardcoded) could be changed to use the global color palette instead of hardcoded values.

I'd like to be able to use the matplotlib palette 'dark_background', which makes it impossible to see the black elbow line:

<img width="656" alt="yellowbrick_elbow_black" src="https://user-images.githubusercontent.com/29611875/108923485-c2278180-75ed-11eb-9c6f-90b052c69f43.png">

Thanks!
Change elbow plot fit time to use color palette instead of hardcoded "g"
https://github.com/DistrictDataLabs/yellowbrick/blob/2d4e23648b33ad42e2e95b559e35ca6de2db5bb6/yellowbrick/cluster/elbow.py#L406

Would be nice if the fit time line, as well as the elblow line - (black/dark-grey? not sure where/if this is hardcoded) could be changed to use the global color palette instead of hardcoded values.

I'd like to be able to use the matplotlib palette 'dark_background', which makes it impossible to see the black elbow line:

<img width="656" alt="yellowbrick_elbow_black" src="https://user-images.githubusercontent.com/29611875/108923485-c2278180-75ed-11eb-9c6f-90b052c69f43.png">

Thanks!


Discussion:

Resolving patch:
diff --git a/yellowbrick/cluster/elbow.py b/yellowbrick/cluster/elbow.py
--- a/yellowbrick/cluster/elbow.py
+++ b/yellowbrick/cluster/elbow.py
@@ -43,6 +43,16 @@
 __all__ = ["KElbowVisualizer", "KElbow", "distortion_score", "kelbow_visualizer"]
 
 
+# Custom colors; note that LINE_COLOR is imported above
+TIMING_COLOR = 'C1'  # Color of timing axis, tick, label, and line
+METRIC_COLOR = 'C0'  # Color of metric axis, tick, label, and line
+
+# Keys for the color dictionary
+CTIMING = "timing"
+CMETRIC = "metric"
+CVLINE  = "vline"
+
+
 ##########################################################################
 ## Metrics
 ##########################################################################
@@ -259,6 +269,13 @@ def __init__(
         self.timings = timings
         self.locate_elbow = locate_elbow
 
+        # Set the values of the colors
+        self.colors = {
+            CTIMING: TIMING_COLOR,
+            CMETRIC: METRIC_COLOR,
+            CVLINE: LINE_COLOR,
+        }
+
         # Convert K into a tuple argument if an integer
         if isinstance(k, int):
             self.k_values_ = list(range(2, k + 1))
@@ -282,6 +299,7 @@ def __init__(
 
         # Holds the values of the silhoutte scores
         self.k_scores_ = None
+
         # Set Default Elbow Value
         self.elbow_value_ = None
 
@@ -356,13 +374,13 @@ def draw(self):
         Draw the elbow curve for the specified scores and values of K.
         """
         # Plot the silhouette score against k
-        self.ax.plot(self.k_values_, self.k_scores_, marker="D")
+        self.ax.plot(self.k_values_, self.k_scores_, marker="D", c=self.metric_color)
         if self.locate_elbow is True and self.elbow_value_ is not None:
             elbow_label = "elbow at $k={}$, $score={:0.3f}$".format(
                 self.elbow_value_, self.elbow_score_
             )
             self.ax.axvline(
-                self.elbow_value_, c=LINE_COLOR, linestyle="--", label=elbow_label
+                self.elbow_value_, c=self.vline_color, linestyle="--", label=elbow_label
             )
 
         # If we're going to plot the timings, create a twinx axis
@@ -372,7 +390,7 @@ def draw(self):
                 self.k_values_,
                 self.k_timers_,
                 label="fit time",
-                c="g",
+                c=self.timing_color,
                 marker="o",
                 linestyle="--",
                 alpha=0.75,
@@ -403,9 +421,33 @@ def finalize(self):
         # Set the second y axis labels
         if self.timings:
             self.axes[1].grid(False)
-            self.axes[1].set_ylabel("fit time (seconds)", color="g")
-            self.axes[1].tick_params("y", colors="g")
-
+            self.axes[1].set_ylabel("fit time (seconds)", color=self.timing_color)
+            self.axes[1].tick_params("y", colors=self.timing_color)
+
+
+    @property
+    def metric_color(self):
+        return self.colors[CMETRIC]
+
+    @metric_color.setter
+    def metric_color(self, val):
+        self.colors[CMETRIC] = val
+    
+    @property
+    def timing_color(self):
+        return self.colors[CTIMING]
+    
+    @timing_color.setter
+    def timing_color(self, val):
+        self.colors[CTIMING] = val
+    
+    @property
+    def vline_color(self):
+        return self.colors[CVLINE]
+
+    @vline_color.setter
+    def vline_color(self, val):
+        self.colors[CVLINE] = val
 
 # alias
 KElbow = KElbowVisualizer


Test patch:
diff --git a/tests/baseline_images/test_cluster/test_elbow/test_integrated_kmeans_elbow.png b/tests/baseline_images/test_cluster/test_elbow/test_integrated_kmeans_elbow.png
Binary files a/tests/baseline_images/test_cluster/test_elbow/test_integrated_kmeans_elbow.png and b/tests/baseline_images/test_cluster/test_elbow/test_integrated_kmeans_elbow.png differ
diff --git a/tests/baseline_images/test_cluster/test_elbow/test_integrated_mini_batch_kmeans_elbow.png b/tests/baseline_images/test_cluster/test_elbow/test_integrated_mini_batch_kmeans_elbow.png
Binary files a/tests/baseline_images/test_cluster/test_elbow/test_integrated_mini_batch_kmeans_elbow.png and b/tests/baseline_images/test_cluster/test_elbow/test_integrated_mini_batch_kmeans_elbow.png differ
diff --git a/tests/baseline_images/test_cluster/test_elbow/test_quick_method.png b/tests/baseline_images/test_cluster/test_elbow/test_quick_method.png
Binary files a/tests/baseline_images/test_cluster/test_elbow/test_quick_method.png and b/tests/baseline_images/test_cluster/test_elbow/test_quick_method.png differ
diff --git a/tests/baseline_images/test_cluster/test_elbow/test_set_colors_manually.png b/tests/baseline_images/test_cluster/test_elbow/test_set_colors_manually.png
new file mode 100644
Binary files /dev/null and b/tests/baseline_images/test_cluster/test_elbow/test_set_colors_manually.png differ
diff --git a/tests/baseline_images/test_cluster/test_elbow/test_timings.png b/tests/baseline_images/test_cluster/test_elbow/test_timings.png
Binary files a/tests/baseline_images/test_cluster/test_elbow/test_timings.png and b/tests/baseline_images/test_cluster/test_elbow/test_timings.png differ
diff --git a/tests/test_cluster/test_elbow.py b/tests/test_cluster/test_elbow.py
--- a/tests/test_cluster/test_elbow.py
+++ b/tests/test_cluster/test_elbow.py
@@ -426,3 +426,28 @@ def test_quick_method_params(self):
             model, X, sample_weight=np.ones(X.shape[0]), title=custom_title
         )
         assert oz.title == custom_title
+
+    @pytest.mark.xfail(sys.platform == "win32", reason="images not close on windows")
+    def test_set_colors_manually(self):
+        """
+        Test the silhouette metric of the k-elbow visualizer
+        """
+        oz = KElbowVisualizer(
+            KMeans(random_state=0), k=5,
+        )
+
+        oz.metric_color = "r"
+        oz.timing_color = "y"
+        oz.vline_color = "c"
+        
+        # Create artificial "fit" data for testing purposes
+        oz.k_values_ = [1, 2, 3, 4, 5, 6, 7, 8]
+        oz.k_timers_ = [6.2, 8.3, 10.1, 15.8, 21.2, 27.9, 38.2, 44.9]
+        oz.k_scores_ = [.8, .7, .55, .48, .40, .38, .35, .30]
+        oz.elbow_value_ = 5
+        oz.elbow_score_ = 0.40
+        
+        # Execute drawing
+        oz.draw()
+        oz.finalize()
+        self.assert_images_similar(oz, tol=3.2)
\ No newline at end of file