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