plotext-plus 1.0.7__py3-none-any.whl → 1.0.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
plotext_plus/__init__.py CHANGED
@@ -26,6 +26,6 @@ from ._api import (
26
26
  Chart, Legend, PlotextAPI, api,
27
27
  ScatterChart, LineChart, BarChart, HistogramChart,
28
28
  CandlestickChart, HeatmapChart, MatrixChart, StemChart,
29
- create_chart, quick_scatter, quick_line, quick_bar,
29
+ create_chart, quick_scatter, quick_line, quick_bar, quick_pie, quick_donut,
30
30
  enable_banners, log_info, log_success, log_warning, log_error
31
31
  )
plotext_plus/_api.py CHANGED
@@ -84,6 +84,22 @@ class Chart:
84
84
  })
85
85
  return self
86
86
 
87
+ def pie(self, labels, values, colors=None, radius=None, show_values=True, show_percentages=True, show_values_on_slices=False, donut=False, remaining_color=None):
88
+ """Add pie chart data"""
89
+ self._data.append({
90
+ 'type': 'pie',
91
+ 'labels': labels,
92
+ 'values': values,
93
+ 'colors': colors,
94
+ 'radius': radius,
95
+ 'show_values': show_values,
96
+ 'show_percentages': show_percentages,
97
+ 'show_values_on_slices': show_values_on_slices,
98
+ 'donut': donut,
99
+ 'remaining_color': remaining_color
100
+ })
101
+ return self
102
+
87
103
  def title(self, title):
88
104
  """Set chart title"""
89
105
  self._config['title'] = title
@@ -172,6 +188,18 @@ class Chart:
172
188
  _core.horizontal_bar(data_item['labels'], data_item['values'], color=data_item['color'])
173
189
  else:
174
190
  _core.bar(data_item['labels'], data_item['values'], color=data_item['color'])
191
+ elif data_item['type'] == 'pie':
192
+ _core.pie(
193
+ data_item['labels'],
194
+ data_item['values'],
195
+ colors=data_item['colors'],
196
+ radius=data_item['radius'],
197
+ show_values=data_item['show_values'],
198
+ show_percentages=data_item['show_percentages'],
199
+ show_values_on_slices=data_item['show_values_on_slices'],
200
+ donut=data_item.get('donut', False),
201
+ remaining_color=data_item.get('remaining_color', None)
202
+ )
175
203
  elif data_item['type'] == 'histogram':
176
204
  _core.hist(data_item['data'], bins=data_item['bins'], color=data_item['color'])
177
205
 
@@ -763,6 +791,25 @@ class PlotextAPI:
763
791
  chart.show()
764
792
  return chart
765
793
 
794
+ @staticmethod
795
+ def quick_pie(labels, values, colors=None, title=None, use_banners=False, banner_title=None,
796
+ show_values=True, show_percentages=True, show_values_on_slices=False, donut=False, remaining_color=None):
797
+ """Quickly create and display a pie chart"""
798
+ chart = Chart(use_banners, banner_title)
799
+ chart.pie(labels, values, colors=colors, show_values=show_values, show_percentages=show_percentages, show_values_on_slices=show_values_on_slices, donut=donut, remaining_color=remaining_color)
800
+ if title:
801
+ chart.title(title)
802
+ chart.show()
803
+ return chart
804
+
805
+ @staticmethod
806
+ def quick_donut(labels, values, colors=None, title=None, use_banners=False, banner_title=None,
807
+ show_values=True, show_percentages=True, show_values_on_slices=False, remaining_color=None):
808
+ """Quickly create and display a doughnut chart"""
809
+ return api.quick_pie(labels, values, colors=colors, title=title, use_banners=use_banners,
810
+ banner_title=banner_title, show_values=show_values,
811
+ show_percentages=show_percentages, show_values_on_slices=show_values_on_slices, donut=True, remaining_color=remaining_color)
812
+
766
813
  @staticmethod
767
814
  def enable_banners(enabled=True, default_title="Plotext Chart"):
768
815
  """Globally enable or disable banner mode"""
@@ -797,6 +844,8 @@ create_chart = api.create_chart
797
844
  quick_scatter = api.quick_scatter
798
845
  quick_line = api.quick_line
799
846
  quick_bar = api.quick_bar
847
+ quick_pie = api.quick_pie
848
+ quick_donut = api.quick_donut
800
849
  enable_banners = api.enable_banners
801
850
  log_info = api.log_info
802
851
  log_success = api.log_success
