CoreScriptsRoot/Modules/Stats/StatsAnnotatedGraph.lua
--[[
  Filename: StatsAnnotatedGraph.lua
  Written by: dbanks
  Description: A graph plus extra annotations like axis markings, 
      target lines, etc.
--]]

--[[ Services ]]--
local CoreGuiService = game:GetService('CoreGui')

--[[ Globals ]]--
local Margin = 10
local LabelXWidth = 30

--[[ Modules ]]--
local StatsUtils = require(CoreGuiService.RobloxGui.Modules.Stats.StatsUtils)
local BarGraphClass = require(CoreGuiService.RobloxGui.Modules.Stats.BarGraph)

--[[ Classes ]]--
local StatsAnnotatedGraphClass = {}
StatsAnnotatedGraphClass.__index = StatsAnnotatedGraphClass

function StatsAnnotatedGraphClass.new(statType, isMaximized) 
  local self = {}
  setmetatable(self, StatsAnnotatedGraphClass)

  self._statType = statType
  self._statMaxName = StatsUtils.StatMaxNames[statType]
  self._isMaximized = isMaximized

  self._values = {}
  
  -- Average value of all bars in the graph.
  self._average = 0
  -- Suggested max value for the stat being measured.
  self._target = 0
  -- Max value we display on the y-axis.  Values higher than this are truncated.
  self._axisMax = 0
  
  self._frame = Instance.new("Frame")
  self._frame.Name = "PS_AnnotatedGraph"
  self._frame.BackgroundTransparency = 1.0
    self._frame.ZIndex = StatsUtils.GraphZIndex

  self._topLabel = Instance.new("TextLabel")
  self._topLabel.Name = "PS_TopAxisLabel"
  self._topLabel.Parent = self._frame
  self._topLabel.TextXAlignment = Enum.TextXAlignment.Left
  self._topLabel.TextYAlignment = Enum.TextYAlignment.Top
  self._topLabel.FontSize = StatsUtils.PanelGraphFontSize
  
  self._bottomLabel = Instance.new("TextLabel")
  self._bottomLabel.Name = "PS_BottomAxisLabel"
  self._bottomLabel.Parent = self._frame
  self._bottomLabel.TextXAlignment = Enum.TextXAlignment.Left
  self._bottomLabel.TextYAlignment = Enum.TextYAlignment.Bottom
  self._bottomLabel.FontSize = StatsUtils.PanelGraphFontSize

  local showExtras = isMaximized
  self._graph = BarGraphClass.new(showExtras)

  StatsUtils.StyleTextWidget(self._topLabel)
  StatsUtils.StyleTextWidget(self._bottomLabel)

  self:_layoutElements()

  return self
end

function StatsAnnotatedGraphClass:SetZIndex(zIndex)
  self._frame.ZIndex = zIndex
  self._topLabel.ZIndex = zIndex
  self._bottomLabel.ZIndex = zIndex
  self._graph:SetZIndex(zIndex)
end

function StatsAnnotatedGraphClass:_layoutElements()
  local labelWidth
  if (self._isMaximized) then
    labelWidth = LabelXWidth
    
    self._topLabel.Visible = true
    self._bottomLabel.Visible = true
  else
    labelWidth = 0
    
    self._topLabel.Visible = false
    self._bottomLabel.Visible = false
  end
  
  local GraphFramePosition = UDim2.new(0, Margin, 0, Margin)
  local GraphFrameSize = UDim2.new(1, -(2 * Margin + labelWidth), 1, -2 * Margin)

  local TopLabelFramePosition = UDim2.new(1, -(Margin + labelWidth), 0, Margin)
  local TopLabelFrameSize = UDim2.new(0, labelWidth, 0.333, -2 * Margin)
  local BottomLabelFramePosition = UDim2.new(1, -(Margin + labelWidth), 0.666, Margin)
  local BottomLabelFrameSize = UDim2.new(0, labelWidth, 0.333, -2 * Margin)
  
  self._topLabel.Size = TopLabelFrameSize
  self._topLabel.Position = TopLabelFramePosition
  self._bottomLabel.Size = BottomLabelFrameSize
  self._bottomLabel.Position = BottomLabelFramePosition
  
  self._graph:PlaceInParent(self._frame, GraphFrameSize, GraphFramePosition)
