├── .github └── FUNDING.yml ├── CTkRadarChart ├── __init__.py └── ctk_radar_chart.py ├── LICENSE └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | ko_fi: akascape 3 | 4 | -------------------------------------------------------------------------------- /CTkRadarChart/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | CTkRadarChart 3 | Author: Akash Bora (Akascape) 4 | Version: 1.0 5 | """ 6 | 7 | from .ctk_radar_chart import CTkRadarChart 8 | -------------------------------------------------------------------------------- /CTkRadarChart/ctk_radar_chart.py: -------------------------------------------------------------------------------- 1 | """ 2 | CTkRadarChart 3 | Author: Akash Bora (Akascape) 4 | """ 5 | 6 | import tkinter as tk 7 | import math 8 | from typing import Union, Tuple, Optional, Any 9 | import random 10 | import customtkinter 11 | from customtkinter.windows.widgets.appearance_mode import CTkAppearanceModeBaseClass 12 | from customtkinter.windows.widgets.scaling import CTkScalingBaseClass 13 | 14 | class CTkRadarChart(tk.Canvas, CTkAppearanceModeBaseClass, CTkScalingBaseClass): 15 | """ A class for displaying radar chart in customtkinter """ 16 | def __init__(self, 17 | master: Any, 18 | radius: int=400, 19 | num_axes: int=6, 20 | radial_lines: int=5, 21 | border_width: int=2, 22 | padding: int=30, 23 | labels: list=[], 24 | font: Optional[Union[tuple, customtkinter.CTkFont]] = None, 25 | bg_color: Union[str, Tuple[str, str]] = "transparent", 26 | fg_color: Optional[Union[str, Tuple[str, str]]] = None, 27 | text_color: Optional[Union[str, Tuple[str, str]]] = None, 28 | **kwargs): 29 | 30 | self.master = master 31 | self.bg_color = master.cget("fg_color") if (bg_color=="transparent" or bg_color is None) else bg_color 32 | 33 | CTkAppearanceModeBaseClass.__init__(self) 34 | CTkScalingBaseClass.__init__(self, scaling_type="widget") 35 | 36 | self.radius = self._apply_widget_scaling(radius) 37 | self.font = font 38 | self.border_width = self._apply_widget_scaling(border_width) 39 | 40 | self.fg_color = customtkinter.ThemeManager.theme["CTkButton"]["border_color"] if fg_color is None else text_color 41 | self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color 42 | self.num_axes = num_axes 43 | self.radial_lines = radial_lines 44 | 45 | if self.num_axes<=2: 46 | raise ValueError("Axes number must be greater or equal to 3") 47 | 48 | if self.radial_lines<=0: 49 | self.radial_lines = 1 50 | 51 | super().__init__(master, bg=self.master._apply_appearance_mode(self.bg_color), 52 | highlightthickness=0, width=self.radius, height=self.radius, 53 | borderwidth=0, **kwargs) 54 | 55 | self.data = [] 56 | self.colors = [] 57 | self.fills = [] 58 | self.tags = [] 59 | 60 | self.labels = labels if labels is not None else [''] * num_axes 61 | 62 | self.padding = padding # Padding from the left side 63 | self.center = (self.radius + self.padding, self.radius + self.padding) 64 | self.draw_chart() 65 | 66 | # Bind the resize event 67 | self.bind("", self.resize, add="+") 68 | 69 | def draw_chart(self): 70 | self.delete("all") # Clear the canvas 71 | self.draw_background() 72 | for data in self.data: 73 | if len(data)!=self.num_axes: 74 | data.append(0) 75 | 76 | for dataset, color, fill in zip(self.data, self.colors, self.fills): 77 | self.draw_dataset(dataset, color, fill) 78 | self.draw_labels() 79 | 80 | def draw_background(self): 81 | # Draw the axes and background lines 82 | for i in range(self.num_axes): 83 | angle = 2 * math.pi * i / self.num_axes 84 | x = self.center[0] + self.radius * math.cos(angle) 85 | y = self.center[1] + self.radius * math.sin(angle) 86 | self.create_line(self.center[0], self.center[1], x, y, 87 | fill=self.master._apply_appearance_mode(self.fg_color)) 88 | self.draw_polygon(i, outline=self.master._apply_appearance_mode(self.fg_color)) 89 | 90 | def draw_polygon(self, i, outline): 91 | points = [] 92 | for j in range(1, self.radial_lines+1): # Draw 5 concentric polygons for the background 93 | current_points = [] 94 | for k in range(self.num_axes): 95 | angle = 2 * math.pi * k / self.num_axes 96 | x = self.center[0] + (self.radius * j / self.radial_lines) * math.cos(angle) 97 | y = self.center[1] + (self.radius * j / self.radial_lines) * math.sin(angle) 98 | current_points.extend([x, y]) 99 | self.create_polygon(current_points, outline=outline, fill='', width=1) 100 | 101 | def draw_dataset(self, dataset, color, fill): 102 | # Draw the data 103 | data_points = [] 104 | for i, value in enumerate(dataset[:self.num_axes]): 105 | angle = 2 * math.pi * i / self.num_axes 106 | x = self.center[0] + self.radius * value / 100 * math.cos(angle) 107 | y = self.center[1] + self.radius * value / 100 * math.sin(angle) 108 | data_points.extend([x, y]) 109 | 110 | outline_color = self.master._apply_appearance_mode(color) 111 | 112 | fill_color = '' 113 | if fill: 114 | fill_color = outline_color 115 | 116 | self.create_polygon(data_points, outline=outline_color, fill=fill_color, stipple='gray12', 117 | width=self.border_width) 118 | 119 | def draw_labels(self): 120 | # Draw labels at each corner 121 | for i, label in enumerate(self.labels[:self.num_axes]): 122 | angle = 2 * math.pi * i / self.num_axes 123 | x = self.center[0] + (self.radius + 10) * math.cos(angle) 124 | y = self.center[1] + (self.radius + 10) * math.sin(angle) 125 | self.create_text(x, y, text=label, font=self.font, anchor='center', 126 | fill=self.master._apply_appearance_mode(self.text_color)) 127 | 128 | def resize(self, event): 129 | # Update the center and radius based on the new window size 130 | self.radius = min(event.width - self.padding * 2, event.height - self.padding * 2) // 2 131 | self.center = (event.width // 2, event.height // 2) 132 | self.draw_chart() 133 | 134 | def add_data(self, tag, data=[], color=None, fill=True): 135 | # Add new data to the chart 136 | if type(data) is not list: 137 | raise ValueError("Chart data must be in a list.") 138 | 139 | if tag in self.tags: 140 | self.delete_data(tag) 141 | 142 | for value in data: 143 | if value>100: 144 | data[data.index(value)] = 100 145 | elif value<0: 146 | data[data.index(value)] = 0 147 | 148 | if color is None: 149 | # select a random color 150 | color = outline_color = "#"+''.join([random.choice('ABCDEF0123456789') for i in range(6)]) 151 | 152 | self.data.append(data) 153 | self.colors.append(color) 154 | self.fills.append(fill) 155 | self.tags.append(tag) 156 | self.draw_chart() 157 | 158 | def delete_data(self, tag): 159 | # delete the tag data from chart 160 | if tag not in self.tags: 161 | return 162 | index = self.tags.index(tag) 163 | self.data.pop(index) 164 | self.fills.pop(index) 165 | self.colors.pop(index) 166 | self.tags.pop(index) 167 | self.draw_chart() 168 | 169 | def update_data(self, tag, data=None, color=None, fill=None): 170 | # update a tag data 171 | if tag not in self.tags: 172 | return 173 | 174 | index = self.tags.index(tag) 175 | if data is not None: 176 | self.data[index] = data 177 | if color is not None: 178 | self.colors[index] = color 179 | if fill is not None: 180 | self.fills[index] = kwargs["fill"] 181 | 182 | self.add_data(tag=tag, data=self.data[index], color=self.colors[index], fill=self.fills[index]) 183 | 184 | def _set_appearance_mode(self, mode_string): 185 | super().config(bg=self.master._apply_appearance_mode(self.bg_color)) 186 | self.draw_chart() 187 | 188 | def _set_scaling(self, new_widget_scaling, new_window_scaling): 189 | super()._set_scaling(new_widget_scaling, new_window_scaling) 190 | self.radius = self._apply_widget_scaling(self.radius) 191 | super().config(width=self.radius, height=self.radius) 192 | self.border_width = self._apply_widget_scaling(self.border_width) 193 | self.draw_chart() 194 | 195 | def configure(self, **kwargs): 196 | # configurable options 197 | if "fg_color" in kwargs: 198 | self.fg_color = kwargs.pop("fg_color") 199 | 200 | if "bg_color" in kwargs: 201 | self.bg_color = kwargs.pop("bg_color") 202 | super().config(bg=self.master._apply_appearance_mode(self.bg_color)) 203 | 204 | if "border_width" in kwargs: 205 | self.border_width = kwargs.pop("border_width") 206 | 207 | if "font" in kwargs: 208 | self.font = kwargs.pop("font") 209 | 210 | if "text_color" in kwargs: 211 | self.text_color = kwargs.pop("text_color") 212 | 213 | if "radial_lines" in kwargs: 214 | self.radial_lines = kwargs.pop("radial_lines") 215 | if self.radial_lines<=0: 216 | self.radial_lines = 1 217 | 218 | if "radius" in kwargs: 219 | self.radius = kwargs.pop("radius") 220 | super().config(width=self.radius, height=self.radius) 221 | 222 | if "num_axes" in kwargs: 223 | self.num_axes = kwargs.pop("num_axes") 224 | 225 | if "labels" in kwargs: 226 | self.labels = kwargs.pop("labels") 227 | 228 | if "padding" in kwargs: 229 | self.padding = kwargs.pop("padding") 230 | 231 | super().config(**kwargs) 232 | self.draw_chart() 233 | 234 | def cget(self, param): 235 | # return required parameter 236 | if param=="fg_color": 237 | return self.fg_color 238 | if param=="bg_color": 239 | return self.bg_color 240 | if param=="border_width": 241 | return self.border_width 242 | if param=="font": 243 | return self.font 244 | if param=="text_color": 245 | return self.text_color 246 | if param=="radial_lines": 247 | return self.radial_lines 248 | if param=="radius": 249 | return self.radius 250 | if param=="num_axes": 251 | return self.num_axes 252 | if param=="labels": 253 | return self.labels 254 | if param=="data": 255 | return self.get() 256 | if param=="padding": 257 | return self.padding 258 | return super().cget(param) 259 | 260 | def get(self, tag=None): 261 | # get values from the chart 262 | if tag: 263 | index = self.tags.index(tag) 264 | return self.data[index], self.colors[index] 265 | 266 | data = {} 267 | for tag in self.tags: 268 | index = self.tags.index(tag) 269 | data.update({tag:{'data':self.data[index],'color':self.colors[index]}}) 270 | return data 271 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CTkRadarChart 2 | A simple widget for customtkinter to display radar chart, made with tkinter canvas. Fully custimasable widget, with resizability and theme colors. 3 | ![screenshot](https://github.com/Akascape/CTkRadarChart/assets/89206401/0d3ecda5-f73d-4d27-b7d7-817cf42905ec) 4 | 5 | **What is a radar chart?** 6 | 7 | A radar chart, also known as a spider chart, web chart, is a graphical method used to display multivariate data. It consists of a sequence of equi-angular spokes, with each spoke representing one of the variables. The data length of a spoke is proportional to the magnitude of the variable for the data point relative to the maximum magnitude of the variable across all data points. 8 | 9 | ## Installation 10 | ### [GitHub repo size](https://github.com/Akascape/CTkRadarChart/archive/refs/heads/main.zip) 11 | 12 | Download the source code, paste the `CTkRadarChart` folder in the directory where your program is present. 13 | 14 | ## Usage 15 | ```python 16 | import customtkinter 17 | from CTkRadarChart import * 18 | 19 | root = customtkinter.CTk() 20 | 21 | # Some labels that are shown at each axis 22 | labels = ['Speed', 'Reliability', 'Comfort', 'Safety', 'Efficiency', 'Capacity'] 23 | 24 | # Create the RadarChart instance 25 | chart = CTkRadarChart(root, labels=labels) 26 | chart.pack(fill="both", expand=True) 27 | 28 | # Add new data 29 | chart.add_data("A", [90, 70, 90, 75, 60, 80]) 30 | chart.add_data("B", [60, 80, 70, 85, 75, 90]) 31 | 32 | root.mainloop() 33 | ``` 34 | 35 | _Note: data should be in a list, maximum value: 100, minimum value: 0_ 36 | 37 | ## Arguments 38 | | Parameters | Details | 39 | |--------|----------| 40 | | master | root window, can be _CTk_ or _CTkFrame_| 41 | | radius | the initial size of the radar chart | 42 | | num_axes | number of axes in the chart | 43 | | radial_lines | number of grid lines inside the chart | 44 | | border_width | size of the data lines | 45 | | fg_color | color of the background chart lines | 46 | | bg_color | background color of the widget | 47 | | text_color | color of the label text | 48 | | labels | text shown at each axes | 49 | | padding | adjust space inside the widget | 50 | | font | font of the text labels | 51 | 52 | ## Methods 53 | - **.add_data(tag, data, color, fill)**: adds new data line in the chart, **tag**: data line name; **data**: list of values; **color**: color of line (optional, choses color randomly by default), **fill**: add color in the polygon (optional, true by default) 54 | - **.delete_data(tag)**: delete a line from the chart 55 | - **.update_data(tag, *args)**: update any tag data 56 | - **.get(tag)**: return data and color of the chart, tag is optional 57 | - **.configure(*args)**: change parameters of the radar chart 58 | - **.cget(parameter)**: return the required parameter from the chart 59 | 60 | Follow me for more stuff like this: [`Akascape`](https://github.com/Akascape/) 61 | ### That's all, hope it will help! 62 | --------------------------------------------------------------------------------