| 1 | /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ |
| 2 | /* If you are missing that file, acquire a complete release at teeworlds.com. */ |
| 3 | |
| 4 | #include "graph.h" |
| 5 | |
| 6 | #include <base/str.h> |
| 7 | #include <base/time.h> |
| 8 | |
| 9 | #include <engine/graphics.h> |
| 10 | #include <engine/textrender.h> |
| 11 | |
| 12 | #include <limits> |
| 13 | |
| 14 | CGraph::CGraph(int MaxEntries, int Precision, bool SummaryStats) : |
| 15 | m_Entries(MaxEntries * (sizeof(CEntry) + 2 * CRingBufferBase::ITEM_SIZE), CRingBufferBase::FLAG_RECYCLE), |
| 16 | m_Precision(Precision), |
| 17 | m_SummaryStats(SummaryStats) |
| 18 | { |
| 19 | } |
| 20 | |
| 21 | void CGraph::Init(float Min, float Max) |
| 22 | { |
| 23 | m_Entries.Clear(); |
| 24 | m_pFirstScaled = nullptr; |
| 25 | m_RenderedTotalTime = 0; |
| 26 | m_Average = 0.0f; |
| 27 | SetMin(Min); |
| 28 | SetMax(Max); |
| 29 | } |
| 30 | |
| 31 | void CGraph::SetMin(float Min) |
| 32 | { |
| 33 | m_MinRange = m_MinValue = m_MinAxis = Min; |
| 34 | } |
| 35 | |
| 36 | void CGraph::SetMax(float Max) |
| 37 | { |
| 38 | m_MaxRange = m_MaxValue = m_MaxAxis = Max; |
| 39 | } |
| 40 | |
| 41 | void CGraph::Scale(int64_t WantedTotalTime) |
| 42 | { |
| 43 | // Scale X axis for wanted total time |
| 44 | if(m_Entries.First() != nullptr) |
| 45 | { |
| 46 | const int64_t EndTime = m_Entries.Last()->m_Time; |
| 47 | bool ScaleTotalTime = false; |
| 48 | m_pFirstScaled = nullptr; |
| 49 | |
| 50 | if(m_Entries.First()->m_Time >= EndTime - WantedTotalTime) |
| 51 | { |
| 52 | m_pFirstScaled = m_Entries.First(); |
| 53 | } |
| 54 | else |
| 55 | { |
| 56 | m_pFirstScaled = m_Entries.Last(); |
| 57 | while(m_pFirstScaled) |
| 58 | { |
| 59 | CEntry *pPrev = m_Entries.Prev(pCurrent: m_pFirstScaled); |
| 60 | if(pPrev == nullptr) |
| 61 | break; |
| 62 | if(pPrev->m_Time < EndTime - WantedTotalTime) |
| 63 | { |
| 64 | // Scale based on actual total time instead of based on wanted total time, |
| 65 | // to avoid flickering last segment due to rounding errors. |
| 66 | ScaleTotalTime = true; |
| 67 | break; |
| 68 | } |
| 69 | m_pFirstScaled = pPrev; |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | m_RenderedTotalTime = ScaleTotalTime ? (EndTime - m_pFirstScaled->m_Time) : WantedTotalTime; |
| 74 | |
| 75 | // Ensure that color is applied to first line segment |
| 76 | if(m_pFirstScaled) |
| 77 | { |
| 78 | m_pFirstScaled->m_ApplyColor = true; |
| 79 | CEntry *pNext = m_Entries.Next(pCurrent: m_pFirstScaled); |
| 80 | if(pNext != nullptr) |
| 81 | { |
| 82 | pNext->m_ApplyColor = true; |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | else |
| 87 | { |
| 88 | m_pFirstScaled = nullptr; |
| 89 | m_RenderedTotalTime = 0; |
| 90 | } |
| 91 | |
| 92 | // Scale Y axis |
| 93 | m_Average = 0.0f; |
| 94 | size_t NumValues = 0; |
| 95 | m_MinAxis = m_MinRange; |
| 96 | m_MaxAxis = m_MaxRange; |
| 97 | m_MinValue = std::numeric_limits<float>::max(); |
| 98 | m_MaxValue = std::numeric_limits<float>::min(); |
| 99 | for(CEntry *pEntry = m_pFirstScaled; pEntry != nullptr; pEntry = m_Entries.Next(pCurrent: pEntry)) |
| 100 | { |
| 101 | const float Value = pEntry->m_Value; |
| 102 | m_Average += Value; |
| 103 | ++NumValues; |
| 104 | |
| 105 | if(Value > m_MaxAxis) |
| 106 | { |
| 107 | m_MaxAxis = Value; |
| 108 | } |
| 109 | else if(Value < m_MinAxis) |
| 110 | { |
| 111 | m_MinAxis = Value; |
| 112 | } |
| 113 | |
| 114 | if(Value > m_MaxValue) |
| 115 | { |
| 116 | m_MaxValue = Value; |
| 117 | } |
| 118 | else if(Value < m_MinValue) |
| 119 | { |
| 120 | m_MinValue = Value; |
| 121 | } |
| 122 | } |
| 123 | if(m_MaxValue < m_MinValue) |
| 124 | { |
| 125 | m_MinValue = m_MaxValue = 0.0f; |
| 126 | } |
| 127 | if(NumValues) |
| 128 | { |
| 129 | m_Average /= NumValues; |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | void CGraph::Add(float Value, ColorRGBA Color) |
| 134 | { |
| 135 | InsertAt(Time: time_get(), Value, Color); |
| 136 | } |
| 137 | |
| 138 | void CGraph::InsertAt(int64_t Time, float Value, ColorRGBA Color) |
| 139 | { |
| 140 | CEntry *pEntry = m_Entries.Allocate(Size: sizeof(CEntry)); |
| 141 | pEntry->m_Time = Time; |
| 142 | pEntry->m_Value = Value; |
| 143 | pEntry->m_Color = Color; |
| 144 | |
| 145 | // Determine whether the line (pPrev, pEntry) has different |
| 146 | // vertex colors than the line (pPrevPrev, pPrev). |
| 147 | CEntry *pPrev = m_Entries.Prev(pCurrent: pEntry); |
| 148 | if(pPrev == nullptr) |
| 149 | { |
| 150 | pEntry->m_ApplyColor = true; |
| 151 | } |
| 152 | else |
| 153 | { |
| 154 | CEntry *pPrevPrev = m_Entries.Prev(pCurrent: pPrev); |
| 155 | if(pPrevPrev == nullptr) |
| 156 | { |
| 157 | pEntry->m_ApplyColor = true; |
| 158 | } |
| 159 | else |
| 160 | { |
| 161 | pEntry->m_ApplyColor = Color != pPrev->m_Color || pPrev->m_Color != pPrevPrev->m_Color; |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | void CGraph::RenderDataLines(IGraphics *pGraphics, float x, float y, float w, float h) |
| 167 | { |
| 168 | if(m_pFirstScaled == nullptr) |
| 169 | { |
| 170 | return; |
| 171 | } |
| 172 | |
| 173 | IGraphics::CLineItemBatch LineItemBatch; |
| 174 | pGraphics->LinesBatchBegin(pBatch: &LineItemBatch); |
| 175 | |
| 176 | const int64_t StartTime = m_pFirstScaled->m_Time; |
| 177 | |
| 178 | CEntry *pEntry0 = m_pFirstScaled; |
| 179 | int a0 = round_to_int(f: (pEntry0->m_Time - StartTime) * w / m_RenderedTotalTime); |
| 180 | int v0 = round_to_int(f: (pEntry0->m_Value - m_MinAxis) * h / (m_MaxAxis - m_MinAxis)); |
| 181 | while(pEntry0 != nullptr) |
| 182 | { |
| 183 | CEntry *pEntry1 = m_Entries.Next(pCurrent: pEntry0); |
| 184 | if(pEntry1 == nullptr) |
| 185 | break; |
| 186 | |
| 187 | const int a1 = round_to_int(f: (pEntry1->m_Time - StartTime) * w / m_RenderedTotalTime); |
| 188 | const int v1 = round_to_int(f: (pEntry1->m_Value - m_MinAxis) * h / (m_MaxAxis - m_MinAxis)); |
| 189 | |
| 190 | if(pEntry1->m_ApplyColor) |
| 191 | { |
| 192 | pGraphics->LinesBatchEnd(pBatch: &LineItemBatch); |
| 193 | pGraphics->LinesBatchBegin(pBatch: &LineItemBatch); |
| 194 | |
| 195 | const IGraphics::CColorVertex aColorVertices[] = { |
| 196 | IGraphics::CColorVertex(0, pEntry0->m_Color.r, pEntry0->m_Color.g, pEntry0->m_Color.b, pEntry0->m_Color.a), |
| 197 | IGraphics::CColorVertex(1, pEntry1->m_Color.r, pEntry1->m_Color.g, pEntry1->m_Color.b, pEntry1->m_Color.a)}; |
| 198 | pGraphics->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices)); |
| 199 | } |
| 200 | const IGraphics::CLineItem Item = IGraphics::CLineItem( |
| 201 | x + a0, |
| 202 | y + h - v0, |
| 203 | x + a1, |
| 204 | y + h - v1); |
| 205 | pGraphics->LinesBatchDraw(pBatch: &LineItemBatch, pArray: &Item, Num: 1); |
| 206 | |
| 207 | pEntry0 = pEntry1; |
| 208 | a0 = a1; |
| 209 | v0 = v1; |
| 210 | } |
| 211 | pGraphics->LinesBatchEnd(pBatch: &LineItemBatch); |
| 212 | } |
| 213 | |
| 214 | void CGraph::Render(IGraphics *pGraphics, ITextRender *pTextRender, float x, float y, float w, float h, const char *pDescription) |
| 215 | { |
| 216 | const float FontSize = 12.0f; |
| 217 | const float Spacing = 2.0f; |
| 218 | const float = FontSize + 2.0f * Spacing; |
| 219 | |
| 220 | pGraphics->TextureClear(); |
| 221 | |
| 222 | pGraphics->QuadsBegin(); |
| 223 | pGraphics->SetColor(ColorRGBA(0.0f, 0.0f, 0.0f, 0.7f)); |
| 224 | const IGraphics::CQuadItem (x, y, w, HeaderHeight); |
| 225 | pGraphics->QuadsDrawTL(pArray: &ItemHeader, Num: 1); |
| 226 | const IGraphics::CQuadItem (x, y + h - HeaderHeight, w, HeaderHeight); |
| 227 | pGraphics->QuadsDrawTL(pArray: &ItemFooter, Num: 1); |
| 228 | pGraphics->SetColor(ColorRGBA(0.0f, 0.0f, 0.0f, 0.6f)); |
| 229 | const IGraphics::CQuadItem ItemGraph(x, y + HeaderHeight, w, h - 2.0f * HeaderHeight); |
| 230 | pGraphics->QuadsDrawTL(pArray: &ItemGraph, Num: 1); |
| 231 | pGraphics->QuadsEnd(); |
| 232 | |
| 233 | pGraphics->LinesBegin(); |
| 234 | pGraphics->SetColor(ColorRGBA(0.5f, 0.5f, 0.5f, 0.75f)); |
| 235 | const IGraphics::CLineItem aLineItems[] = { |
| 236 | IGraphics::CLineItem(ItemGraph.m_X, ItemGraph.m_Y + (ItemGraph.m_Height * 3) / 4, ItemGraph.m_X + ItemGraph.m_Width, ItemGraph.m_Y + (ItemGraph.m_Height * 3) / 4), |
| 237 | IGraphics::CLineItem(ItemGraph.m_X, ItemGraph.m_Y + ItemGraph.m_Height / 2, ItemGraph.m_X + ItemGraph.m_Width, ItemGraph.m_Y + ItemGraph.m_Height / 2), |
| 238 | IGraphics::CLineItem(ItemGraph.m_X, ItemGraph.m_Y + ItemGraph.m_Height / 4, ItemGraph.m_X + ItemGraph.m_Width, ItemGraph.m_Y + ItemGraph.m_Height / 4)}; |
| 239 | pGraphics->LinesDraw(pArray: aLineItems, Num: std::size(aLineItems)); |
| 240 | pGraphics->LinesEnd(); |
| 241 | |
| 242 | RenderDataLines(pGraphics, x: ItemGraph.m_X, y: ItemGraph.m_Y + 1.0f, w: ItemGraph.m_Width, h: ItemGraph.m_Height + 2.0f); |
| 243 | |
| 244 | char aBuf[128]; |
| 245 | |
| 246 | pTextRender->Text(x: ItemHeader.m_X + Spacing, y: ItemHeader.m_Y + Spacing, Size: FontSize, pText: pDescription); |
| 247 | |
| 248 | if(m_SummaryStats) |
| 249 | { |
| 250 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Avg. %.*f ↓ %.*f ↑ %.*f" , m_Precision, m_Average, m_Precision, m_MinValue, m_Precision, m_MaxValue); |
| 251 | pTextRender->Text(x: ItemFooter.m_X + Spacing, y: ItemFooter.m_Y + ItemFooter.m_Height - FontSize - Spacing, Size: FontSize, pText: aBuf); |
| 252 | } |
| 253 | |
| 254 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%.*f" , m_Precision, m_MaxAxis); |
| 255 | pTextRender->Text(x: ItemHeader.m_X + ItemHeader.m_Width - pTextRender->TextWidth(Size: FontSize, pText: aBuf) - Spacing, y: ItemHeader.m_Y + Spacing, Size: FontSize, pText: aBuf); |
| 256 | |
| 257 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%.*f" , m_Precision, m_MinAxis); |
| 258 | pTextRender->Text(x: ItemFooter.m_X + ItemFooter.m_Width - pTextRender->TextWidth(Size: FontSize, pText: aBuf) - Spacing, y: ItemFooter.m_Y + ItemFooter.m_Height - FontSize - Spacing, Size: FontSize, pText: aBuf); |
| 259 | } |
| 260 | |