@@ -820,6 +869,8 @@ __all__ = [
820
869
  'quick_scatter',
821
870
  'quick_line',
822
871
  'quick_bar',
872
+ 'quick_pie',
873
+ 'quick_donut',
823
874
  'enable_banners',
824
875
  'log_info',
825
876
  'log_success',
plotext_plus/_core.py CHANGED
@@ -278,6 +278,27 @@ def matrix_plot(matrix, marker = None, style = None, fast = False):
278
278
  _figure._active.matrix_plot(matrix, marker = marker, style = style, fast = fast)
279
279
  _figure.show() if _figure._interactive else None
280
280
 
281
+ def pie(labels, values, colors = None, radius = None, show_values = True, show_percentages = True, title = None, show_values_on_slices = False, donut = False, remaining_color = None):
282
+ """
283
+ Create a pie chart representation using terminal characters.
284
+
285
+ Args:
286
+ labels (list): Labels for each slice
287
+ values (list): Values for each slice
288
+ colors (list, optional): Colors for each slice
289
+ radius (int, optional): Radius of the pie chart
290
+ show_values (bool): Whether to show actual values in legend
291
+ show_percentages (bool): Whether to show percentages in legend
292
+ title (str, optional): Title for the chart
293
+ show_values_on_slices (bool): Whether to show values/percentages on pie slices themselves
294
+ donut (bool): If True, creates a doughnut chart with hollow center (inner radius = 1/3 outer radius)
295
+ remaining_color (str): If specified, colors the remaining slice with this color instead of leaving it as spaces
296
+ """
297
+ _figure._active.pie(labels, values, colors = colors, radius = radius,
298
+ show_values = show_values, show_percentages = show_percentages,
299
+ title = title, show_values_on_slices = show_values_on_slices, donut = donut, remaining_color = remaining_color)
300
+ _figure.show() if _figure._interactive else None
301
+
281
302
  def heatmap(dataframe, color = None, style=None):
282
303
  _figure._active.heatmap(dataframe, color = color, style = style)
283
304
  _figure.show() if _figure._interactive else None
plotext_plus/_dict.py CHANGED
@@ -196,7 +196,7 @@ themes['solarized_dark'] = [(0, 43, 54), (5, 5, 5), (147, 161, 161), no_color, s
196
196
  # Solarized Light - light version of solarized
197
197
  sequence = [(42, 161, 152), (133, 153, 0), (181, 137, 0), (203, 75, 22), (211, 54, 130)] # same colors as dark
198
198
  sequence += [el for el in color_sequence if el not in sequence]
199
- themes['solarized_light'] = [(253, 246, 227), (101, 123, 131), (88, 110, 117), no_color, sequence]
199
+ themes['solarized_light'] = [(253, 246, 227), (101, 123, 131), (64, 64, 64), no_color, sequence]
200
200
 
201
201
  # === CHUK-TERM COMPATIBLE THEMES ===
202
202
  # Import and integrate chuk-term compatible themes
plotext_plus/_figure.py CHANGED
@@ -277,6 +277,9 @@ class _figure_class():
277
277
  def matrix_plot(self, matrix, marker = None, style = None, fast = False):
278
278
  self.monitor.draw_matrix(matrix, marker = marker, style = style, fast = fast) if self._no_plots else [[self._get_subplot(row, col).matrix_plot(matrix, marker = marker, style = style, fast = fast) for col in self._Cols] for row in self._Rows]
279
279
 
280
+ def pie(self, labels, values, colors = None, radius = None, show_values = True, show_percentages = True, title = None, show_values_on_slices = False, donut = False, remaining_color = None):
281
+ self.monitor.draw_pie(labels, values, colors = colors, radius = radius, show_values = show_values, show_percentages = show_percentages, title = title, show_values_on_slices = show_values_on_slices, donut = donut, remaining_color = remaining_color) if self._no_plots else [[self._get_subplot(row, col).pie(labels, values, colors = colors, radius = radius, show_values = show_values, show_percentages = show_percentages, title = title, show_values_on_slices = show_values_on_slices, donut = donut, remaining_color = remaining_color) for col in self._Cols] for row in self._Rows]
282
+
280
283
  def heatmap(self, dataframe, color = None, style = None):
281
284
  self.monitor.draw_heatmap(dataframe, color = color, style = style) if self._no_plots else [[self._get_subplot(row, col).heatmap(dataframe, color = color, style = style) for col in self._Cols] for row in self._Rows]
282
285
 
plotext_plus/_monitor.py CHANGED
@@ -775,6 +775,409 @@ class monitor_class(build_class):
775
775
  self.matrix.canvas = '\n'.join([''.join(row) for row in matrix])
776
776
  self.fast_plot = True
777
777
 
778
+ def draw_pie(self, labels, values, colors = None, radius = None, show_values = True, show_percentages = True, title = None, show_values_on_slices = False, donut = False, remaining_color = None):
779
+ """
780
+ Draw a pie chart using filled colored segments and a legend.
781
+
782
+ Args:
783
+ donut (bool): If True, creates a doughnut chart with hollow center (inner radius = 1/3 outer radius)
784
+ remaining_color (str): If specified, colors the remaining slice with this color instead of leaving it as spaces
785
+ """
786
+ import math
787
+
788
+ # Input validation
789
+ if len(labels) != len(values):
790
+ raise ValueError("Labels and values must have the same length")
791
+
792
+ # Calculate total and percentages
793
+ total = sum(values)
794
+ if total == 0:
795
+ raise ValueError("Total of values cannot be zero")
796
+
797
+ percentages = [(value / total) * 100 for value in values]
798
+
799
+ # Default colors if not provided
800
+ if colors is None:
801
+ color_cycle = ['red', 'blue', 'green', 'orange', 'magenta', 'cyan', 'white']
802
+ colors = [color_cycle[i % len(color_cycle)] for i in range(len(labels))]
803
+
804
+ # Default radius - calculate based on available plot space
805
+ if radius is None:
806
+ # Get the actual plot area dimensions
807
+ plot_width, plot_height = self.size
808
+
809
+ # Set radius to half of the smaller dimension minus 4 for border margin
810
+ radius = (min(plot_width, plot_height) - 4) / 2.0
811
+ radius = max(radius, 3) # Ensure minimum radius of 3
812
+
813
+ # Center the pie chart
814
+ center_x = 0
815
+ center_y = 0
816
+
817
+ # Terminal characters have an aspect ratio of approximately 1.5:1 (height:width)
818
+ # To make circles appear circular, we need to adjust the x-axis scaling
819
+ aspect_ratio = 1.5
820
+
821
+ # Remove axes - pie charts don't have them
822
+ self.set_xfrequency(0)
823
+ self.set_yfrequency(0)
824
+ self.set_axes_color('default')
825
+ self.set_canvas_color('default')
826
+
827
+ # Collect all points for each segment, then draw each segment in one call
828
+ # Use efficient scanning - just slightly beyond the actual pie radius
829
+ # For doughnuts, use denser scanning to ensure solid ring
830
+ if donut:
831
+ scan_radius_x = int(radius * aspect_ratio * 1.5 + 3)
832
+ scan_radius_y = int(radius * 1.5 + 3)
833
+ else:
834
+ scan_radius_x = int(radius * aspect_ratio * 1.2 + 2)
835
+ scan_radius_y = int(radius * 1.2 + 2)
836
+
837
+ # Pre-calculate cumulative angles for segment boundaries
838
+ segment_boundaries = []
839
+ current_cumulative = 0
840
+ for value in values:
841
+ slice_angle = (value / total) * 2 * math.pi
842
+ segment_boundaries.append((current_cumulative, current_cumulative + slice_angle))
843
+ current_cumulative += slice_angle
844
+
845
+ # Collect all points for each segment using sets to avoid duplicates
846
+ segment_points = [set() for _ in range(len(labels))] # One set per segment
847
+
848
+ # Use FLOOD FILL approach - systematically fill every position in concentric circles
849
+ # This ensures no gaps by filling from center outward
850
+ for y_offset in range(-scan_radius_y, scan_radius_y + 1):
851
+ for x_offset in range(-scan_radius_x, scan_radius_x + 1):
852
+ # Calculate distance from center with aspect ratio correction
853
+ # Since terminal chars are ~1.5x taller than wide, compress x coordinate
854
+ adjusted_x = x_offset / aspect_ratio
855
+ distance = math.sqrt(adjusted_x * adjusted_x + y_offset * y_offset)
856
+
857
+ # For doughnut inner boundary, use elliptical check to create circular appearance
858
+ # The inner boundary should be elliptical in terminal coordinates to appear circular
859
+ inner_radius = radius / 3.0 if donut else 0
860
+
861
+ # Check if point is outside the inner ellipse (for circular appearance)
862
+ if donut:
863
+ # Create elliptical inner boundary: x^2/a^2 + y^2/b^2 > r^2
864
+ # where a = inner_radius * aspect_ratio, b = inner_radius
865
+ ellipse_x_term = (x_offset * x_offset) / (inner_radius * aspect_ratio * inner_radius * aspect_ratio)
866
+ ellipse_y_term = (y_offset * y_offset) / (inner_radius * inner_radius)
867
+ ellipse_value = ellipse_x_term + ellipse_y_term
868
+ outside_inner = ellipse_value > 1.0
869
+
870
+ else:
871
+ outside_inner = True
872
+
873
+ # Use exact radius to stay within plot boundaries
874
+ threshold = radius
875
+
876
+ if distance <= threshold and outside_inner:
877
+ # Calculate angle for this position using adjusted coordinates
878
+ angle = math.atan2(y_offset, adjusted_x)
879
+ if angle < 0:
880
+ angle += 2 * math.pi
881
+
882
+ # Find which segment this position belongs to using robust angle detection
883
+ segment_idx = 0
884
+ found_segment = False
885
+ epsilon = 0.02 # Even larger epsilon for maximum boundary coverage
886
+
887
+ for i, (start_angle, end_angle) in enumerate(segment_boundaries):
888
+ # Handle wraparound case for segments that cross 0 degrees
889
+ if end_angle > 2 * math.pi:
890
+ wrap_end = end_angle - 2 * math.pi
891
+ if angle >= start_angle - epsilon or angle <= wrap_end + epsilon:
892
+ segment_idx = i
893
+ found_segment = True
894
+ break
895
+ else:
896
+ # Use very generous boundary detection
897
+ # For the last segment, use <= to include the boundary
898
+ if i == len(segment_boundaries) - 1:
899
+ if start_angle - epsilon <= angle <= end_angle + epsilon:
900
+ segment_idx = i
901
+ found_segment = True
902
+ break
903
+ else:
904
+ if start_angle - epsilon <= angle < end_angle + epsilon:
905
+ segment_idx = i
906
+ found_segment = True
907
+ break
908
+
909
+ # If no segment found (due to floating point precision), assign based on closest angle
910
+ if not found_segment:
911
+ # Find the segment with the smallest angle distance
912
+ min_distance = float('inf')
913
+ for i, (start_angle, end_angle) in enumerate(segment_boundaries):
914
+ mid_angle = (start_angle + end_angle) / 2
915
+ # Handle wraparound for mid angle calculation
916
+ if end_angle > 2 * math.pi:
917
+ mid_angle = start_angle + ((end_angle - start_angle) / 2)
918
+ if mid_angle > 2 * math.pi:
919
+ mid_angle -= 2 * math.pi
920
+
921
+ # Calculate angular distance (accounting for circular nature)
922
+ angle_diff = abs(angle - mid_angle)
923
+ if angle_diff > math.pi:
924
+ angle_diff = 2 * math.pi - angle_diff
925
+
926
+ if angle_diff < min_distance:
927
+ min_distance = angle_diff
928
+ segment_idx = i
929
+
930
+ # Add this exact character position to the appropriate segment
931
+ char_x = center_x + x_offset
932
+ char_y = center_y + y_offset
933
+ segment_points[segment_idx].add((char_x, char_y))
934
+
935
+ # SECOND PASS: Fill any potential gaps by adding adjacent positions
936
+ # This ensures complete coverage by adding neighboring positions to existing points
937
+ additional_points = [set() for _ in range(len(labels))]
938
+ for segment_idx, points in enumerate(segment_points):
939
+ for (x, y) in points:
940
+ # Add neighboring positions to ensure no gaps
941
+ for dx in [-1, 0, 1]:
942
+ for dy in [-1, 0, 1]:
943
+ neighbor_x = x + dx
944
+ neighbor_y = y + dy
945
+
946
+ # Check if this neighbor is within the circular area using same logic as main pass
947
+ x_offset = neighbor_x - center_x
948
+ y_offset = neighbor_y - center_y
949
+ adjusted_x = x_offset / aspect_ratio
950
+ neighbor_distance = math.sqrt(adjusted_x * adjusted_x + y_offset * y_offset)
951
+
952
+ # Use same boundary checks as main algorithm
953
+ inner_radius = radius / 3.0 if donut else 0
954
+
955
+ # Check if point is outside the inner ellipse (for circular appearance)
956
+ if donut:
957
+ # Use same elliptical inner boundary as main pass
958
+ ellipse_x_term = (x_offset * x_offset) / (inner_radius * aspect_ratio * inner_radius * aspect_ratio)
959
+ ellipse_y_term = (y_offset * y_offset) / (inner_radius * inner_radius)
960
+ outside_inner = ellipse_x_term + ellipse_y_term > 1.0
961
+ else:
962
+ outside_inner = True
963
+
964
+ # Use exact radius to stay within plot boundaries (same as main pass)
965
+ threshold = radius
966
+
967
+ if neighbor_distance <= threshold and outside_inner:
968
+ additional_points[segment_idx].add((neighbor_x, neighbor_y))
969
+
970
+ # Merge additional points with main points
971
+ for segment_idx in range(len(labels)):
972
+ segment_points[segment_idx].update(additional_points[segment_idx])
973
+
974
+ # Draw each segment using a different approach - draw filled shapes row by row
975
+ # This ensures complete filling without gaps
976
+ for segment_idx, (points, color) in enumerate(zip(segment_points, colors)):
977
+ if points: # Only draw if segment has points
978
+ # Handle remaining_color for single-value pie charts
979
+ if color == "default":
980
+ if remaining_color is not None:
981
+ # Use the specified remaining_color instead of default
982
+ color = remaining_color
983
+ else:
984
+ # Skip drawing - leave as spaces (current behavior)
985
+ continue
986
+
987
+ points_list = list(points)
988
+
989
+ # Group points by y-coordinate to draw horizontal filled lines
990
+ y_groups = {}
991
+ for x, y in points_list:
992
+ if y not in y_groups:
993
+ y_groups[y] = []
994
+ y_groups[y].append(x)
995
+
996
+ # For doughnut charts, use smart filling that avoids the hollow center
997
+ # For regular pie charts, use full horizontal line filling
998
+ if donut:
999
+ # Smart filling for doughnut charts: fill gaps within ring segments but avoid center
1000
+ for y_coord, x_coords in y_groups.items():
1001
+ if x_coords:
1002
+ x_coords.sort() # Sort x coordinates
1003
+
1004
+ # Find continuous segments, avoiding the center gap
1005
+ fill_x_coords = []
1006
+ x_step = 0.5
1007
+
1008
+ # Determine if this y_coord passes through the hollow center
1009
+ y_offset = y_coord - center_y
1010
+ center_x_range = []
1011
+
1012
+ # Calculate the x-range that should be hollow at this y-coordinate
1013
+ if abs(y_offset) < inner_radius:
1014
+ # This y-line passes through the hollow center
1015
+ # Calculate x-bounds of the elliptical hollow area
1016
+ ellipse_y_term = (y_offset * y_offset) / (inner_radius * inner_radius)
1017
+ if ellipse_y_term < 1.0:
1018
+ ellipse_x_term_needed = 1.0 - ellipse_y_term
1019
+ max_x_offset = math.sqrt(ellipse_x_term_needed) * inner_radius * aspect_ratio
1020
+ center_x_min = center_x - max_x_offset
1021
+ center_x_max = center_x + max_x_offset
1022
+ center_x_range = [center_x_min, center_x_max]
1023
+
1024
+ # Fill between consecutive x-coordinates, but avoid the center region
1025
+ i = 0
1026
+ while i < len(x_coords):
1027
+ segment_start = x_coords[i]
1028
+
1029
+ # Find the end of this continuous segment
1030
+ j = i
1031
+ while j < len(x_coords) - 1:
1032
+ gap = x_coords[j + 1] - x_coords[j]
1033
+ # If there's a large gap, this segment ends
1034
+ if gap > 2.0: # Allow small gaps but break on large ones
1035
+ break
1036
+ j += 1
1037
+
1038
+ segment_end = x_coords[j]
1039
+
1040
+ # Fill this segment, but avoid the center region
1041
+ if center_x_range:
1042
+ # Split segment around the hollow center
1043
+ center_min, center_max = center_x_range
1044
+
1045
+ # Fill left part (before center)
1046
+ if segment_start < center_min:
1047
+ left_end = min(segment_end, center_min)
1048
+ current_x = segment_start
1049
+ while current_x <= left_end:
1050
+ fill_x_coords.append(current_x)
1051
+ current_x += x_step
1052
+
1053
+ # Fill right part (after center)
1054
+ if segment_end > center_max:
1055
+ right_start = max(segment_start, center_max)
1056
+ current_x = right_start
1057
+ while current_x <= segment_end:
1058
+ fill_x_coords.append(current_x)
1059
+ current_x += x_step
1060
+ else:
1061
+ # No center interference, fill entire segment
1062
+ current_x = segment_start
1063
+ while current_x <= segment_end:
1064
+ fill_x_coords.append(current_x)
1065
+ current_x += x_step
1066
+
1067
+ i = j + 1
1068
+
1069
+ # Draw the filled segments
1070
+ if fill_x_coords:
1071
+ fill_y_coords = [y_coord] * len(fill_x_coords)
1072
+ self.draw(fill_x_coords, fill_y_coords, marker='sd', color=color)
1073
+ else:
1074
+ # For regular pie charts, use full horizontal line filling
1075
+ for y_coord, x_coords in y_groups.items():
1076
+ if x_coords:
1077
+ x_coords.sort() # Sort x coordinates
1078
+ x_min, x_max = min(x_coords), max(x_coords)
1079
+
1080
+ # Create a continuous range of x coordinates to fill the gap
1081
+ if x_max > x_min:
1082
+ # Draw filled horizontal line from x_min to x_max
1083
+ fill_x_coords = []
1084
+ x_step = 0.5 # Smaller step for better coverage
1085
+ current_x = x_min
1086
+ while current_x <= x_max:
1087
+ fill_x_coords.append(current_x)
1088
+ current_x += x_step
1089
+ fill_y_coords = [y_coord] * len(fill_x_coords)
1090
+ self.draw(fill_x_coords, fill_y_coords, marker='sd', color=color)
1091
+ else:
1092
+ # Single point
1093
+ self.draw([x_min], [y_coord], marker='sd', color=color)
1094
+
1095
+ # Reset cumulative_angle for label drawing
1096
+ cumulative_angle = 0
1097
+ for i, (label, value, percentage, color) in enumerate(zip(labels, values, percentages, colors)):
1098
+ slice_angle = (value / total) * 2 * math.pi
1099
+
1100
+ # Add value labels on the pie slice (only if show_values_on_slices is True)
1101
+ if show_values_on_slices and (show_values or show_percentages):
1102
+ # Calculate middle angle of the slice for label placement
1103
+ middle_angle = cumulative_angle + slice_angle / 2
1104
+ # Position label at 70% of radius for better visibility
1105
+ label_radius = radius * 0.7
1106
+ label_x = center_x + (label_radius * math.cos(middle_angle)) * aspect_ratio
1107
+ label_y = center_y + label_radius * math.sin(middle_angle)
1108
+
1109
+ # Build label text for the slice
1110
+ slice_label = ""
1111
+ if show_values and show_percentages:
1112
+ slice_label = f"{value}\n({percentage:.1f}%)"
1113
+ elif show_values:
1114
+ slice_label = str(value)
1115
+ elif show_percentages:
1116
+ slice_label = f"{percentage:.1f}%"
1117
+
1118
+ # Draw the label on the slice
1119
+ self.draw_text(slice_label, label_x, label_y, color='white', alignment='center')
1120
+
1121
+ cumulative_angle += slice_angle
1122
+
1123
+ # Extend the plot area to accommodate legend (calculate before filtering)
1124
+ max_text_length = max(len(f"{label}: {value} ({percentage:.1f}%)")
1125
+ for label, value, percentage in zip(labels, values, percentages)
1126
+ if label.lower() != "remaining") if any(label.lower() != "remaining" for label in labels) else 20
1127
+
1128
+ # Set plot limits to include legend area (adjust x for aspect ratio)
1129
+ x_radius = radius * aspect_ratio
1130
+ self.set_xlim(-x_radius - 1, x_radius + max_text_length + 2)
1131
+ self.set_ylim(-radius - 1, radius + 1)
1132
+
1133
+ # Create legend positioned in the bottom right corner of the chart
1134
+ legend_start_x = x_radius + 1.5
1135
+ legend_start_y = -radius + len(labels) * 1.0 - 0.5
1136
+
1137
+ # Filter out "Remaining" labels and default colors for single-value pie charts
1138
+ legend_items = []
1139
+ for i, (label, value, percentage, color) in enumerate(zip(labels, values, percentages, colors)):
1140
+ # Handle remaining_color logic for legend
1141
+ if color == "default":
1142
+ if remaining_color is not None:
1143
+ # Show "Remaining" in legend when remaining_color is specified
1144
+ legend_items.append((label, value, percentage, remaining_color))
1145
+ # Skip if no remaining_color (leave as spaces)
1146
+ else:
1147
+ # Always show non-default colors
1148
+ legend_items.append((label, value, percentage, color))
1149
+
1150
+ # Adjust legend positioning for filtered items
1151
+ legend_start_y = -radius + len(legend_items) * 1.0 - 0.5
1152
+
1153
+ for i, (label, value, percentage, color) in enumerate(legend_items):
1154
+ legend_x = legend_start_x
1155
+ legend_y = legend_start_y - i * 1.2 # Space between legend items
1156
+
1157
+ # Draw colored square for legend matching pie chart blocks
1158
+ self.draw([legend_x], [legend_y], marker='sd', color=color)
1159
+
1160
+ # Build legend text with colored block prefix
1161
+ block_char = "█" # Solid block character
1162
+ legend_text = f"{block_char} {label}"
1163
+ if show_values and show_percentages:
1164
+ legend_text += f": {value} ({percentage:.1f}%)"
1165
+ elif show_values:
1166
+ legend_text += f": {value}"
1167
+ elif show_percentages:
1168
+ legend_text += f": {percentage:.1f}%"
1169
+
1170
+ # Use draw_text for the legend with the same color as the segment
1171
+ self.draw_text(legend_text, legend_x, legend_y, color=color)
1172
+
1173
+ # Set title if provided
1174
+ if title:
1175
+ self.set_title(title)
1176
+
1177
+ # Remove axis labels since pie charts don't need them
1178
+ self.set_xlabel('')
1179
+ self.set_ylabel('')
1180
+
778
1181
  def draw_heatmap(self, dataframe, color = None, style=None):
779
1182
  color = self.default.cmatrix_color if color is None else self.check_color(color)
780
1183
  style = self.default.cmatrix_style if style is None else self.check_style(style)
plotext_plus/_themes.py CHANGED
@@ -53,6 +53,7 @@ rgb_colors = {
53
53
  'term_black': (0, 0, 0),
54
54
  'term_white': (255, 255, 255),
55
55
  'term_gray': (128, 128, 128),
56
+ 'dark_gray': (64, 64, 64), # Dark gray for better readability
56
57
  }
57
58
 
58
59
  def create_chuk_term_themes():
@@ -148,7 +149,7 @@ def create_chuk_term_themes():
148
149
  rgb_colors['sol_blue'], rgb_colors['sol_magenta'], rgb_colors['sol_red']]
149
150
  sequence += [el for el in color_sequence if el not in sequence]
150
151
  themes['solarized_light'] = [rgb_colors['sol_base3'], rgb_colors['sol_base01'],
151
- rgb_colors['sol_base00'], no_color, sequence]
152
+ rgb_colors['dark_gray'], no_color, sequence]
152
153
 