end

function StatsAnnotatedGraphClass:PlaceInParent(parent, size, position) 
  self._frame.Position = position
  self._frame.Size = size
  self._frame.Parent = parent
end

function StatsAnnotatedGraphClass:_getTarget()
  -- Get the current target value for the graphed stat.
  if self._performanceStats == nil then
    return 0
  end  
  
  local maxItemStats = self._performanceStats:FindFirstChild(self._statMaxName)
  if maxItemStats == nil then
    return 0
  end
  
  return maxItemStats:GetValue()
end

function StatsAnnotatedGraphClass:_render()  
  self._graph:SetAxisMax(self._axisMax)
  self._graph:SetValues(self._values)

  self._graph:SetAverage(self._average)
  self._graph:SetTarget(self._target)
  self._graph:Render()
  
  self._topLabel.Text = string.format("%.2f", self._axisMax)
  self._bottomLabel.Text = string.format("%.2f", 0,.0)
end

function StatsAnnotatedGraphClass:_calculateAxisMax()
  -- Calculate an optimal max axis label for this graph, given this 'target' value.
  -- We want target to be roughly in the middle.
  -- Say, roughly twice the target.
  local max = self._target * 2
  local orderOfMagnitude = self:_recursiveGetOrderOfMagnitude(1.0, max)
  local div = math.floor(0.5 + max/orderOfMagnitude)
  self._axisMax = div * orderOfMagnitude
end

function StatsAnnotatedGraphClass:SetStatsAggregator(aggregator)
  if (self._aggregator) then
    self._aggregator:RemoveListener(self._listenerId)
    self._listenerId = nil
    self._aggregator = nil
  end
  
  self._aggregator = aggregator
  
  self:_refreshVisibility()
end

function StatsAnnotatedGraphClass:_stopListening()
  if (self._aggregator == nil) then
    return
  end
  
  if (self._listenerId == nil) then 
    return
  end
  
  self._aggregator:RemoveListener(self._listenerId)
  self._listenerId = nil  
end

function StatsAnnotatedGraphClass:_startListening()
  if (self._aggregator == nil) then
    return
  end
  
  if (self._listenerId ~= nil) then 
    return
  end
  
  self._listenerId = self._aggregator:AddListener(function()
      self:_updateValuesAndRender()
  end)  
end

function StatsAnnotatedGraphClass:_shouldBeVisible()
  if StatsUtils:PerformanceStatsShouldBeVisible() then 
    return self._frame.Visible
  else
    return false
  end
end

function StatsAnnotatedGraphClass:SetVisible(visible)
  self._frame.Visible = visible
  self:_refreshVisibility()
end

function StatsAnnotatedGraphClass:_refreshVisibility()
  if self:_shouldBeVisible() then
    self:_startListening()
    self:_updateValuesAndRender()
  else
    self:_stopListening()
  end
end

function StatsAnnotatedGraphClass:OnPerformanceStatsShouldBeVisibleChanged()
  self:_refreshVisibility()
end

function StatsAnnotatedGraphClass:_recursiveGetOrderOfMagnitude(estimate, target)
  if (estimate > target) then 
    return self:_recursiveGetOrderOfMagnitude(estimate/10.0, target)
  end
  
  if (estimate * 10 >= target) then 
    return estimate
  end
  
  return self:_recursiveGetOrderOfMagnitude(estimate*10.0, target)
end

function StatsAnnotatedGraphClass:_updateValuesAndRender() 
  self._values = {}
  self._average = 0
  self._target = 0
  if self._aggregator ~= nil then 
    self._values = self._aggregator:GetValues()
    self._average = self._aggregator:GetAverage()
    self._target = self._aggregator:GetTarget()
  end
  
  self:_calculateAxisMax()
  self:_render()
end

return StatsAnnotatedGraphClass
