aboutsummaryrefslogtreecommitdiff
path: root/game/src/Game/Theme/PieChart/PieChart.gd
blob: aba1f816e2802e36964c2c572b687ee9afb9689f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
@tool
extends TextureRect

class_name PieChart


@export var donut : bool = false
@export_range(0.0,1.0) var donut_inner_radius : float = 0.5
@export_range(0.0,0.5) var radius : float = 0.4
@export var shadow_displacement : Vector2 = Vector2(0.6,0.6)
@export var shadow_focus : float = 1.0
@export var shadow_radius : float = 0.6
@export var shadow_thickness : float = 1.0

@export var trim_colour : Color = Color(0.0,0.0,0.0)
@export_range(0.0,1.0) var trim_size : float = 0.02
@export var donut_inner_trim : bool = true
@export var slice_gradient_falloff : float = 3.6
@export var slice_gradient_base : float = 3.1

#@onready
@export var RichTooltip : RichTextLabel# = $RichToolTip

#a data class for the pie chart
class SliceData:
   #primary properties, change these to change
   #the displayed piechart
   var colour:Color = Color(1.0,0.0,0.0):
      get:
         return colour
      set(value):
         colour = value
   var tooltip:String = "DEFAULT":
      get:
         return tooltip
      set(value):
         tooltip = value
   var quantity:float = -1:
      get:
         return quantity
      set(value):
         quantity = value
   #derived properties, don't set from an external script
   var final_angle:float = -1:
      get:
         return final_angle
      set(value):
         final_angle = value
   var percentage:float = 0:
      get:
         return percentage
      set(value):
         percentage = clampf(value,0,1)

   func _init(quantityIn:float,tooltipIn:String,colourIn:Color):
      colour = colourIn
      tooltip = tooltipIn
      quantity = quantityIn

#The key of an entry of this dictionnary should be an easy to reference constant
#The tooltip label is what the user will actually read
var slices: Dictionary = {

}
#example slices:
"""
   "label1":SliceData.new(5,"Conservative",Color(0.0,0.0,1.0)),
   "label2":SliceData.new(3,"Liberal",Color(1.0,1.0,0.0)),
   "label3":SliceData.new(2,"Reactionary",Color(0.4,0.0,0.6))
"""

#These functions are the interface a developer will use to update the piechart
#The piechart will only redraw once one of these has been triggered
func addOrReplaceLabel(labelName:String,quantity:float,tooltip:String,colour:Color=Color(0.0,0.0,0.0)) -> void:
   slices[labelName] = SliceData.new(quantity,tooltip,colour)
   _recalculate()

func updateLabelQuantity(labelName:String,quantity:float) -> void:
   if slices.has(labelName):
      slices[labelName].quantity = quantity
   _recalculate()

func updateLabelColour(labelName:String,colour:Color) -> void:
   if slices.has(labelName):
      slices[labelName].colour = colour
   _recalculate()

func updateLabelTooltip(labelName:String,tooltip:String) -> void:
   if slices.has(labelName):
      slices[labelName].tooltip = tooltip

func RemoveLabel(labelName:String) -> bool:
   var out = slices.erase(labelName)
   _recalculate()
   return out

#Perhaps in the future, a method to reorder the labels?


#In editor only, force the shader parameters to update whenever _draw
#is called so developers can see their changes
#otherwise, for performance, reduce the number of material resets
func _draw():
   if Engine.is_editor_hint():
      if not material:
         _reset_material()
      _setShaderParams()
      _recalculate()

func _ready():
   _reset_material()
   _setShaderParams()

func _reset_material():
   texture = CanvasTexture.new()
   var mat_res = load("res://src/Game/Theme/PieChart/PieChartMat.tres")
   material = mat_res.duplicate(true)
   custom_minimum_size = Vector2(50.0,50.0)
   size_flags_horizontal = Control.SIZE_SHRINK_CENTER
   size_flags_vertical = Control.SIZE_SHRINK_CENTER
   _recalculate()

