├── .gitignore ├── Demo ├── Demo.unity ├── DemoDrawGraph.cs ├── pic1.png └── pic2.png ├── Editor ├── GraphRenderer.cs ├── GraphStrings.cs └── GraphWindow.cs ├── README.md └── Scripts ├── DrawGraph.cs ├── GraphItem.cs ├── GraphItemExtension.cs └── ItemData.cs /.gitignore: -------------------------------------------------------------------------------- 1 | *.meta -------------------------------------------------------------------------------- /Demo/Demo.unity: -------------------------------------------------------------------------------- 1 | %YAML 1.1 2 | %TAG !u! tag:unity3d.com,2011: 3 | --- !u!29 &1 4 | OcclusionCullingSettings: 5 | m_ObjectHideFlags: 0 6 | serializedVersion: 2 7 | m_OcclusionBakeSettings: 8 | smallestOccluder: 5 9 | smallestHole: 0.25 10 | backfaceThreshold: 100 11 | m_SceneGUID: 00000000000000000000000000000000 12 | m_OcclusionCullingData: {fileID: 0} 13 | --- !u!104 &2 14 | RenderSettings: 15 | m_ObjectHideFlags: 0 16 | serializedVersion: 9 17 | m_Fog: 0 18 | m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} 19 | m_FogMode: 3 20 | m_FogDensity: 0.01 21 | m_LinearFogStart: 0 22 | m_LinearFogEnd: 300 23 | m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} 24 | m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} 25 | m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} 26 | m_AmbientIntensity: 1 27 | m_AmbientMode: 0 28 | m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} 29 | m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} 30 | m_HaloStrength: 0.5 31 | m_FlareStrength: 1 32 | m_FlareFadeSpeed: 3 33 | m_HaloTexture: {fileID: 0} 34 | m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} 35 | m_DefaultReflectionMode: 0 36 | m_DefaultReflectionResolution: 128 37 | m_ReflectionBounces: 1 38 | m_ReflectionIntensity: 1 39 | m_CustomReflection: {fileID: 0} 40 | m_Sun: {fileID: 0} 41 | m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} 42 | m_UseRadianceAmbientProbe: 0 43 | --- !u!157 &3 44 | LightmapSettings: 45 | m_ObjectHideFlags: 0 46 | serializedVersion: 11 47 | m_GIWorkflowMode: 1 48 | m_GISettings: 49 | serializedVersion: 2 50 | m_BounceScale: 1 51 | m_IndirectOutputScale: 1 52 | m_AlbedoBoost: 1 53 | m_EnvironmentLightingMode: 0 54 | m_EnableBakedLightmaps: 1 55 | m_EnableRealtimeLightmaps: 0 56 | m_LightmapEditorSettings: 57 | serializedVersion: 12 58 | m_Resolution: 2 59 | m_BakeResolution: 40 60 | m_AtlasSize: 1024 61 | m_AO: 0 62 | m_AOMaxDistance: 1 63 | m_CompAOExponent: 1 64 | m_CompAOExponentDirect: 0 65 | m_ExtractAmbientOcclusion: 0 66 | m_Padding: 2 67 | m_LightmapParameters: {fileID: 0} 68 | m_LightmapsBakeMode: 1 69 | m_TextureCompression: 1 70 | m_FinalGather: 0 71 | m_FinalGatherFiltering: 1 72 | m_FinalGatherRayCount: 256 73 | m_ReflectionCompression: 2 74 | m_MixedBakeMode: 2 75 | m_BakeBackend: 1 76 | m_PVRSampling: 1 77 | m_PVRDirectSampleCount: 32 78 | m_PVRSampleCount: 512 79 | m_PVRBounces: 2 80 | m_PVREnvironmentSampleCount: 256 81 | m_PVREnvironmentReferencePointCount: 2048 82 | m_PVRFilteringMode: 1 83 | m_PVRDenoiserTypeDirect: 1 84 | m_PVRDenoiserTypeIndirect: 1 85 | m_PVRDenoiserTypeAO: 1 86 | m_PVRFilterTypeDirect: 0 87 | m_PVRFilterTypeIndirect: 0 88 | m_PVRFilterTypeAO: 0 89 | m_PVREnvironmentMIS: 1 90 | m_PVRCulling: 1 91 | m_PVRFilteringGaussRadiusDirect: 1 92 | m_PVRFilteringGaussRadiusIndirect: 5 93 | m_PVRFilteringGaussRadiusAO: 2 94 | m_PVRFilteringAtrousPositionSigmaDirect: 0.5 95 | m_PVRFilteringAtrousPositionSigmaIndirect: 2 96 | m_PVRFilteringAtrousPositionSigmaAO: 1 97 | m_ExportTrainingData: 0 98 | m_TrainingDataDestination: TrainingData 99 | m_LightProbeSampleCountMultiplier: 4 100 | m_LightingDataAsset: {fileID: 0} 101 | m_UseShadowmask: 1 102 | --- !u!196 &4 103 | NavMeshSettings: 104 | serializedVersion: 2 105 | m_ObjectHideFlags: 0 106 | m_BuildSettings: 107 | serializedVersion: 2 108 | agentTypeID: 0 109 | agentRadius: 0.5 110 | agentHeight: 2 111 | agentSlope: 45 112 | agentClimb: 0.4 113 | ledgeDropHeight: 0 114 | maxJumpAcrossDistance: 0 115 | minRegionArea: 2 116 | manualCellSize: 0 117 | cellSize: 0.16666667 118 | manualTileSize: 0 119 | tileSize: 256 120 | accuratePlacement: 0 121 | debug: 122 | m_Flags: 0 123 | m_NavMeshData: {fileID: 0} 124 | --- !u!1 &755724144 125 | GameObject: 126 | m_ObjectHideFlags: 3 127 | m_CorrespondingSourceObject: {fileID: 0} 128 | m_PrefabInstance: {fileID: 0} 129 | m_PrefabAsset: {fileID: 0} 130 | serializedVersion: 6 131 | m_Component: 132 | - component: {fileID: 755724146} 133 | - component: {fileID: 755724145} 134 | m_Layer: 0 135 | m_Name: DrawGraphMonoBehaviour 136 | m_TagString: Untagged 137 | m_Icon: {fileID: 0} 138 | m_NavMeshLayer: 0 139 | m_StaticEditorFlags: 0 140 | m_IsActive: 1 141 | --- !u!114 &755724145 142 | MonoBehaviour: 143 | m_ObjectHideFlags: 3 144 | m_CorrespondingSourceObject: {fileID: 0} 145 | m_PrefabInstance: {fileID: 0} 146 | m_PrefabAsset: {fileID: 0} 147 | m_GameObject: {fileID: 755724144} 148 | m_Enabled: 1 149 | m_EditorHideFlags: 0 150 | m_Script: {fileID: 11500000, guid: de00e468778bbee4cbc437ba3012c775, type: 3} 151 | m_Name: 152 | m_EditorClassIdentifier: 153 | --- !u!4 &755724146 154 | Transform: 155 | m_ObjectHideFlags: 3 156 | m_CorrespondingSourceObject: {fileID: 0} 157 | m_PrefabInstance: {fileID: 0} 158 | m_PrefabAsset: {fileID: 0} 159 | m_GameObject: {fileID: 755724144} 160 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} 161 | m_LocalPosition: {x: 0, y: 0, z: 0} 162 | m_LocalScale: {x: 1, y: 1, z: 1} 163 | m_Children: [] 164 | m_Father: {fileID: 0} 165 | m_RootOrder: 2 166 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 167 | --- !u!1 &999488489 168 | GameObject: 169 | m_ObjectHideFlags: 0 170 | m_CorrespondingSourceObject: {fileID: 0} 171 | m_PrefabInstance: {fileID: 0} 172 | m_PrefabAsset: {fileID: 0} 173 | serializedVersion: 6 174 | m_Component: 175 | - component: {fileID: 999488492} 176 | - component: {fileID: 999488491} 177 | - component: {fileID: 999488490} 178 | m_Layer: 0 179 | m_Name: Main Camera 180 | m_TagString: MainCamera 181 | m_Icon: {fileID: 0} 182 | m_NavMeshLayer: 0 183 | m_StaticEditorFlags: 0 184 | m_IsActive: 1 185 | --- !u!81 &999488490 186 | AudioListener: 187 | m_ObjectHideFlags: 0 188 | m_CorrespondingSourceObject: {fileID: 0} 189 | m_PrefabInstance: {fileID: 0} 190 | m_PrefabAsset: {fileID: 0} 191 | m_GameObject: {fileID: 999488489} 192 | m_Enabled: 1 193 | --- !u!20 &999488491 194 | Camera: 195 | m_ObjectHideFlags: 0 196 | m_CorrespondingSourceObject: {fileID: 0} 197 | m_PrefabInstance: {fileID: 0} 198 | m_PrefabAsset: {fileID: 0} 199 | m_GameObject: {fileID: 999488489} 200 | m_Enabled: 1 201 | serializedVersion: 2 202 | m_ClearFlags: 1 203 | m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} 204 | m_projectionMatrixMode: 1 205 | m_GateFitMode: 2 206 | m_FOVAxisMode: 0 207 | m_SensorSize: {x: 36, y: 24} 208 | m_LensShift: {x: 0, y: 0} 209 | m_FocalLength: 50 210 | m_NormalizedViewPortRect: 211 | serializedVersion: 2 212 | x: 0 213 | y: 0 214 | width: 1 215 | height: 1 216 | near clip plane: 0.3 217 | far clip plane: 1000 218 | field of view: 60 219 | orthographic: 0 220 | orthographic size: 5 221 | m_Depth: -1 222 | m_CullingMask: 223 | serializedVersion: 2 224 | m_Bits: 4294967295 225 | m_RenderingPath: -1 226 | m_TargetTexture: {fileID: 0} 227 | m_TargetDisplay: 0 228 | m_TargetEye: 3 229 | m_HDR: 1 230 | m_AllowMSAA: 1 231 | m_AllowDynamicResolution: 0 232 | m_ForceIntoRT: 0 233 | m_OcclusionCulling: 1 234 | m_StereoConvergence: 10 235 | m_StereoSeparation: 0.022 236 | --- !u!4 &999488492 237 | Transform: 238 | m_ObjectHideFlags: 0 239 | m_CorrespondingSourceObject: {fileID: 0} 240 | m_PrefabInstance: {fileID: 0} 241 | m_PrefabAsset: {fileID: 0} 242 | m_GameObject: {fileID: 999488489} 243 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} 244 | m_LocalPosition: {x: 0, y: 1, z: -10} 245 | m_LocalScale: {x: 1, y: 1, z: 1} 246 | m_Children: [] 247 | m_Father: {fileID: 0} 248 | m_RootOrder: 0 249 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 250 | --- !u!1 &1671776605 251 | GameObject: 252 | m_ObjectHideFlags: 0 253 | m_CorrespondingSourceObject: {fileID: 0} 254 | m_PrefabInstance: {fileID: 0} 255 | m_PrefabAsset: {fileID: 0} 256 | serializedVersion: 6 257 | m_Component: 258 | - component: {fileID: 1671776607} 259 | - component: {fileID: 1671776606} 260 | m_Layer: 0 261 | m_Name: GameObject 262 | m_TagString: Untagged 263 | m_Icon: {fileID: 0} 264 | m_NavMeshLayer: 0 265 | m_StaticEditorFlags: 0 266 | m_IsActive: 1 267 | --- !u!114 &1671776606 268 | MonoBehaviour: 269 | m_ObjectHideFlags: 0 270 | m_CorrespondingSourceObject: {fileID: 0} 271 | m_PrefabInstance: {fileID: 0} 272 | m_PrefabAsset: {fileID: 0} 273 | m_GameObject: {fileID: 1671776605} 274 | m_Enabled: 1 275 | m_EditorHideFlags: 0 276 | m_Script: {fileID: 11500000, guid: 631b9e58f27e22847af7682b3ab09ee2, type: 3} 277 | m_Name: 278 | m_EditorClassIdentifier: 279 | updateGraph: 1 280 | --- !u!4 &1671776607 281 | Transform: 282 | m_ObjectHideFlags: 0 283 | m_CorrespondingSourceObject: {fileID: 0} 284 | m_PrefabInstance: {fileID: 0} 285 | m_PrefabAsset: {fileID: 0} 286 | m_GameObject: {fileID: 1671776605} 287 | m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} 288 | m_LocalPosition: {x: 0, y: 0, z: 0} 289 | m_LocalScale: {x: 1, y: 1, z: 1} 290 | m_Children: [] 291 | m_Father: {fileID: 0} 292 | m_RootOrder: 3 293 | m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} 294 | --- !u!1 &1778928640 295 | GameObject: 296 | m_ObjectHideFlags: 0 297 | m_CorrespondingSourceObject: {fileID: 0} 298 | m_PrefabInstance: {fileID: 0} 299 | m_PrefabAsset: {fileID: 0} 300 | serializedVersion: 6 301 | m_Component: 302 | - component: {fileID: 1778928642} 303 | - component: {fileID: 1778928641} 304 | m_Layer: 0 305 | m_Name: Directional Light 306 | m_TagString: Untagged 307 | m_Icon: {fileID: 0} 308 | m_NavMeshLayer: 0 309 | m_StaticEditorFlags: 0 310 | m_IsActive: 1 311 | --- !u!108 &1778928641 312 | Light: 313 | m_ObjectHideFlags: 0 314 | m_CorrespondingSourceObject: {fileID: 0} 315 | m_PrefabInstance: {fileID: 0} 316 | m_PrefabAsset: {fileID: 0} 317 | m_GameObject: {fileID: 1778928640} 318 | m_Enabled: 1 319 | serializedVersion: 10 320 | m_Type: 1 321 | m_Shape: 0 322 | m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} 323 | m_Intensity: 1 324 | m_Range: 10 325 | m_SpotAngle: 30 326 | m_InnerSpotAngle: 21.80208 327 | m_CookieSize: 10 328 | m_Shadows: 329 | m_Type: 2 330 | m_Resolution: -1 331 | m_CustomResolution: -1 332 | m_Strength: 1 333 | m_Bias: 0.05 334 | m_NormalBias: 0.4 335 | m_NearPlane: 0.2 336 | m_CullingMatrixOverride: 337 | e00: 1 338 | e01: 0 339 | e02: 0 340 | e03: 0 341 | e10: 0 342 | e11: 1 343 | e12: 0 344 | e13: 0 345 | e20: 0 346 | e21: 0 347 | e22: 1 348 | e23: 0 349 | e30: 0 350 | e31: 0 351 | e32: 0 352 | e33: 1 353 | m_UseCullingMatrixOverride: 0 354 | m_Cookie: {fileID: 0} 355 | m_DrawHalo: 0 356 | m_Flare: {fileID: 0} 357 | m_RenderMode: 0 358 | m_CullingMask: 359 | serializedVersion: 2 360 | m_Bits: 4294967295 361 | m_RenderingLayerMask: 1 362 | m_Lightmapping: 4 363 | m_LightShadowCasterMode: 0 364 | m_AreaSize: {x: 1, y: 1} 365 | m_BounceIntensity: 1 366 | m_ColorTemperature: 6570 367 | m_UseColorTemperature: 0 368 | m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0} 369 | m_UseBoundingSphereOverride: 0 370 | m_ShadowRadius: 0 371 | m_ShadowAngle: 0 372 | --- !u!4 &1778928642 373 | Transform: 374 | m_ObjectHideFlags: 0 375 | m_CorrespondingSourceObject: {fileID: 0} 376 | m_PrefabInstance: {fileID: 0} 377 | m_PrefabAsset: {fileID: 0} 378 | m_GameObject: {fileID: 1778928640} 379 | m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261} 380 | m_LocalPosition: {x: 0, y: 3, z: 0} 381 | m_LocalScale: {x: 1, y: 1, z: 1} 382 | m_Children: [] 383 | m_Father: {fileID: 0} 384 | m_RootOrder: 1 385 | m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0} 386 | -------------------------------------------------------------------------------- /Demo/DemoDrawGraph.cs: -------------------------------------------------------------------------------- 1 | 2 | using UnityGraphs; 3 | using System.Linq; 4 | using UnityEngine; 5 | 6 | namespace UnityGraphs.Demo 7 | { 8 | // Graphs can be called while play mode is off, by updating the MonoBehaviour transform for example 9 | // or within another editor script or a window 10 | [ExecuteInEditMode] 11 | 12 | public class DemoDrawGraph : MonoBehaviour 13 | { 14 | public bool updateGraph = true; 15 | 16 | void Start() 17 | { 18 | // Supported types: float, Quaternion, Vector3 19 | DrawGraph.Add( "Vec3Test", Vector3.zero ) 20 | // Use System.Linq::ForEach( ... ) to assing group style values 21 | .ForEach( graph => graph 22 | .SetLineWidth( 2f ) 23 | .SetGraphHeight( 120f ) 24 | .SetStepSize( 0.25f ) 25 | .SetLimits( -2f, 2f ) 26 | ); 27 | 28 | DrawGraph.Add( "Empty graph test", 0f ); 29 | 30 | //Combine two graphs in the same view 31 | DrawGraph.Get("c1").SetGroup("GroupC"); 32 | DrawGraph.Get("c2").SetGroup("GroupC"); 33 | 34 | } 35 | 36 | void Update() 37 | { 38 | if( ! updateGraph ) return; 39 | 40 | // Add values to be drawn in the graph 41 | // ( seperate graphs, not in a group ) 42 | // Since no settings are assigned, default style 43 | // values are used for A & B 44 | DrawGraph.Add( "A", Random.value ); 45 | DrawGraph.Add( "B", Random.value ); 46 | 47 | // Automaticlly create a group for Vector3 and 48 | // assign each of the x,y,z componenets into the 49 | // same group 50 | DrawGraph.Add( "Vec3Test", new Vector3( 51 | Mathf.Sin( Time.time / 10 ), 52 | Mathf.Tan( 1 / Time.time ), 53 | Mathf.Cos( Time.time / 10 ) 54 | ) ); 55 | 56 | // c1 & c2 will be drawn in the same group 57 | DrawGraph.Add( "c1", Mathf.Sin( 1 / Time.time ) ); 58 | 59 | // If you are feeling particulary lazy, style values 60 | // can be assined during update 61 | DrawGraph.Add( "Random red", Random.value ).SetLengthLimit( 250 ) 62 | .SetColor( Color.red ) 63 | .SetLineWidth( 4f ) 64 | .SetStepSize( 2f ); 65 | 66 | // Grouped items don't need to be updated in the same place or time 67 | if( Random.value > 0.7 ) 68 | { 69 | // c1 & c2 will be drawn in the same group 70 | DrawGraph.Add( "c2", Mathf.Tan( 1 / Time.time ) ); 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /Demo/pic1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nukadelic/UnityGraphs/5f8332cc88c8ca2fd2998c3dcb1a6ba0a0a1582d/Demo/pic1.png -------------------------------------------------------------------------------- /Demo/pic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nukadelic/UnityGraphs/5f8332cc88c8ca2fd2998c3dcb1a6ba0a0a1582d/Demo/pic2.png -------------------------------------------------------------------------------- /Editor/GraphRenderer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | 6 | namespace UnityGraphs 7 | { 8 | public static class GraphRenderer 9 | { 10 | ////////////////////////////////// 11 | // 12 | // Graph GUI wrappers 13 | // 14 | 15 | public static void RenderGUI( List group ) 16 | { 17 | float height = group._GetHeight(); 18 | 19 | using( new EditorGUI.DisabledGroupScope( true ) ) 20 | { 21 | GUILayout.TextArea( "", GUILayout.Height( height ) ); 22 | } 23 | 24 | Rect area = GUILayoutUtility.GetLastRect(); 25 | 26 | if( Event.current.type == EventType.Repaint ) 27 | 28 | RenderGraphGroup( area, group ); 29 | 30 | var mouse_area = new Rect( area.x, area.y + area.height / 2, area.width, area.height + 30 ); 31 | 32 | var mouse_over = mouse_area.Contains( Event.current.mousePosition ); 33 | 34 | var scroll = group.Select( data => data._.scroll ).Aggregate( (a,b) => a > b ? a : b ); 35 | 36 | var ratio = group.Select( data => data._.overflow_ratio ).Aggregate( (a,b) => a < b ? a : b ); 37 | 38 | if( mouse_over && scroll > -1 ) scroll = GraphScrollbarGUI( area, scroll, ratio ); 39 | 40 | group.ForEach( data => data._.scroll = scroll ); 41 | 42 | GUILayout.Space( 14 ); 43 | } 44 | 45 | public static float GraphScrollbarGUI( Rect area, float scroll, float ratio = 0.5f ) 46 | { 47 | return GUI.HorizontalScrollbar( new Rect( area.x + 1, area.yMax, area.width - 2, 16 ), scroll, ratio, 0, 1 ); 48 | } 49 | 50 | ///////////////////////// 51 | // 52 | // Render Handles 53 | // 54 | 55 | /// TODO: add method to override graph light / dark style 56 | static bool IsDark { get { return true; }} // EditorGUIUtility.isProSkin; } } 57 | 58 | static float GridSize = 10f; 59 | 60 | static Color GetAxisColor() { return IsDark ? new Color( 1, 1, 1, 0.5f ) : new Color( 0, 0, 0, 0.5f ); } 61 | static Color GetGridColor() { return IsDark ? new Color( 1, 1, 1, 0.2f ) : new Color( 0, 0, 0, 0.2f ); } 62 | 63 | public static void RenderLabel( string label, float x, float y, Color color, TextAnchor anchor = TextAnchor.UpperLeft, bool background = false, int fontSize = -1 ) 64 | { 65 | RenderLabel( label, x, y, "#" + ColorUtility.ToHtmlStringRGBA( color ), anchor, background, fontSize ); 66 | } 67 | 68 | public static void RenderLabel( string label, float x, float y, string cssColor = "", TextAnchor anchor = TextAnchor.UpperLeft, bool background = false, int fontSize = -1 ) 69 | { 70 | GUIStyle style = new GUIStyle( GUI.skin.label ); 71 | Color color; 72 | 73 | if( ! string.IsNullOrEmpty( cssColor ) && ColorUtility.TryParseHtmlString( cssColor, out color ) ) 74 | { 75 | style.normal.textColor = color; 76 | } 77 | 78 | style.alignment = anchor; 79 | 80 | if( fontSize > -1 ) style.fontSize = fontSize; 81 | 82 | if( background && ColorUtility.TryParseHtmlString( IsDark ? "#00000022" : "#FFFFFF22", out color ) ) 83 | { 84 | var size = style.CalcSize( new GUIContent( label ) ); 85 | 86 | Rect rect = new Rect( x - 4, y - 1, size.x + 8, size.y + 1 ); 87 | 88 | Handles.color = color; 89 | 90 | Handles.DrawSolidRectangleWithOutline( rect, Color.white, Color.clear ); 91 | } 92 | 93 | Handles.Label( new Vector3( x, y, 0 ), label, style ); 94 | } 95 | 96 | public static void RenderLine( float x1, float y1, float x2, float y2, Color c ) 97 | { 98 | Handles.color = c; 99 | Handles.DrawLine( new Vector3( x1, y1, 0 ) , new Vector3( x2, y2, 0 ) ); 100 | } 101 | 102 | public static void RenderLines( float width, Color c, Vector3[] points ) 103 | { 104 | Handles.color = c; 105 | Handles.DrawAAPolyLine( width, points ); 106 | } 107 | 108 | public static void RenderGraphData( Rect area, GraphItem data ) 109 | { 110 | RenderGraphGroup( area, new List { data } ); 111 | } 112 | 113 | public static void RenderGraphGroup( Rect area, List data ) 114 | { 115 | Vector2 mouse = Event.current.mousePosition; 116 | float DPI = EditorGUIUtility.pixelsPerPoint; 117 | 118 | data.ForEach( d => d._.color = d._.color == default( Color ) ? ( IsDark ? Color.white : Color.black ) : d._.color ); 119 | 120 | Handles.BeginGUI(); 121 | 122 | Handles.color = IsDark ? Color.black : Color.white; 123 | Handles.DrawSolidRectangleWithOutline( area, Handles.color, Color.grey ); 124 | 125 | float grid_offset = - Mathf.Clamp01( data[ 0 ]._.scroll ) * area.width; 126 | grid_offset = grid_offset % GridSize; 127 | if( grid_offset < 0 ) grid_offset += GridSize; 128 | 129 | string group = data[ 0 ]._.group; 130 | 131 | for( var x = area.x + grid_offset; x < area.xMax; x += GridSize ) 132 | RenderLine( x, area.y, x, area.yMax, GetGridColor() ); 133 | 134 | for( var y = area.y; y < area.yMax; y += GridSize ) 135 | RenderLine( area.x, y, area.xMax, y, GetGridColor() ); 136 | 137 | bool mouseInBounds = area.Contains( mouse ); 138 | 139 | if( mouseInBounds ) RenderLine( mouse.x, area.y, mouse.x, area.yMax, GetAxisColor() ); 140 | 141 | for( var i = 0; i < data.Count; ++ i ) 142 | { 143 | // poker face variables 144 | int totalPoints = data[ i ]._.points.Count; 145 | float step = area.width / totalPoints; 146 | step = step < data[ i ]._.step_size ? data[ i ]._.step_size : step; 147 | var data_scroll = data[ i ]._.scroll < 0 ? 0 : data[ i ]._.scroll; 148 | int visiblePoints = Mathf.FloorToInt( area.width / step ); 149 | float scroll_space = ( totalPoints - 1 ) * step - area.width; 150 | float position = scroll_space * data_scroll; 151 | float delta_space = position % step; 152 | bool overflow = scroll_space > 0; 153 | 154 | // Default starting indexes for [ j ] loop 155 | int startIndex = 0; 156 | int lastIndexPlus1 = totalPoints; 157 | 158 | // If graph bigger then the draw area, adjust the indexes 159 | if( overflow ) 160 | { 161 | // Enable scrolling & auto scroll to the end 162 | if( data[ i ]._.scroll < 0 ) data[ i ]._.scroll = 1; 163 | } 164 | // If there is no overflow, disable scrolling 165 | else data[ i ]._.scroll = -1; 166 | 167 | startIndex = Mathf.RoundToInt( ( totalPoints - visiblePoints ) * data_scroll ); 168 | lastIndexPlus1 = visiblePoints + startIndex; 169 | 170 | var visible_points = data[ i ]._.points.GetRange( startIndex, visiblePoints ); 171 | 172 | // quick fix ? 173 | if( visible_points.Count == 0 ) visible_points = data[ i ]._.points; 174 | 175 | // Calculate point limits 176 | float maxValue = 1f; 177 | float minValue = 0f; 178 | 179 | if( visible_points.Count > 2 ) 180 | { 181 | maxValue = visible_points.Aggregate( (a,b) => a > b ? a : b ); 182 | minValue = visible_points.Aggregate( (a,b) => a < b ? a : b ); 183 | } 184 | 185 | if( data[ i ]._.limits != Vector2.zero ) 186 | { 187 | minValue = data[ i ]._.limits.x; 188 | maxValue = data[ i ]._.limits.y; 189 | } 190 | 191 | int mouseIndex = -1; 192 | 193 | if( mouseInBounds ) 194 | { 195 | float mouse_shift = position; 196 | 197 | float relative_mouse = mouse.x + ( overflow ? mouse_shift : 0 ); 198 | 199 | mouseIndex = Mathf.RoundToInt( ( relative_mouse - area.x ) / step ); 200 | } 201 | 202 | bool draw_hover = false; 203 | float hover_value = 0f; 204 | Vector2 point = Vector2.zero; 205 | 206 | var points = new List(); 207 | 208 | for( var j = startIndex; j < lastIndexPlus1; ++ j ) 209 | { 210 | int index_shift = overflow ? startIndex : 0; 211 | float x_shift = overflow ? - delta_space : 0; 212 | float value = data[ i ]._.points[ j ]; 213 | 214 | /// Normalize 215 | float x = ( j - index_shift ) * step; 216 | float y = 1 - ( value - minValue ) / ( maxValue - minValue ); 217 | 218 | // Prevent render overflow 219 | y = Mathf.Clamp01( y ); 220 | 221 | /// Relative to draw area 222 | x += area.x + x_shift; 223 | y = y * area.height + area.y; 224 | 225 | 226 | points.Add( new Vector3( x, y ) ); 227 | 228 | /// Display point data at mouse X 229 | if( mouseInBounds && mouseIndex == j ) 230 | { 231 | point = new Vector2( x, y ); 232 | draw_hover = true; 233 | hover_value = value; 234 | } 235 | } 236 | 237 | RenderLines( data[ i ]._.line_width, data[ i ]._.color, points.ToArray() ); 238 | 239 | // Hover values and display their values 240 | if( draw_hover ) 241 | { 242 | Handles.color = data[ i ]._.color; 243 | Handles.DrawSolidDisc( new Vector3( point.x,point.y,0 ), Vector3.forward, 5 ); 244 | 245 | string s = hover_value.ToString("N3"); 246 | RenderLabel( s, area.xMax - 9 * s.Length, area.y + 5 + 15 * i, data[ i ]._.color ); 247 | } 248 | // Display latest values on multiple items graph 249 | else if( ! draw_hover && data.Count > 1 ) 250 | { 251 | 252 | } 253 | // Display limits if there is only single item in graph ( groups not supported ) 254 | else if( data.Count == 1 && ! mouseInBounds ) 255 | { 256 | string s1 = maxValue.ToString("N3"); 257 | RenderLabel( s1, area.xMax - 9 * s1.Length, area.y + 5, GetAxisColor() ); 258 | string s2 = minValue.ToString("N3"); 259 | RenderLabel( s2, area.xMax - 9 * s2.Length, area.yMax - 20, GetAxisColor() ); 260 | } 261 | } 262 | 263 | if( data.Count > 1 && data[ 0 ].inGroup ) for( var i = 0; i < data.Count; ++ i ) 264 | { 265 | string title = data[ i ]._.title; 266 | 267 | if( title.IndexOf( group ) == 0 && title != group ) 268 | 269 | title = title.Remove( 0, group.Length ); 270 | 271 | RenderLabel( title, area.x + 6f , area.y + 3f + i * 14, data[ i ]._.color ); 272 | } 273 | 274 | Handles.EndGUI(); 275 | } 276 | } 277 | } -------------------------------------------------------------------------------- /Editor/GraphStrings.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace UnityGraphs 5 | { 6 | public static class GraphStrings 7 | { 8 | 9 | public static GUIContent MenuAutoOpen, MenuDataLength, MenuHeaderValues, MenuClearAll, 10 | MenuExpandAll, MenuCollapseAll, MenuDefaultItems; 11 | 12 | public static void MenuStrings() 13 | { 14 | if( MenuAutoOpen != null ) return; 15 | 16 | MenuAutoOpen = EditorGUIUtility.TrTextContent( $"Auto Open on {MainFunc}" ); 17 | MenuDataLength = EditorGUIUtility.TrTextContent( "Display graph data length" ); 18 | MenuHeaderValues = EditorGUIUtility.TrTextContent( "Preview values in header" ); 19 | MenuClearAll = EditorGUIUtility.TrTextContent( "Clear All" ); 20 | MenuExpandAll = EditorGUIUtility.TrTextContent( "Expend All" ); 21 | MenuCollapseAll = EditorGUIUtility.TrTextContent( "Collapse All" ); 22 | MenuDefaultItems = EditorGUIUtility.TrTextContent( "Default Items:" ); 23 | } 24 | 25 | static readonly string MainFunc = "DrawGraph.Add( ... )"; 26 | 27 | public static readonly string NoData = "No data" 28 | + "\n\n* " + $"Use {MainFunc} to draw graphs" 29 | + "\n\n* " + "To view values in edit mode, add [ExecuteInEditMode] to MonoBehaviour"; 30 | 31 | public static readonly string AutoOpened = 32 | "Window was auto opened, you can disable this in tab menu"; 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /Editor/GraphWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using UnityEditor; 5 | using UnityEngine; 6 | 7 | namespace UnityGraphs 8 | { 9 | [InitializeOnLoad] 10 | class DrawGraphHook 11 | { 12 | static DrawGraphHook () 13 | { 14 | EditorApplication.update += () => DrawGraph.onInput += delegate 15 | { 16 | if( ! GraphWindow.GetData().autoOpen ) return; 17 | 18 | if( GraphWindow.instance == null ) 19 | { 20 | if( ! GraphWindow.DismissAutoOpen && GraphWindow.GetData().autoOpenCounter < 5 ) 21 | { 22 | GraphWindow.GetData().autoOpenCounter ++ ; 23 | 24 | GraphWindow.WasAutoOpen = true; 25 | } 26 | 27 | GraphWindow.OpenWindow(); 28 | } 29 | }; 30 | } 31 | } 32 | 33 | public class GraphWindow : EditorWindow, IHasCustomMenu 34 | { 35 | static string TITLE = "Graph Window"; 36 | 37 | public static GraphWindow instance; 38 | 39 | [MenuItem("Window/Analysis/Graph Window")] 40 | public static void OpenWindow() 41 | { 42 | instance = GetWindow( false, TITLE, true ); 43 | instance.minSize = new Vector2( 320, 220 ); 44 | } 45 | 46 | public static bool WasAutoOpen = false; 47 | public static bool DismissAutoOpen = false; 48 | 49 | public void AddItemsToMenu( GenericMenu menu ) 50 | { 51 | GraphStrings.MenuStrings(); 52 | 53 | menu.AddItem(GraphStrings.MenuAutoOpen, 54 | data.autoOpen, () => { data.autoOpen = ! data.autoOpen; SaveData(); } ); 55 | 56 | menu.AddItem(GraphStrings.MenuDataLength, 57 | data.showCounts, () => { data.showCounts = ! data.showCounts; SaveData(); } ); 58 | 59 | menu.AddItem(GraphStrings.MenuHeaderValues, 60 | data.showValues, ()=> { data.showValues = ! data.showValues; SaveData(); } ); 61 | 62 | menu.AddSeparator(""); 63 | 64 | if( DrawGraph.Singelton().hasData ) 65 | menu.AddItem( GraphStrings.MenuClearAll, false, ()=> DrawGraph.Clear() ); 66 | else 67 | menu.AddDisabledItem( GraphStrings.MenuClearAll ); 68 | 69 | menu.AddSeparator(""); 70 | 71 | menu.AddItem(GraphStrings.MenuExpandAll, false, () => 72 | DrawGraph.Singelton().data.Values.ToList().ForEach( item => item._.visible = true ) ); 73 | 74 | menu.AddItem(GraphStrings.MenuCollapseAll, false, () => 75 | DrawGraph.Singelton().data.Values.ToList().ForEach( item => item._.visible = false ) ); 76 | 77 | menu.AddSeparator(""); 78 | 79 | menu.AddDisabledItem( GraphStrings.MenuDefaultItems ); 80 | } 81 | 82 | //////////////////////////////////// 83 | // 84 | // Editor preference 85 | // 86 | 87 | [Serializable] 88 | public class GraphWindowEditorData 89 | { 90 | public Vector2 scroll = new Vector2(); 91 | public bool autoOpen = true; 92 | public int autoOpenCounter = 0; 93 | public bool autoFocus = false; 94 | public bool showCounts = false; 95 | public bool showValues = true; 96 | } 97 | 98 | static GraphWindowEditorData data; 99 | 100 | public static GraphWindowEditorData GetData() 101 | { 102 | if( data == null ) LoadData(); 103 | 104 | return data; 105 | } 106 | 107 | static string GetDataKey() { return "GraphWindowEditorData_EditorPrefsKey"; } 108 | 109 | static void LoadData() 110 | { 111 | data = new GraphWindowEditorData(); 112 | 113 | var json = EditorPrefs.GetString( GetDataKey() , ""); 114 | 115 | if ( json != "") data = JsonUtility.FromJson( json ); 116 | 117 | else SaveData(); 118 | } 119 | 120 | static void SaveData() 121 | { 122 | var json = JsonUtility.ToJson( data ); 123 | EditorPrefs.SetString( GetDataKey(), json ); 124 | } 125 | 126 | ////////////////////////////////////// 127 | // 128 | // Event Cycle 129 | // 130 | 131 | bool focused = false; 132 | 133 | void OnFocus() { focused = true; } 134 | 135 | void OnLostFocus() { focused = false; } 136 | 137 | void OnEnable() 138 | { 139 | LoadData(); 140 | 141 | DrawGraph.onInput += OnInput; 142 | // DrawGraph.onData += OnData; 143 | } 144 | 145 | void OnDisable() 146 | { 147 | SaveData(); 148 | 149 | DrawGraph.Clear(); 150 | 151 | DrawGraph.onInput -= OnInput; 152 | } 153 | 154 | public bool needsRepaint = false; 155 | 156 | void OnInput() 157 | { 158 | needsRepaint = true; 159 | 160 | if( data.autoFocus && ! focused ) Focus(); 161 | } 162 | 163 | int repaintTimer = 0; 164 | 165 | void Update() 166 | { 167 | if( needsRepaint || ( focused && ++ repaintTimer > 5 ) ) 168 | { 169 | needsRepaint = false; 170 | repaintTimer = 0; 171 | Repaint(); 172 | } 173 | } 174 | 175 | void OnInspectorUpdate() { Repaint(); } 176 | 177 | /// https://unitylist.com/p/5c3/Unity-editor-icons 178 | GUIContent IconGUI( string name, bool darkSkinSupported = true ) 179 | { 180 | string prefix = darkSkinSupported ? ( EditorGUIUtility.isProSkin ? "d_" : "" ) : ""; 181 | return new GUIContent( EditorGUIUtility.IconContent( prefix + name ) ); 182 | } 183 | 184 | void OnGUI() 185 | { 186 | if( ! DrawGraph.Singelton().hasData ) 187 | { 188 | EditorGUILayout.HelpBox( GraphStrings.NoData, MessageType.Info ); 189 | return; 190 | } 191 | 192 | if( WasAutoOpen && ! DismissAutoOpen ) 193 | { 194 | using( new GUILayout.HorizontalScope() ) 195 | { 196 | EditorGUILayout.HelpBox( GraphStrings.AutoOpened, MessageType.Info ); 197 | 198 | if( GUILayout.Button( "X" , GUILayout.Width( 24 ), GUILayout.Height( 38 ) ) ) 199 | 200 | DismissAutoOpen = true; 201 | 202 | } 203 | } 204 | 205 | float ppp = EditorGUIUtility.pixelsPerPoint; 206 | bool isWide = Screen.width / ppp > 400; 207 | 208 | using( var scope = new GUILayout.ScrollViewScope( data.scroll ) ) 209 | { 210 | GUILayout.Space( 10 ); 211 | 212 | data.scroll = scope.scrollPosition; 213 | 214 | var groups = DrawGraph.Singelton().data._GetDataWGroups(); 215 | var groups_count = groups.Count; 216 | 217 | for( var i = 0; i < groups_count; ++i ) 218 | { 219 | if( isWide && groups_count > 1 && ! ( i == groups_count - 1 && groups_count % 2 == 1 ) ) 220 | { 221 | if( i % 2 == 0 ) GUILayout.BeginHorizontal(); 222 | 223 | GraphGroupGUI( groups[ i ] , Screen.width / ppp / 2 - 5 ); 224 | 225 | if( i % 2 != 0 ) GUILayout.EndHorizontal(); 226 | } 227 | 228 | else GraphGroupGUI( groups[ i ] , Screen.width ); 229 | } 230 | 231 | var graphs = DrawGraph.Singelton().data._GetDataWoGroups(); 232 | var graphs_count = graphs.Count; 233 | 234 | for( var i = 0; i < graphs_count; ++i ) 235 | { 236 | if( isWide && graphs_count > 1 && ! ( i == graphs_count - 1 && graphs_count % 2 == 1 ) ) 237 | { 238 | if( i % 2 == 0 ) GUILayout.BeginHorizontal(); 239 | 240 | GraphGUI( graphs[ i ], Screen.width / ppp / 2 - 5 ); 241 | 242 | if( i % 2 != 0 ) GUILayout.EndHorizontal(); 243 | } 244 | 245 | else GraphGUI( graphs[ i ], Screen.width ); 246 | } 247 | 248 | GUILayout.Space( 20 ); 249 | } 250 | } 251 | 252 | void GraphGUI( GraphItem graph, float width ) 253 | { 254 | var foldoutHeader = new GUIStyle( EditorStyles.foldoutHeader ); 255 | // foldoutHeader.fixedWidth = width - 10; 256 | // foldoutHeader.fixedWidth = Screen.width / EditorGUIUtility.pixelsPerPoint - 25; 257 | 258 | var graphContianerLayout = GUILayout.MaxWidth( Screen.width ); 259 | 260 | var group = new List { graph } ; 261 | 262 | using( new GUILayout.VerticalScope( GUILayout.MaxWidth( width ) ) ) 263 | { 264 | var header = graph._.title; 265 | 266 | if( data.showValues ) header += " " + graph._GetLastValues(); 267 | if( data.showCounts ) header += " " + graph._GetCounts(); 268 | 269 | graph._.visible = EditorGUILayout.BeginFoldoutHeaderGroup( graph._.visible, header, foldoutHeader ); 270 | EditorGUILayout.EndFoldoutHeaderGroup(); 271 | 272 | if( graph._.visible ) // using( new GUILayout.HorizontalScope( graphContianerLayout ) ) 273 | 274 | GraphRenderer.RenderGUI( group ); 275 | 276 | else GUILayout.Space( 4 ); 277 | } 278 | } 279 | 280 | void GraphGroupGUI( List group, float width ) 281 | { 282 | var foldoutHeader = new GUIStyle( EditorStyles.foldoutHeader ); 283 | // foldoutHeader.fixedWidth = width - 10; 284 | // foldoutHeader.fixedWidth = Screen.width / EditorGUIUtility.pixelsPerPoint - 25; 285 | 286 | var graphContianerLayout = GUILayout.MaxWidth( Screen.width ); 287 | 288 | using( new GUILayout.VerticalScope( GUILayout.MaxWidth( width ) ) ) 289 | { 290 | var header = group[ 0 ]._.group; 291 | 292 | if( data.showCounts ) header += " " + group._GetCounts(); 293 | if( data.showValues ) header += " " + group._GetLastValues(); 294 | 295 | var visible = group._IsVisible(); 296 | 297 | visible = EditorGUILayout.BeginFoldoutHeaderGroup( visible, header, foldoutHeader ); 298 | EditorGUILayout.EndFoldoutHeaderGroup(); 299 | group._SetVisible( visible ); 300 | 301 | if( visible ) // using( new GUILayout.HorizontalScope( graphContianerLayout ) ) 302 | 303 | GraphRenderer.RenderGUI( group ); 304 | 305 | else GUILayout.Space( 4 ); 306 | } 307 | } 308 | } 309 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity Draw Graphs 2 | 3 | Supports: 2019.1 + 4 | 5 | Usage: 6 | ``` 7 | // string title, float value, Color color = default 8 | DrawGraph.Add( "title", 33.3f ); 9 | ``` 10 | Supports: Vector3, Vector2, Quaternion, float 11 | 12 | Open in application top level menu: `Window > Analysis > Graph` 13 | 14 | See Demo Scene 15 | 16 | ![](Demo/pic1.png?raw=true) 17 | 18 | Access window settings: 19 | 20 | ![](Demo/pic2.png?raw=true) 21 | -------------------------------------------------------------------------------- /Scripts/DrawGraph.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEngine; 4 | using UnityGraphs; 5 | 6 | [ExecuteInEditMode] 7 | // Keep class at top level namespace for ease of use 8 | public class DrawGraph : MonoBehaviour 9 | { 10 | static readonly string GameObjectName = "DrawGraphMonoBehaviour"; 11 | 12 | public static event System.Action onData; 13 | public static event System.Action onInput; 14 | 15 | static void InitEvents() 16 | { 17 | if( onInput != null ) return; 18 | onInput += () => {}; 19 | } 20 | 21 | public Dictionary data; 22 | 23 | void OnEanble() 24 | { 25 | if( data == null ) data = new Dictionary(); 26 | } 27 | 28 | 29 | static DrawGraph instance; 30 | public static DrawGraph Singelton() 31 | { 32 | if( instance == null ) 33 | { 34 | InitEvents(); 35 | 36 | instance = GameObject.Find( GameObjectName )?.GetComponent(); 37 | 38 | if( instance == null ) 39 | { 40 | var go = new GameObject( GameObjectName ); 41 | 42 | go.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy; 43 | 44 | instance = go.AddComponent(); 45 | } 46 | 47 | instance.data = new Dictionary(); 48 | } 49 | 50 | return instance; 51 | } 52 | 53 | public bool hasData { get { return data != null && data.Count > 0; } } 54 | 55 | static GraphItem ReadOrWrite( string title, float value = float.NaN, Color color = default, string group = "" ) 56 | { 57 | var data = Singelton().data; 58 | 59 | if( ! data.ContainsKey( title ) ) data.Add( title, new GraphItem( title ) ); 60 | 61 | var item = data[ title ]; 62 | 63 | if( ! object.Equals( color, default( Color )) ) item._.color = color; 64 | 65 | if( ! string.IsNullOrEmpty( group ) ) item._.group = group; 66 | 67 | if( ! float.IsNaN( value ) ) item._.points.Add( value ); 68 | 69 | while( item._.points.Count > item._.max_points ) item._.points.RemoveAt( 0 ); 70 | 71 | return item; 72 | } 73 | 74 | public static List GetData( string title ) 75 | { 76 | return ReadOrWrite( title )._.points; 77 | } 78 | 79 | public static List Add( string title, Quaternion rotation ) 80 | { 81 | if( ! Singelton().data.ContainsKey( title ) ) 82 | { 83 | ReadOrWrite( title + "X" ).SetColor( Color.red ); 84 | ReadOrWrite( title + "Y" ).SetColor( Color.green ); 85 | ReadOrWrite( title + "Z" ).SetColor( Color.cyan ); 86 | ReadOrWrite( title + "W" ).SetColor( Color.yellow ) 87 | 88 | .SetGraphHeight( 65 ); 89 | 90 | foreach( var s in new string[] { "X", "Y", "Z", "W" } ) 91 | { 92 | ReadOrWrite( title + s ) 93 | .SetGroup( title ) 94 | .SetStepSize( 0.5f ); 95 | } 96 | } 97 | 98 | var points = new List { 99 | ReadOrWrite( title + "X", rotation.x ), 100 | ReadOrWrite( title + "Y", rotation.y ), 101 | ReadOrWrite( title + "Z", rotation.z ), 102 | ReadOrWrite( title + "W", rotation.w ) 103 | }; 104 | 105 | onInput?.Invoke(); 106 | onData?.Invoke( new float[] { rotation.x, rotation.y, rotation.z, rotation.w } ); 107 | 108 | return points; 109 | } 110 | 111 | public static List Add( string title, Vector3 vector ) 112 | { 113 | if( ! Singelton().data.ContainsKey( title ) ) 114 | { 115 | ReadOrWrite( title + "X" ).SetGroup( title ).SetColor( Color.red ); 116 | ReadOrWrite( title + "Y" ).SetGroup( title ).SetColor( Color.green ); 117 | ReadOrWrite( title + "Z" ).SetGroup( title ).SetColor( Color.cyan ) 118 | 119 | .SetGraphHeight( 65 ); 120 | 121 | foreach( var s in new string[] { "X", "Y", "Z" } ) 122 | { 123 | ReadOrWrite( title + s ) 124 | .SetGroup( title ) 125 | .SetStepSize( 0.5f ); 126 | } 127 | } 128 | 129 | var points = new List { 130 | ReadOrWrite( title + "X", vector.x ), 131 | ReadOrWrite( title + "Y", vector.y ), 132 | ReadOrWrite( title + "Z", vector.z ) 133 | }; 134 | 135 | onInput?.Invoke(); 136 | onData?.Invoke( new float[] { vector.x, vector.y, vector.z } ); 137 | 138 | return points; 139 | } 140 | 141 | public static GraphItem Add( string title, float value ) 142 | { 143 | var output = ReadOrWrite( title, value ); 144 | 145 | onInput?.Invoke(); 146 | onData?.Invoke( new float[] { value } ); 147 | 148 | return output; 149 | } 150 | 151 | public static GraphItem Add( string title, float value, Color color = default ) 152 | { 153 | var output = ReadOrWrite( title, value, color ); 154 | 155 | onInput?.Invoke(); 156 | onData?.Invoke( new float[] { value } ); 157 | 158 | return output; 159 | } 160 | 161 | public static GraphItem Get( string title ) 162 | { 163 | return ReadOrWrite( title ); 164 | } 165 | 166 | // Clear everything 167 | public static void Clear() 168 | { 169 | Singelton().data = new Dictionary(); 170 | 171 | onInput?.Invoke(); 172 | } 173 | 174 | // Clear group or item 175 | public static void Clear( string title ) 176 | { 177 | var data = Singelton().data; 178 | 179 | if( ! data.ContainsKey( title ) ) 180 | { 181 | foreach( var item in data.Values.Where( item => item._.group == title ).ToArray() ) 182 | 183 | data.Remove( item._.title ); 184 | 185 | onInput?.Invoke(); 186 | 187 | return; 188 | } 189 | 190 | data.Remove( title ); 191 | 192 | onInput?.Invoke(); 193 | } 194 | } -------------------------------------------------------------------------------- /Scripts/GraphItem.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | namespace UnityGraphs 4 | { 5 | [System.Serializable] 6 | public class GraphItem 7 | { 8 | /// Variables data. Avoid using this when possible 9 | public ItemData _; 10 | /// Must declare title 11 | public GraphItem( string title ) { this._ = new ItemData { title = title }; } 12 | /// Is this item part of a group 13 | public bool inGroup { get { return ! string.IsNullOrEmpty( _.group ); } } 14 | /// Clear group from this item 15 | public GraphItem RemoveFromGroup() { _.group = ""; return this; } 16 | public GraphItem SetColor( Color c ) { _.color = c; return this; } 17 | /// Use this to combine multiple items in the same graph 18 | public GraphItem SetGroup( string s ) { _.group = s; return this; } 19 | /// Set minimum step size 20 | public GraphItem SetStepSize( float f ) { _.step_size = f; return this; } 21 | /// Line thickness 22 | public GraphItem SetLineWidth( float f ) { _.line_width = f; return this; } 23 | /// Set single item render height, or in a group use the max value 24 | public GraphItem SetGraphHeight( float f ) { _.graph_height = f; return this; } 25 | /// If the limit is reached the graph will pop the first element to avoid exceeding 26 | public GraphItem SetLengthLimit( int i ) { _.max_points = i; return this; } 27 | /// Limit graph min & max values 28 | public GraphItem SetLimits( float min, float max ) { _.limits = new Vector2( min, max ); return this; } 29 | } 30 | } -------------------------------------------------------------------------------- /Scripts/GraphItemExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace UnityGraphs 5 | { 6 | public static class GraphItemExtension 7 | { 8 | public static bool _IsVisible( this List group ) 9 | { 10 | return group.Where( item => item._.visible ).Count() > 0; 11 | } 12 | 13 | public static void _SetVisible( this List group, bool value ) 14 | { 15 | group.ForEach( item => item._.visible = value ); 16 | } 17 | 18 | public static float _GetHeight( this List group ) 19 | { 20 | return group.Select( d => d._.graph_height ).Aggregate( (a,b) => a > b ? a : b ); 21 | } 22 | 23 | public static string _GetCounts( this List group ) 24 | { 25 | return "(" + string.Join( ", ", group.Select( item => item._.points.Count ) ) + ")"; 26 | } 27 | 28 | public static string _GetLastValues( this List group ) 29 | { 30 | return "(" + string.Join( ", ", group.Select( graph => graph._.points.LastOrDefault().ToString("N2") ) ) + ")"; 31 | } 32 | 33 | public static string _GetLastValues( this GraphItem graph ) 34 | { 35 | return "(" + graph._.points.LastOrDefault().ToString("N2") + ")"; 36 | } 37 | 38 | public static string _GetCounts( this GraphItem graph ) 39 | { 40 | return "(" + graph._.points.Count + ")"; 41 | } 42 | 43 | /// Get list of groups of data items 44 | public static List> _GetDataWGroups( this Dictionary data ) 45 | { 46 | return data 47 | .Where( kv => kv.Value.inGroup ) 48 | .GroupBy( kv => kv.Value._.group ) 49 | .ToDictionary( 50 | grp => grp.Key, 51 | grp => grp.ToList() ) 52 | .Select( grp => grp.Value 53 | .Select( kv => kv.Value ) 54 | .ToList() ) 55 | .ToList(); 56 | } 57 | 58 | /// Get data items without a group 59 | public static List _GetDataWoGroups( this Dictionary data ) 60 | { 61 | return data.Where( kv => ! kv.Value.inGroup ).Select( kv => kv.Value ).ToList(); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /Scripts/ItemData.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections.Generic; 3 | 4 | namespace UnityGraphs 5 | { 6 | [System.Serializable] 7 | public class ItemData 8 | { 9 | public float overflow_ratio = 0.5f; 10 | public float line_width = 1.5f; 11 | public string group = ""; 12 | public string title = ""; 13 | public List points = new List(); 14 | public Color color = default; 15 | public float scroll = 0; 16 | public float step_size = 5f; 17 | public bool visible = true; 18 | public float graph_height = 50; 19 | public int max_points = 1000; 20 | public Vector2 limits = Vector3.zero; 21 | } 22 | } 23 | --------------------------------------------------------------------------------