153
154
  # Matrix theme (enhanced version)
154
155
  sequence = [(0, 255, 65), (0, 200, 50), (0, 150, 35), (0, 100, 20)]
plotext_plus/charts.py CHANGED
@@ -20,7 +20,7 @@ from ._api import (
20
20
  CandlestickChart, HeatmapChart, MatrixChart, StemChart,
21
21
 
22
22
  # Convenience functions
23
- create_chart, quick_scatter, quick_line, quick_bar,
23
+ create_chart, quick_scatter, quick_line, quick_bar, quick_pie, quick_donut,
24
24
 
25
25
  # Banner and logging utilities
26
26
  enable_banners, log_info, log_success, log_warning, log_error,
@@ -35,7 +35,7 @@ __all__ = [
35
35
  'CandlestickChart', 'HeatmapChart', 'MatrixChart', 'StemChart',
36
36
 
37
37
  # Convenience functions
38
- 'create_chart', 'quick_scatter', 'quick_line', 'quick_bar',
38
+ 'create_chart', 'quick_scatter', 'quick_line', 'quick_bar', 'quick_pie', 'quick_donut',
39
39
 
40
40
  # Utilities
41
41
  'enable_banners', 'log_info', 'log_success', 'log_warning', 'log_error',
@@ -71,11 +71,15 @@ async def scatter_plot(x: List[Union[int, float]], y: List[Union[int, float]],
71
71
  Returns:
72
72
  The rendered plot as text
73
73
  """
74
+ # Convert string inputs to float
75
+ x_numeric = [float(val) if isinstance(val, str) else val for val in x]
76
+ y_numeric = [float(val) if isinstance(val, str) else val for val in y]
77
+
74
78
  plotting.clear_figure()
75
79
  if title:
76
80
  plotting.title(title)
77
81
 
78
- _, output = _capture_plot_output(plotting.scatter, x, y, marker=marker, color=color)
82
+ _, output = _capture_plot_output(plotting.scatter, x_numeric, y_numeric, marker=marker, color=color)
79
83
  _, show_output = _capture_plot_output(plotting.show)
80
84
 
81
85
  return output + show_output
@@ -95,14 +99,41 @@ async def line_plot(x: List[Union[int, float]], y: List[Union[int, float]],
95
99
  Returns:
96
100
  The rendered plot as text
97
101
  """
98
- plotting.clear_figure()
99
- if title:
100
- plotting.title(title)
102
+ import sys
103
+ print(f"DEBUG: line_plot called with x={x}, y={y}, title={title}", file=sys.stderr)
101
104
 
102
- _, output = _capture_plot_output(plotting.plot, x, y, color=color)
103
- _, show_output = _capture_plot_output(plotting.show)
105
+ # Convert string inputs to float
106
+ try:
107
+ x_numeric = [float(val) if isinstance(val, str) else val for val in x]
108
+ y_numeric = [float(val) if isinstance(val, str) else val for val in y]
109
+ print(f"DEBUG: Converted to x_numeric={x_numeric}, y_numeric={y_numeric}", file=sys.stderr)
110
+ except Exception as e:
111
+ print(f"DEBUG: Error converting inputs: {e}", file=sys.stderr)
112
+ raise
104
113
 
105
- return output + show_output
114
+ try:
115
+ plotting.clear_figure()
116
+ print("DEBUG: Cleared figure", file=sys.stderr)
117
+
118
+ if title:
119
+ plotting.title(title)
120
+ print(f"DEBUG: Set title: {title}", file=sys.stderr)
121
+
122
+ _, output = _capture_plot_output(plotting.plot, x_numeric, y_numeric, color=color)
123
+ print(f"DEBUG: Generated plot output, length: {len(output)}", file=sys.stderr)
124
+
125
+ _, show_output = _capture_plot_output(plotting.show)
126
+ print(f"DEBUG: Generated show output, length: {len(show_output)}", file=sys.stderr)
127
+
128
+ result = output + show_output
129
+ print(f"DEBUG: Returning result, total length: {len(result)}", file=sys.stderr)
130
+ return result
131
+
132
+ except Exception as e:
133
+ print(f"DEBUG: Error during plotting: {e}", file=sys.stderr)
134
+ import traceback
135
+ traceback.print_exc(file=sys.stderr)
136
+ raise
106
137
 
107
138
 
108
139
  @tool
@@ -119,11 +150,14 @@ async def bar_chart(labels: List[str], values: List[Union[int, float]],
119
150
  Returns:
120
151
  The rendered plot as text
121
152
  """
153
+ # Convert string inputs to float
154
+ values_numeric = [float(val) if isinstance(val, str) else val for val in values]
155
+
122
156
  plotting.clear_figure()
123
157
  if title:
124
158
  plotting.title(title)
125
159
 
126
- _, output = _capture_plot_output(plotting.bar, labels, values, color=color)
160
+ _, output = _capture_plot_output(plotting.bar, labels, values_numeric, color=color)
127
161
  _, show_output = _capture_plot_output(plotting.show)
128
162
 
129
163
  return output + show_output
@@ -150,6 +184,53 @@ async def matrix_plot(data: List[List[Union[int, float]]], title: Optional[str]
150
184
  return output + show_output
151
185
 
152
186
 
187
+ @tool
188
+ async def image_plot(image_path: str, title: Optional[str] = None,
189
+ marker: Optional[str] = None, style: Optional[str] = None,
190
+ fast: bool = False, grayscale: bool = False) -> str:
191
+ """Display an image in the terminal using ASCII art.
192
+
193
+ Args:
194
+ image_path: Path to the image file to display
195
+ title: Plot title (optional)
196
+ marker: Custom marker for image rendering (optional)
197
+ style: Style for image rendering (optional)
198
+ fast: Enable fast rendering mode for better performance (optional)
199
+ grayscale: Render image in grayscale (optional)
200
+
201
+ Returns:
202
+ The rendered image plot as text
203
+ """
204
+ plotting.clear_figure()
205
+ if title:
206
+ plotting.title(title)
207
+
208
+ _, output = _capture_plot_output(plotting.image_plot, image_path,
209
+ marker=marker, style=style,
210
+ fast=fast, grayscale=grayscale)
211
+ _, show_output = _capture_plot_output(plotting.show)
212
+
213
+ return output + show_output
214
+
215
+
216
+ @tool
217
+ async def play_gif(gif_path: str) -> str:
218
+ """Play a GIF animation in the terminal.
219
+
220
+ Args:
221
+ gif_path: Path to the GIF file to play
222
+
223
+ Returns:
224
+ Confirmation message (GIF plays automatically)
225
+ """
226
+ plotting.clear_figure()
227
+
228
+ # play_gif handles its own output and doesn't need show()
229
+ plotting.play_gif(gif_path)
230
+
231
+ return f"Playing GIF: {gif_path}"
232
+
233
+
153
234
  # Chart Class Tools
154
235
  @tool
155
236
  async def quick_scatter(x: List[Union[int, float]], y: List[Union[int, float]],
@@ -165,7 +246,11 @@ async def quick_scatter(x: List[Union[int, float]], y: List[Union[int, float]],
165
246
  Returns:
166
247
  The rendered chart as text
167
248
  """
168
- _, output = _capture_plot_output(charts.quick_scatter, x, y, title=title, theme=theme_name)
249
+ # Convert string inputs to float
250
+ x_numeric = [float(val) if isinstance(val, str) else val for val in x]
251
+ y_numeric = [float(val) if isinstance(val, str) else val for val in y]
252
+
253
+ _, output = _capture_plot_output(charts.quick_scatter, x_numeric, y_numeric, title=title, theme=theme_name)
169
254
  return output
170
255
 
171
256
 
@@ -183,7 +268,11 @@ async def quick_line(x: List[Union[int, float]], y: List[Union[int, float]],
183
268
  Returns:
184
269
  The rendered chart as text
185
270
  """
186
- _, output = _capture_plot_output(charts.quick_line, x, y, title=title, theme=theme_name)
271
+ # Convert string inputs to float
272
+ x_numeric = [float(val) if isinstance(val, str) else val for val in x]
273
+ y_numeric = [float(val) if isinstance(val, str) else val for val in y]
274
+
275
+ _, output = _capture_plot_output(charts.quick_line, x_numeric, y_numeric, title=title, theme=theme_name)
187
276
  return output
188
277
 
189
278
 
@@ -201,7 +290,75 @@ async def quick_bar(labels: List[str], values: List[Union[int, float]],
201
290
  Returns:
202
291
  The rendered chart as text
203
292
  """
204
- _, output = _capture_plot_output(charts.quick_bar, labels, values, title=title, theme=theme_name)
293
+ # Convert string inputs to float
294
+ values_numeric = [float(val) if isinstance(val, str) else val for val in values]
295
+
296
+ _, output = _capture_plot_output(charts.quick_bar, labels, values_numeric, title=title, theme=theme_name)
297
+ return output
298
+
299
+
300
+ @tool
301
+ async def quick_pie(labels: List[str], values: List[Union[int, float]],
302
+ colors: Optional[List[str]] = None, title: Optional[str] = None,
303
+ show_values: bool = True, show_percentages: bool = True,
304
+ show_values_on_slices: bool = False, donut: bool = False,
305
+ remaining_color: Optional[str] = None) -> str:
306
+ """Create a quick pie chart using the chart classes API.
307
+
308
+ Args:
309
+ labels: List of pie segment labels
310
+ values: List of pie segment values
311
+ colors: List of colors for segments (optional)
312
+ title: Chart title (optional)
313
+ show_values: Show values in legend (optional, default True)
314
+ show_percentages: Show percentages in legend (optional, default True)
315
+ show_values_on_slices: Show values directly on pie slices (optional, default False)
316
+ donut: Create doughnut chart with hollow center (optional, default False)
317
+ remaining_color: Color for remaining slice in single-value charts (optional)
318
+
319
+ Returns:
320
+ The rendered pie chart as text
321
+ """
322
+ # Convert string inputs to float
323
+ values_numeric = [float(val) if isinstance(val, str) else val for val in values]
324
+
325
+ _, output = _capture_plot_output(charts.quick_pie, labels, values_numeric, colors=colors,
326
+ title=title, show_values=show_values,
327
+ show_percentages=show_percentages,
328
+ show_values_on_slices=show_values_on_slices,
329
+ donut=donut, remaining_color=remaining_color)
330
+ return output
331
+
332
+
333
+ @tool
334
+ async def quick_donut(labels: List[str], values: List[Union[int, float]],
335
+ colors: Optional[List[str]] = None, title: Optional[str] = None,
336
+ show_values: bool = True, show_percentages: bool = True,
337
+ show_values_on_slices: bool = False,
338
+ remaining_color: Optional[str] = None) -> str:
339
+ """Create a quick doughnut chart (pie chart with hollow center) using the chart classes API.
340
+
341
+ Args:
342
+ labels: List of pie segment labels
343
+ values: List of pie segment values
344
+ colors: List of colors for segments (optional)
345
+ title: Chart title (optional)
346
+ show_values: Show values in legend (optional, default True)
347
+ show_percentages: Show percentages in legend (optional, default True)
348
+ show_values_on_slices: Show values directly on pie slices (optional, default False)
349
+ remaining_color: Color for remaining slice in single-value charts (optional)
350
+
351
+ Returns:
352
+ The rendered doughnut chart as text
353
+ """
354
+ # Convert string inputs to float
355
+ values_numeric = [float(val) if isinstance(val, str) else val for val in values]
356
+
357
+ _, output = _capture_plot_output(charts.quick_donut, labels, values_numeric, colors=colors,
358
+ title=title, show_values=show_values,
359
+ show_percentages=show_percentages,
360
+ show_values_on_slices=show_values_on_slices,
361
+ remaining_color=remaining_color)
205
362
  return output
206
363
 
207
364
 
@@ -371,6 +528,66 @@ async def get_plot_config() -> Dict[str, Any]:
371
528
  }
372
529
 
373
530
 
531
+ # Resource for tool documentation/info
532
+ @resource("info://plotext")
533
+ async def get_tool_info() -> Dict[str, Any]:
534
+ """Get comprehensive information about all available plotting tools."""
535
+ return {
536
+ "server_info": {
537
+ "name": "Plotext Plus MCP Server",
538
+ "description": "Model Context Protocol server for plotext_plus terminal plotting library",
539
+ "version": "1.0.0",
540
+ "capabilities": ["plotting", "theming", "multimedia", "charts"]
541
+ },
542
+ "plotting_tools": {
543
+ "scatter_plot": "Create scatter plots with x/y data points",
544
+ "line_plot": "Create line plots for time series and continuous data",
545
+ "bar_chart": "Create bar charts for categorical data",
546
+ "matrix_plot": "Create heatmaps from 2D matrix data",
547
+ "image_plot": "Display images in terminal using ASCII art",
548
+ "play_gif": "Play animated GIFs in the terminal"
549
+ },
550
+ "quick_chart_tools": {
551
+ "quick_scatter": "Quickly create scatter charts with theming",
552
+ "quick_line": "Quickly create line charts with theming",
553
+ "quick_bar": "Quickly create bar charts with theming",
554
+ "quick_pie": "Quickly create pie charts with custom colors, donut mode, and remaining_color options",
555
+ "quick_donut": "Quickly create doughnut charts (hollow center pie charts)"
556
+ },
557
+ "theme_tools": {
558
+ "get_available_themes": "List all available color themes",
559
+ "apply_plot_theme": "Apply a theme to plots"
560
+ },
561
+ "utility_tools": {
562
+ "get_terminal_width": "Get current terminal width",
563
+ "colorize_text": "Apply colors to text output",
564
+ "log_info": "Output informational messages",
565
+ "log_success": "Output success messages",
566
+ "log_warning": "Output warning messages",
567
+ "log_error": "Output error messages"
568
+ },
569
+ "configuration_tools": {
570
+ "set_plot_size": "Set plot dimensions",
571
+ "enable_banner_mode": "Enable/disable banner mode",
572
+ "clear_plot": "Clear current plot"
573
+ },
574
+ "supported_formats": {
575
+ "image_formats": ["PNG", "JPG", "JPEG", "BMP", "GIF (static)"],
576
+ "gif_formats": ["GIF (animated)"],
577
+ "chart_types": ["scatter", "line", "bar", "pie", "doughnut", "matrix/heatmap", "image"],
578
+ "themes": "20+ built-in themes including solarized, dracula, cyberpunk"
579
+ },
580
+ "usage_tips": {
581
+ "pie_charts": "Best for 3-7 categories, use full terminal dimensions",
582
+ "doughnut_charts": "Modern alternative to pie charts with hollow center, great for progress indicators",
583
+ "single_value_charts": "Perfect for progress/completion rates: ['Complete', 'Remaining'] with 'default' color",
584
+ "images": "Use fast=True for better performance with large images",
585
+ "themes": "Apply themes before creating plots for consistent styling",
586
+ "banners": "Enable banner mode for professional-looking outputs"
587
+ }
588
+ }
589
+
590
+
374
591
  # MCP Prompts for common plotting scenarios
375
592
  @prompt("basic_scatter")
376
593
  async def basic_scatter_prompt() -> str:
@@ -494,11 +711,295 @@ async def complete_workflow_prompt() -> str:
494
711
  6. Generate a summary report"""
495
712
 
496
713
 
714
+ @prompt("image_display")
715
+ async def image_display_prompt() -> str:
716
+ """Image plotting example"""
717
+ return """Display an image in the terminal:
718
+ 1. First download the test cat image using utilities.download() with the test_image_url
719
+ 2. Display it using image_plot with title 'Test Image Display'
720
+ 3. Try both normal and grayscale versions
721
+ 4. Clean up by deleting the file afterward"""
722
+
723
+
724
+ @prompt("gif_animation")
725
+ async def gif_animation_prompt() -> str:
726
+ """GIF animation example"""
727
+ return """Play a GIF animation in terminal:
728
+ 1. Download the test Homer Simpson GIF using utilities.download() with test_gif_url
729
+ 2. Play the GIF animation using play_gif
730
+ 3. Clean up the file afterward
731
+ Note: The GIF will play automatically in the terminal"""
732
+
733
+
734
+ @prompt("image_styling")
735
+ async def image_styling_prompt() -> str:
736
+ """Custom image styling example"""
737
+ return """Experiment with image rendering styles:
738
+ 1. Display the same image with different markers (try 'CuteCat' as marker)
739
+ 2. Use 'inverted' style for visual effects
740
+ 3. Compare fast vs normal rendering modes
741
+ 4. Show both color and grayscale versions"""
742
+
743
+
744
+ @prompt("multimedia_showcase")
745
+ async def multimedia_showcase_prompt() -> str:
746
+ """Complete multimedia demonstration"""
747
+ return """Showcase multimedia capabilities:
748
+ 1. Download and display a static image with custom styling
749
+ 2. Download and play an animated GIF
750
+ 3. Set appropriate plot sizes for optimal viewing
751
+ 4. Add descriptive titles to each display
752
+ 5. Clean up all downloaded files"""
753
+
754
+
755
+ @prompt("basic_pie_chart")
756
+ async def basic_pie_chart_prompt() -> str:
757
+ """Basic pie chart example"""
758
+ return """Create a simple pie chart showing market share data:
759
+ - Categories: ['iOS', 'Android', 'Windows', 'Other']
760
+ - Values: [35, 45, 15, 5]
761
+ - Use colors: ['blue', 'green', 'orange', 'gray']
762
+ - Add title 'Mobile OS Market Share'
763
+ - Use quick_pie tool for fast creation"""
764
+
765
+
766
+ @prompt("pie_chart_styling")
767
+ async def pie_chart_styling_prompt() -> str:
768
+ """Advanced pie chart styling example"""
769
+ return """Create a styled pie chart with advanced features:
770
+ 1. Use quick_pie with show_values_on_slices=True
771
+ 2. Data: Budget categories ['Housing', 'Food', 'Transport', 'Entertainment']
772
+ 3. Values: [1200, 400, 300, 200] (monthly budget)
773
+ 4. Custom colors for each category
774
+ 5. Add meaningful title and ensure full terminal usage"""
775
+
776
+
777
+ @prompt("pie_chart_comparison")
778
+ async def pie_chart_comparison_prompt() -> str:
779
+ """Pie chart comparison example"""
780
+ return """Create multiple pie charts for comparison:
781
+ 1. Q1 Sales: ['Product A', 'Product B', 'Product C'] = [30, 45, 25]
782
+ 2. Q2 Sales: ['Product A', 'Product B', 'Product C'] = [25, 50, 25]
783
+ 3. Show both charts with different colors
784
+ 4. Use appropriate titles ('Q1 Sales Distribution', 'Q2 Sales Distribution')
785
+ 5. Discuss the trends visible in the comparison"""
786
+
787
+
788
+ @prompt("pie_chart_best_practices")
789
+ async def pie_chart_best_practices_prompt() -> str:
790
+ """Pie chart best practices demonstration"""
791
+ return """Demonstrate pie chart best practices:
792
+ 1. Start with many categories: ['A', 'B', 'C', 'D', 'E', 'F', 'G'] = [5, 8, 12, 15, 25, 20, 15]
793
+ 2. Show why this is problematic (too many small segments)
794
+ 3. Combine small categories: ['A+B+C', 'D', 'E', 'F', 'G'] = [25, 15, 25, 20, 15]
795
+ 4. Create the improved version with title 'Improved: Combined Small Categories'
796
+ 5. Explain the improvement in readability"""
797
+
798
+
799
+ @prompt("single_value_pie_chart")
800
+ async def single_value_pie_chart_prompt() -> str:
801
+ """Single-value pie chart for progress indicators"""
802
+ return """Create single-value pie charts perfect for progress indicators:
803
+ 1. Basic progress chart: ['Complete', 'Remaining'] = [75, 25], colors=['green', 'default']
804
+ 2. Title: 'Project Progress: 75%'
805
+ 3. Show only percentages (show_values=False, show_percentages=True)
806
+ 4. Note: Remaining area appears as spaces, legend only shows 'Complete' entry
807
+ 5. Perfect for dashboards, completion meters, utilization rates"""
808
+
809
+
810
+ @prompt("single_value_pie_with_remaining_color")
811
+ async def single_value_pie_with_remaining_color_prompt() -> str:
812
+ """Single-value pie chart with colored remaining area"""
813
+ return """Create single-value pie chart with remaining_color parameter:
814
+ 1. Data: ['Complete', 'Remaining'] = [60, 40], colors=['blue', 'default']
815
+ 2. Add remaining_color='gray' to color the remaining slice
816
+ 3. Title: 'Task Completion: 60%'
817
+ 4. Compare with version without remaining_color
818
+ 5. Note: When remaining_color is specified, 'Remaining' appears in legend"""
819
+
820
+
821
+ @prompt("doughnut_chart_basic")
822
+ async def doughnut_chart_basic_prompt() -> str:
823
+ """Basic doughnut chart with hollow center"""
824
+ return """Create a doughnut chart with hollow center:
825
+ 1. Data: ['Sales', 'Marketing', 'Support', 'Development'] = [40, 25, 15, 20]
826
+ 2. Colors: ['blue', 'orange', 'green', 'red']
827
+ 3. Add donut=True parameter to create hollow center
828
+ 4. Title: 'Department Budget - Doughnut Chart'
829
+ 5. Note: Inner radius automatically set to 1/3 of outer radius, center remains empty"""
830
+
831
+
832
+ @prompt("doughnut_progress_indicator")
833
+ async def doughnut_progress_indicator_prompt() -> str:
834
+ """Doughnut chart as progress indicator"""
835
+ return """Create a doughnut chart progress indicator:
836
+ 1. Single-value data: ['Completed', 'Remaining'] = [85, 15]
837
+ 2. Colors: ['cyan', 'default']
838
+ 3. Use both donut=True and show only percentages
839
+ 4. Title: 'Project Progress - 85% Complete'
840
+ 5. Perfect for modern dashboards - combines hollow center with progress visualization"""
841
+
842
+
843
+ @prompt("quick_donut_convenience")
844
+ async def quick_donut_convenience_prompt() -> str:
845
+ """Using quick_donut convenience function"""
846
+ return """Demonstrate the quick_donut convenience function:
847
+ 1. Use quick_donut instead of quick_pie with donut=True
848
+ 2. Data: ['Task A', 'Task B', 'Task C'] = [30, 45, 25]
849
+ 3. Colors: ['purple', 'yellow', 'green']
850
+ 4. Title: 'Task Distribution'
851
+ 5. Show how quick_donut automatically creates hollow center charts"""
852
+
853
+
497
854
  # Main server entry point
498
855
  def start_server():
499
856
  """Start the MCP server."""
500
- print("Starting Plotext Plus MCP Server...")
501
- run()
857
+ import sys
858
+ import os
859
+
860
+ # Check if we're being run in STDIO mode (by MCP CLI)
861
+ # Force HTTP mode if MCP_HTTP_MODE is set, otherwise detect based on stdin
862
+ force_http = os.getenv('MCP_HTTP_MODE', '').lower() == 'true'
863
+ force_stdio = os.getenv('MCP_STDIO_MODE', '').lower() == 'true'
864
+
865
+ if force_http:
866
+ is_stdio_mode = False
867
+ elif force_stdio:
868
+ is_stdio_mode = True
869
+ else:
870
+ # Auto-detect: STDIO mode when stdin is not a terminal (pipes/redirects)
871
+ is_stdio_mode = not sys.stdin.isatty()
872
+
873
+ if is_stdio_mode:
874
+ # STDIO mode for MCP CLI - simple JSON-RPC implementation
875
+ import json
876
+ import asyncio
877
+
878
+ async def handle_stdio():
879
+ try:
880
+ for line in sys.stdin:
881
+ if not line.strip():
882
+ continue
883
+
884
+ try:
885
+ request = json.loads(line)
886
+ method = request.get('method')
887
+ request_id = request.get('id')
888
+
889
+ if method == 'initialize':
890
+ response = {
891
+ "jsonrpc": "2.0",
892
+ "id": request_id,
893
+ "result": {
894
+ "protocolVersion": "2024-11-05",
895
+ "capabilities": {
896
+ "tools": {},
897
+ "prompts": {},
898
+ "resources": {}
899
+ },
900
+ "serverInfo": {
901
+ "name": "Plotext Plus MCP Server",
902
+ "version": "1.0.0"
903
+ }
904
+ }
905
+ }
906
+ elif method == 'tools/list':
907
+ response = {
908
+ "jsonrpc": "2.0",
909
+ "id": request_id,
910
+ "result": {
911
+ "tools": [
912
+ {
913
+ "name": "line_plot",
914
+ "description": "Create a line plot with given x and y data points",
915
+ "inputSchema": {
916
+ "type": "object",
917
+ "properties": {
918
+ "x": {"type": "array", "items": {"type": "number"}},
919
+ "y": {"type": "array", "items": {"type": "number"}},
920
+ "title": {"type": "string"}
921
+ },
922
+ "required": ["x", "y"]
923
+ }
924
+ }
925
+ ]
926
+ }
927
+ }
928
+ elif method == 'tools/call':
929
+ tool_name = request['params']['name']
930
+ arguments = request['params']['arguments']
931
+
932
+ if tool_name == 'line_plot':
933
+ try:
934
+ result = await line_plot(**arguments)
935
+ response = {
936
+ "jsonrpc": "2.0",
937
+ "id": request_id,
938
+ "result": {
939
+ "content": [
940
+ {
941
+ "type": "text",
942
+ "text": result
943
+ }
944
+ ]
945
+ }
946
+ }
947
+ except Exception as e:
948
+ response = {
949
+ "jsonrpc": "2.0",
950
+ "id": request_id,
951
+ "error": {
952
+ "code": -32000,
953
+ "message": str(e)
954
+ }
955
+ }
956
+ else:
957
+ response = {
958
+ "jsonrpc": "2.0",
959
+ "id": request_id,
960
+ "error": {
961
+ "code": -32601,
962
+ "message": f"Unknown tool: {tool_name}"
963
+ }
964
+ }
965
+ else:
966
+ response = {
967
+ "jsonrpc": "2.0",
968
+ "id": request_id,
969
+ "error": {
970
+ "code": -32601,
971
+ "message": f"Unknown method: {method}"
972
+ }
973
+ }
974
+
975
+ print(json.dumps(response), flush=True)
976
+
977
+ except Exception as e:
978
+ error_response = {
979
+ "jsonrpc": "2.0",
980
+ "id": request.get('id') if 'request' in locals() else None,
981
+ "error": {
982
+ "code": -32700,
983
+ "message": f"Parse error: {str(e)}"
984
+ }
985
+ }
986
+ print(json.dumps(error_response), flush=True)
987
+
988
+ except Exception as e:
989
+ print(f"STDIO handler error: {e}", file=sys.stderr)
990
+
991
+ asyncio.run(handle_stdio())
992
+ else:
993
+ # HTTP server mode (default)
994
+ print("Starting Plotext Plus MCP Server...")
995
+ from chuk_mcp_server import ChukMCPServer
996
+ server = ChukMCPServer(
997
+ name="Plotext Plus MCP Server",
998
+ version="1.0.0",
999
+ prompts=True, # Enable prompts capability
1000
+ logging=True # Enable logging capability for MCP clients
1001
+ )
1002
+ server.run()
502
1003
 
503
1004
 
504
1005
  if __name__ == "__main__":
plotext_plus/plotting.py CHANGED
@@ -12,7 +12,7 @@ All the core plotting capabilities are exposed through clean, public interfaces.
12
12
  # Import all main plotting functions from the internal core module
13
13
  from ._core import (
14
14
  # Basic plotting functions
15
- scatter, plot, bar,
15
+ scatter, plot, bar, pie,
16
16
  matrix_plot, candlestick,
17
17
 
18
18
  # Plot customization
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotext_plus
3
- Version: 1.0.7
3
+ Version: 1.0.9
4
4
  Summary: Modern terminal plotting library with enhanced visual features, themes, and AI integration
5
5
  Project-URL: Homepage, https://github.com/ccmitchellusa/plotext_plus
6
6
  Project-URL: Repository, https://github.com/ccmitchellusa/plotext_plus.git
@@ -52,7 +52,7 @@ Description-Content-Type: text/markdown
52
52
 
53
53
  ## ✨ Key Features
54
54
 
55
- 🎯 **Multiple Plot Types**: [scatter](docs/basic.md#scatter-plot), [line](docs/basic.md#line-plot), [bar](docs/bar.md), [histogram](docs/bar.md#histogram-plot), [candlestick](docs/datetime.md#candlestick-plot), [heatmap](docs/special.md), [confusion matrix](docs/special.md#confusion-matrix), and more
55
+ 🎯 **Multiple Plot Types**: [scatter](docs/basic.md#scatter-plot), [line](docs/basic.md#line-plot), [bar](docs/bar.md), [histogram](docs/bar.md#histogram-plot), [candlestick](docs/datetime.md#candlestick-plot), [heatmap](docs/special.md), [confusion matrix](docs/special.md#confusion-matrix), [pie](docs/basic.md#pie-plot), [doughnut](docs/basic.md#doughnut-charts) and more
56
56
 
57
57
  🎨 **Rich Visuals**: [Banner mode](docs/chart_classes.md), [themes](docs/themes.md), [colored text](docs/utilities.md#colored-text), automatic terminal width detection
58
58
 
@@ -1,33 +1,33 @@
1
- plotext_plus/__init__.py,sha256=lFka1k6nnii0o746a8J6xzpy9rfHK6x1zQ24R1DHFf8,831
1
+ plotext_plus/__init__.py,sha256=ZqQT3ybOtVRB4rY406K_RzBZ3CJ1Z3z2FZU0J_Wwc04,855
2
2
  plotext_plus/__main__.py,sha256=UkpSWVzOAfWBm3O1jyN1QO6KILQWhaSw4DcIdohJlbI,37
3
- plotext_plus/_api.py,sha256=E3AR-hNkQalVdZ2yv9lWv6PJxdDWgi81vPyrNYzA-lA,28429
3
+ plotext_plus/_api.py,sha256=EwP0LePRoqvid0IyqhYZzCKTTknAbspa4R2WeVGhxk8,31026
4
4
  plotext_plus/_build.py,sha256=sW7Fm2hYEkRDFapbk_B7sp7u9addBOzFH9MSULn54U8,20936
5
- plotext_plus/_core.py,sha256=dST_wXAHsJMrILPkUzON7eJz41bwVlsz_oi2X8oWTLU,20673
5
+ plotext_plus/_core.py,sha256=sP3WIzXFO4ltdNX-QXm00Duc4GXe71b8gI02kb1DrkM,22025
6
6
  plotext_plus/_date.py,sha256=kTjx-_df5M0yvUTJlg1LqGyDRLxOL2or0op6Si0CUU0,2879
7
7
  plotext_plus/_default.py,sha256=bbPO60SFziIpPOI8jY6xtP3urri0H2vhYzwasfmybz4,3316
8
- plotext_plus/_dict.py,sha256=Ip4YUgkoZAZ2tDwcKLjZEYaX9gD9MeDnC3SFMr8AwSI,21834
8
+ plotext_plus/_dict.py,sha256=I9Wb2YZauMaHjv-1p4oyQ4kRncvhjJGXzwHefeVirMI,21832
9
9
  plotext_plus/_doc.py,sha256=qJ--cy1sssH_1EDDkrcOtLeAHahmdqSaYyhOrj4gk54,37183
10
10
  plotext_plus/_doc_utils.py,sha256=vVwtDnAJ7vVUafL9zulkILXupGNK3v3Z9aeddX1F828,10397
11
- plotext_plus/_figure.py,sha256=zXLV-GAUo-VpVPrcKbo9RobEwY9iIAXxuiv94HM5zHU,31144
11
+ plotext_plus/_figure.py,sha256=xLkCqi5nu8NHY9Lv9Zo2jcg-5BP07ggkJ35rs8peSiY,31924
12
12
  plotext_plus/_global.py,sha256=NFEupN17WY2Xk4EgDY-On_1J3MOvwNBYubnt6Rg8QW4,15387
13
13
  plotext_plus/_matrix.py,sha256=e4sHwghNZeclBORUgWc5BZxdfqoH0yDeh49XymPNHlY,8298
14
- plotext_plus/_monitor.py,sha256=ccBuN1DcY85LVJ7ScUrstlKhqYxeEQ6yxi3DGEZ8f_Q,40749
14
+ plotext_plus/_monitor.py,sha256=TH-WC2qQj-GRMxtrcvHdgXi67VPsY4_xSGr9kJWJMtI,62946
15
15
  plotext_plus/_output.py,sha256=9hBPOum2R6FA1EgIyIMgkMsL7DjtSpX3En8zXGbdx-c,4315
16
16
  plotext_plus/_shtab.py,sha256=l1bDdxUbbg5urjNjyC5agbdiYu-OLbruRJZ_AuyirbQ,281
17
- plotext_plus/_themes.py,sha256=6Imn9hwcb8uEbCm2u_ihg3ZN9dUzdkMe0IGdAX3ErKU,14111
17
+ plotext_plus/_themes.py,sha256=2KQFpAxBvksV_iE8QCMs3lbjq5Tv2TsK_OQLiRg5Gmg,14177
18
18
  plotext_plus/_utility.py,sha256=kp5FvE5kooOw0fcVfK_k4o0Fr9ClyFIXMYh4BiXOMlc,32918
19
19
  plotext_plus/api.py,sha256=56xYn9uzkMe88iELbanRQ-12bC8Rvv2Jk0BX45Fk8zY,28424
20
- plotext_plus/charts.py,sha256=jQcIe_9MQBFN845l-11-VIhnaYjTca6hxkQhnc_j4dY,1263
20
+ plotext_plus/charts.py,sha256=VfL0keEo02TPwjUZ6eNFx2q2qOTpiVoq_igL5rGE-gY,1315
21
21
  plotext_plus/core.py,sha256=k6g2dYuHFcNevLJM0elkYupz7fC8Qqk_vLEyrXu2V3I,20668
22
22
  plotext_plus/mcp_cli.py,sha256=otZaP1tolvAL2vx84TVNiTlFdJyDx1cMIMcbhqsSna8,2651
23
- plotext_plus/mcp_server.py,sha256=6DW98E38TTR8OfTp-nBumcQf9Djz1w5980ueN2_O12M,14528
23
+ plotext_plus/mcp_server.py,sha256=cL-6CL3nQ53LbC3ZReC77_zxFrRVA8UgqQEqNRakaXI,37130
24
24
  plotext_plus/plotext_cli.py,sha256=o3JmSNc7Ify4C6Wkva8x8QRh-pLeiD3VArc3E78xlVU,17677
25
- plotext_plus/plotting.py,sha256=upSC-HTcamuWV89G-GkLWH2ClzhezZKVd1FLgskJg08,2059
25
+ plotext_plus/plotting.py,sha256=7YUISs2Aolqo-lMEX8amGfMNOSFV9zcSqlrEMwVK2RM,2063
26
26
  plotext_plus/themes.py,sha256=xX6XqL2U-7MQmv-k1HU-UTSZRbTfv9op5_4ZRXm435g,725
27
27
  plotext_plus/utilities.py,sha256=scdPDGf6po2rvEL5DX7y1pY41uxlCXTVVIo0w5qA8JE,1170
28
28
  plotext_plus/utils.py,sha256=NFEupN17WY2Xk4EgDY-On_1J3MOvwNBYubnt6Rg8QW4,15387
29
- plotext_plus-1.0.7.dist-info/METADATA,sha256=6p_C9FXFIAh3E3PXCMhL3AsIuRmvdWLa1o4O8FxbXI0,11282
30
- plotext_plus-1.0.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
- plotext_plus-1.0.7.dist-info/entry_points.txt,sha256=XocAh2z8hTGtuQL1zEcGTulrgfZ2g5UMyHSV-JOSNN8,103
32
- plotext_plus-1.0.7.dist-info/licenses/LICENSE,sha256=MkgUiRFwIvXwUVDEPy11uIULq7pGDHpIulD4KulsjnM,1150
33
- plotext_plus-1.0.7.dist-info/RECORD,,
29
+ plotext_plus-1.0.9.dist-info/METADATA,sha256=mMcu8ukY6WNT9L86-ROTSG9-PVhxTi2ENtP2owMLtmE,11355
30
+ plotext_plus-1.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ plotext_plus-1.0.9.dist-info/entry_points.txt,sha256=XocAh2z8hTGtuQL1zEcGTulrgfZ2g5UMyHSV-JOSNN8,103
32
+ plotext_plus-1.0.9.dist-info/licenses/LICENSE,sha256=MkgUiRFwIvXwUVDEPy11uIULq7pGDHpIulD4KulsjnM,1150
33
+ plotext_plus-1.0.9.dist-info/RECORD,,