func _setShaderParams():
   material.set_shader_parameter("donut",donut)
   material.set_shader_parameter("donut_inner_trim",donut_inner_trim)
   material.set_shader_parameter("radius",radius)
   material.set_shader_parameter("donut_inner_radius",donut_inner_radius/2.0)
   
   material.set_shader_parameter("trim_colour",Vector3(trim_colour.r,trim_colour.g,trim_colour.b))
   material.set_shader_parameter("trim_size",trim_size)
   material.set_shader_parameter("gradient_falloff",slice_gradient_falloff)
   material.set_shader_parameter("gradient_base",slice_gradient_base)
   
   material.set_shader_parameter("shadow_displacement",shadow_displacement)
   material.set_shader_parameter("shadow_tightness",shadow_focus)
   material.set_shader_parameter("shadow_radius",shadow_radius)
   material.set_shader_parameter("shadow_thickness",shadow_thickness)


#Update the slice angles based on the new slice data
func _recalculate() -> void:
   #where the slices are the public interface, these are the actual paramters
   #which will be sent to the shader
   var angles: Array = []
   var colours: Array = []
   
   var total:float = 0
   for slice in slices.values():
      total += slice.quantity

   var current_arc_start:float = 0
   var current_arc_finish:float = 0

   for slice in slices.values():
      slice.percentage = slice.quantity / total
      var rads_to_cover:float = slice.percentage * 2.0*PI
      current_arc_finish = current_arc_start + rads_to_cover
      slice.final_angle = current_arc_finish
      current_arc_start = current_arc_finish
      angles.push_back(current_arc_finish)
      colours.push_back(Vector3(slice.colour.r,slice.colour.g,slice.colour.b) )
   material.set_shader_parameter("stopAngles",angles)
   material.set_shader_parameter("colours",colours)
   

#Process mouse to select the appropriate tooltip for the slice
func _gui_input(event:InputEvent):
   if event is InputEventMouse:
      var pos = event.position
      var _handled:bool = _handleTooltip(pos)

func _on_mouse_exited():
   RichTooltip.visible = false

#takes a mouse position, and sets an appropriate tooltip for the slice the mouse
#is hovered over. Returns a boolean on whether the tooltip was handled.
func _handleTooltip(pos:Vector2) -> bool:
   #is it within the circle?
   var center = Vector2(size.x/2.0, size.x/2.0)
   var radius = size.x/2.0
   var distance = center.distance_to(pos)
   #print(distance >= donut_inner_radius/2.0)
   var real_donut_inner_radius:float = radius * donut_inner_radius
   if distance <= radius and (not donut or distance >= real_donut_inner_radius):
      var angle = _convertAngle(center.angle_to_point(pos))
      for label in slices.keys():
         var slice = slices.get(label)
         if angle <= slice.final_angle:
            RichTooltip.visible = true
            RichTooltip.text = _createTooltip(label)
            RichTooltip.position =  pos + Vector2(5,5) + get_global_rect().position #get_global_rect().position +
            RichTooltip.reset_size()
            
            return true
   else:
      #Technically the corners of the bounding box
      #are part of the chart, but we don't want a tooltip there
      RichTooltip.visible = false
   return false

#create a list of all the values and percentages
# but with the hovered one on top and highlighted
func _createTooltip(labelHovered:String) -> String:
   var tooltip:String = ""
   var hoveredSlice = slices.get(labelHovered)
   var formatted_percent = _formatpercent(hoveredSlice.percentage)
   #TOOD: perhaps this is a bit much, but final feedback should determine this
   tooltip += "[font_size=10][i][u][b]>> {name} {percentage}% <<[/b][/u][/i]".format(
      {"name":hoveredSlice.tooltip,"percentage":formatted_percent})
   
   for label in slices.keys():
      if label == labelHovered: continue
      var slice = slices.get(label)
      var percent = _formatpercent(slice.percentage)
      tooltip += "\n{name} {percentage}%".format(
         {"name":slice.tooltip,"percentage":percent})
   tooltip += "[/font_size]"
   return tooltip

#angle from center.angle_to_point is measured from the +x axis
#, but the chart starts from +y
#the input angle is also -180 to 180, where we want 0 to 360
func _convertAngle(angleIn:float) -> float:
   #make the angle start from +y, range is now -90 to 270
   var angle = angleIn + PI/2.0
   #adjust range to be 0 to 360
   if angle < 0:
      angle = 2.0*PI + angle
   return angle
   
func _formatpercent(percentIn:float) -> float:
   return snappedf((percentIn * 100),0.1)