616 lines
21 KiB
Lua
616 lines
21 KiB
Lua
|
-- ------------------------------------------------------------------------------ --
|
||
|
-- TradeSkillMaster --
|
||
|
-- https://tradeskillmaster.com --
|
||
|
-- All Rights Reserved - Detailed license information included with addon. --
|
||
|
-- ------------------------------------------------------------------------------ --
|
||
|
|
||
|
--- Graph UI Element Class.
|
||
|
-- The graph element allows for generating line graphs. It is a subclass of the @{Element} class.
|
||
|
-- @classmod Graph
|
||
|
|
||
|
local _, TSM = ...
|
||
|
local Math = TSM.Include("Util.Math")
|
||
|
local Theme = TSM.Include("Util.Theme")
|
||
|
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
|
||
|
local Graph = TSM.Include("LibTSMClass").DefineClass("Graph", TSM.UI.Element)
|
||
|
local UIElements = TSM.Include("UI.UIElements")
|
||
|
UIElements.Register(Graph)
|
||
|
TSM.UI.Graph = Graph
|
||
|
local private = {}
|
||
|
local PLOT_X_LABEL_WIDTH = 48
|
||
|
local PLOT_X_LABEL_HEIGHT = 16
|
||
|
local PLOT_X_LABEL_MARGIN = 6
|
||
|
local PLOT_Y_LABEL_WIDTH = 48
|
||
|
local PLOT_Y_LABEL_HEIGHT = 16
|
||
|
local PLOT_Y_LABEL_MARGIN = 4
|
||
|
local PLOT_HIGHLIGHT_TEXT_WIDTH = 80
|
||
|
local PLOT_HIGHLIGHT_TEXT_HEIGHT = 16
|
||
|
local PLOT_X_EXTRA_HIT_RECT = 4
|
||
|
local PLOT_Y_MARGIN = 4
|
||
|
local LINE_THICKNESS = 1
|
||
|
local LINE_THICKNESS_RATIO = 16
|
||
|
local PLOT_MIN_X_LINE_SPACING = PLOT_X_LABEL_WIDTH * 1.5 + 8
|
||
|
local PLOT_MIN_Y_LINE_SPACING = PLOT_Y_LABEL_HEIGHT * 1.5 + 8
|
||
|
local HOVER_LINE_THICKNESS = 1
|
||
|
local MAX_FILL_ALPHA = 0.5
|
||
|
local SELECTION_ALPHA = 0.2
|
||
|
local MAX_PLOT_POINTS = 300
|
||
|
|
||
|
|
||
|
|
||
|
-- ============================================================================
|
||
|
-- Public Class Methods
|
||
|
-- ============================================================================
|
||
|
|
||
|
function Graph.__init(self)
|
||
|
local frame = UIElements.CreateFrame(self, "Frame", nil, nil, TSM.IsShadowlands() and "BackdropTemplate" or nil)
|
||
|
|
||
|
self.__super:__init(frame)
|
||
|
|
||
|
frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
|
||
|
|
||
|
frame.plot = CreateFrame("Frame", nil, frame, nil)
|
||
|
frame.plot:SetPoint("BOTTOMLEFT", PLOT_Y_LABEL_WIDTH, PLOT_X_LABEL_HEIGHT)
|
||
|
frame.plot:SetPoint("TOPRIGHT", -PLOT_X_EXTRA_HIT_RECT, -PLOT_HIGHLIGHT_TEXT_HEIGHT - PLOT_Y_MARGIN)
|
||
|
frame.plot:SetHitRectInsets(-PLOT_X_EXTRA_HIT_RECT, -PLOT_X_EXTRA_HIT_RECT, 0, 0)
|
||
|
frame.plot:EnableMouse(true)
|
||
|
ScriptWrapper.Set(frame.plot, "OnEnter", private.PlotFrameOnEnter, self)
|
||
|
ScriptWrapper.Set(frame.plot, "OnLeave", private.PlotFrameOnLeave, self)
|
||
|
ScriptWrapper.Set(frame.plot, "OnMouseDown", private.PlotFrameOnMouseDown, self)
|
||
|
ScriptWrapper.Set(frame.plot, "OnMouseUp", private.PlotFrameOnMouseUp, self)
|
||
|
|
||
|
frame.plot.dot = frame.plot:CreateTexture(nil, "ARTWORK", nil, 3)
|
||
|
TSM.UI.TexturePacks.SetTextureAndSize(frame.plot.dot, "uiFrames.HighlightDot")
|
||
|
|
||
|
frame.plot.hoverLine = frame.plot:CreateTexture(nil, "ARTWORK", nil, 2)
|
||
|
frame.plot.hoverLine:SetWidth(HOVER_LINE_THICKNESS)
|
||
|
frame.plot.hoverLine:Hide()
|
||
|
|
||
|
frame.plot.hoverText = frame.plot:CreateFontString()
|
||
|
frame.plot.hoverText:SetSize(PLOT_HIGHLIGHT_TEXT_WIDTH, PLOT_HIGHLIGHT_TEXT_HEIGHT)
|
||
|
frame.plot.hoverText:Hide()
|
||
|
|
||
|
frame.plot.selectionBox = frame.plot:CreateTexture(nil, "ARTWORK", nil, 2)
|
||
|
frame.plot.selectionBox:Hide()
|
||
|
|
||
|
self._usedTextures = {}
|
||
|
self._freeTextures = {}
|
||
|
self._usedFontStrings = {}
|
||
|
self._freeFontStrings = {}
|
||
|
self._xValuesFiltered = {}
|
||
|
self._yLookup = {}
|
||
|
self._yValueFunc = nil
|
||
|
self._xFormatFunc = nil
|
||
|
self._yFormatFunc = nil
|
||
|
self._xStepFunc = nil
|
||
|
self._yStepFunc = nil
|
||
|
self._xMin = nil
|
||
|
self._xMax = nil
|
||
|
self._yMin = nil
|
||
|
self._yMax = nil
|
||
|
self._isMouseOver = false
|
||
|
self._selectionStartX = nil
|
||
|
self._zoomStart = nil
|
||
|
self._zoomEnd = nil
|
||
|
self._onZoomChanged = nil
|
||
|
self._onHoverUpdate = nil
|
||
|
end
|
||
|
|
||
|
function Graph.Release(self)
|
||
|
self:_ReleaseAllTextures()
|
||
|
self:_ReleaseAllFontStrings()
|
||
|
wipe(self._xValuesFiltered)
|
||
|
wipe(self._yLookup)
|
||
|
self._yValueFunc = nil
|
||
|
self._xFormatFunc = nil
|
||
|
self._yFormatFunc = nil
|
||
|
self._xStepFunc = nil
|
||
|
self._yStepFunc = nil
|
||
|
self._xMin = nil
|
||
|
self._xMax = nil
|
||
|
self._yMin = nil
|
||
|
self._yMax = nil
|
||
|
self._isMouseOver = false
|
||
|
self._selectionStartX = nil
|
||
|
self._zoomStart = nil
|
||
|
self._zoomEnd = nil
|
||
|
self._onZoomChanged = nil
|
||
|
self._onHoverUpdate = nil
|
||
|
self.__super:Release()
|
||
|
end
|
||
|
|
||
|
--- Sets the step size of the axes.
|
||
|
-- @tparam Graph self The graph object
|
||
|
-- @tparam function x A function which gets the next x-axis step value
|
||
|
-- @tparam function y A function which gets the next y-axis step value
|
||
|
-- @treturn Graph The graph object
|
||
|
function Graph.SetAxisStepFunctions(self, x, y)
|
||
|
self._xStepFunc = x
|
||
|
self._yStepFunc = y
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
function Graph.SetXRange(self, xMin, xMax, stepInterval)
|
||
|
assert(xMin <= xMax)
|
||
|
self._xMin = xMin
|
||
|
self._xMax = xMax
|
||
|
self._xStepInterval = stepInterval
|
||
|
self._zoomStart = xMin
|
||
|
self._zoomEnd = xMax
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
function Graph.SetZoom(self, zoomStart, zoomEnd)
|
||
|
self._zoomStart = zoomStart
|
||
|
self._zoomEnd = zoomEnd
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
function Graph.GetZoom(self)
|
||
|
return self._zoomStart, self._zoomEnd
|
||
|
end
|
||
|
|
||
|
function Graph.GetXRange(self)
|
||
|
local yMin, yMax = nil, nil
|
||
|
for _, x in ipairs(self._xValuesFiltered) do
|
||
|
local y = self._yValueFunc(x)
|
||
|
yMin = min(yMin or math.huge, y)
|
||
|
yMax = max(yMax or -math.huge, y)
|
||
|
end
|
||
|
return self._xMin, self._xMax
|
||
|
end
|
||
|
|
||
|
function Graph.GetYRange(self)
|
||
|
local yMin, yMax = nil, nil
|
||
|
for _, x in ipairs(self._xValuesFiltered) do
|
||
|
local y = self._yValueFunc(x)
|
||
|
yMin = min(yMin or math.huge, y)
|
||
|
yMax = max(yMax or -math.huge, y)
|
||
|
end
|
||
|
return yMin, yMax
|
||
|
end
|
||
|
|
||
|
function Graph.SetYValueFunction(self, func)
|
||
|
self._yValueFunc = func
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--- Sets functions for formatting values.
|
||
|
-- @tparam Graph self The graph object
|
||
|
-- @tparam function xFormatFunc A function which is passed an x value and returns a formatted string
|
||
|
-- @tparam function yFormatFunc A function which is passed a y value and returns a formatted string
|
||
|
-- @treturn Graph The graph object
|
||
|
function Graph.SetFormatFunctions(self, xFormatFunc, yFormatFunc)
|
||
|
self._xFormatFunc = xFormatFunc
|
||
|
self._yFormatFunc = yFormatFunc
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--- Registers a script handler.
|
||
|
-- @tparam ScrollingTable self The graph object
|
||
|
-- @tparam string script The script to register for (supported scripts: `OnZoomChanged`)
|
||
|
-- @tparam function handler The script handler which will be called with the graph object followed by any
|
||
|
-- arguments to the script
|
||
|
-- @treturn Graph The graph object
|
||
|
function Graph.SetScript(self, script, handler)
|
||
|
if script == "OnZoomChanged" then
|
||
|
self._onZoomChanged = handler
|
||
|
elseif script == "OnHoverUpdate" then
|
||
|
self._onHoverUpdate = handler
|
||
|
else
|
||
|
error("Unknown Graph script: "..tostring(script))
|
||
|
end
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
function Graph.Draw(self)
|
||
|
self.__super:Draw()
|
||
|
self:_ReleaseAllTextures()
|
||
|
self:_ReleaseAllFontStrings()
|
||
|
local frame = self:_GetBaseFrame()
|
||
|
frame:SetBackdropColor(Theme.GetColor("PRIMARY_BG"):GetFractionalRGBA())
|
||
|
local plot = frame.plot
|
||
|
plot.hoverText:SetFont(Theme.GetFont("TABLE_TABLE1"):GetWowFont())
|
||
|
plot.hoverText:SetTextColor(Theme.GetColor("INDICATOR_ALT"):GetFractionalRGBA())
|
||
|
|
||
|
local plotWidth = plot:GetWidth()
|
||
|
local plotHeight = plot:GetHeight()
|
||
|
|
||
|
-- update the filtered set of x values to show and the bounds of the plot data
|
||
|
self:_PopulateFilteredData(plotWidth)
|
||
|
|
||
|
-- calculate the min and max y values which should be shown
|
||
|
self._yMin, self._yMax = self._yStepFunc("RANGE", self._yMin, self._yMax, floor(plotHeight / PLOT_MIN_Y_LINE_SPACING))
|
||
|
if Math.IsNan(self._yMax) then
|
||
|
-- this happens when we're resizing the application frame
|
||
|
return
|
||
|
end
|
||
|
|
||
|
-- draw the y axis lines and labels
|
||
|
local prevYAxisOffset = -math.huge
|
||
|
local yAxisValue = self._yMin
|
||
|
while yAxisValue <= self._yMax do
|
||
|
local yAxisOffset = Math.Scale(yAxisValue, self._yMin, self._yMax, 0, plotHeight)
|
||
|
if not prevYAxisOffset or (yAxisOffset - prevYAxisOffset) >= PLOT_MIN_Y_LINE_SPACING then
|
||
|
self:_DrawYAxisLine(yAxisOffset, yAxisValue, plotWidth, plotHeight)
|
||
|
prevYAxisOffset = yAxisOffset
|
||
|
end
|
||
|
yAxisValue = self._yStepFunc("NEXT", yAxisValue, self._yMax)
|
||
|
end
|
||
|
|
||
|
-- draw the x axis lines and labels
|
||
|
local xSuggestedStep = Math.Scale(PLOT_MIN_X_LINE_SPACING, 0, plotWidth, 0, self._zoomEnd - self._zoomStart)
|
||
|
local prevXAxisOffset = -math.huge
|
||
|
local xAxisValue = self._xStepFunc(self._zoomStart, xSuggestedStep)
|
||
|
while xAxisValue <= self._zoomEnd do
|
||
|
local xAxisOffset = Math.Scale(xAxisValue, self._zoomStart, self._zoomEnd, 0, plotWidth)
|
||
|
if not prevXAxisOffset or (xAxisOffset - prevXAxisOffset) > PLOT_MIN_X_LINE_SPACING then
|
||
|
self:_DrawXAxisLine(xAxisOffset, xAxisValue, plotWidth, plotHeight, xSuggestedStep)
|
||
|
prevXAxisOffset = xAxisOffset
|
||
|
end
|
||
|
xAxisValue = self._xStepFunc(xAxisValue, xSuggestedStep)
|
||
|
end
|
||
|
|
||
|
-- draw all the lines
|
||
|
local color = nil
|
||
|
if self._isMouseOver or self._selectionStartX then
|
||
|
color = Theme.GetColor("INDICATOR_ALT")
|
||
|
elseif self._yLookup[self._xValuesFiltered[1]] <= self._yLookup[self._xValuesFiltered[#self._xValuesFiltered]] then
|
||
|
color = Theme.GetFeedbackColor("GREEN")
|
||
|
else
|
||
|
color = Theme.GetFeedbackColor("RED")
|
||
|
end
|
||
|
local xPrev, yPrev = nil, nil
|
||
|
for _, x in ipairs(self._xValuesFiltered) do
|
||
|
local y = self._yLookup[x]
|
||
|
local xCoord = Math.Scale(x, self._zoomStart, self._zoomEnd, 0, plotWidth)
|
||
|
local yCoord = Math.Scale(y, self._yMin, self._yMax, 0, plotHeight)
|
||
|
if xPrev then
|
||
|
self:_DrawFillLine(xPrev, yPrev, xCoord, yCoord, LINE_THICKNESS, plotHeight, color)
|
||
|
end
|
||
|
xPrev = xCoord
|
||
|
yPrev = yCoord
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
|
||
|
-- ============================================================================
|
||
|
-- Private Class Methods
|
||
|
-- ============================================================================
|
||
|
|
||
|
function Graph._PopulateFilteredData(self, plotWidth)
|
||
|
wipe(self._xValuesFiltered)
|
||
|
wipe(self._yLookup)
|
||
|
self._yMin = math.huge
|
||
|
self._yMax = -math.huge
|
||
|
local minStep = Math.Ceil((self._zoomEnd - self._zoomStart) / min(plotWidth / 3, MAX_PLOT_POINTS), self._xStepInterval)
|
||
|
local x = self._zoomStart
|
||
|
while x <= self._zoomEnd do
|
||
|
local prevX = self._xValuesFiltered[#self._xValuesFiltered]
|
||
|
if not prevX or x == self._zoomEnd or (x - prevX > minStep and self._zoomEnd - x > minStep) then
|
||
|
-- this is either the first / last point or a middle point which is sufficiently far from the previous and last points
|
||
|
tinsert(self._xValuesFiltered, x)
|
||
|
local y = self._yValueFunc(x)
|
||
|
self._yMin = min(self._yMin, y)
|
||
|
self._yMax = max(self._yMax, y)
|
||
|
self._yLookup[x] = y
|
||
|
end
|
||
|
if x == self._zoomEnd then
|
||
|
break
|
||
|
end
|
||
|
x = min(x + minStep, self._zoomEnd)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function Graph._DrawYAxisLine(self, yOffset, yValue, plotWidth, plotHeight, ySuggestedStep)
|
||
|
local line = self:_AcquireLine("ARTWORK")
|
||
|
local thickness = LINE_THICKNESS
|
||
|
local textureHeight = thickness * LINE_THICKNESS_RATIO
|
||
|
-- trim the texture a bit on the left/right since it's not completely filled to the edges which is noticeable on long lines
|
||
|
line:SetTexCoord(0.1, 1, 0.1, 0, 0.9, 1, 0.9, 0)
|
||
|
line:SetPoint("BOTTOMLEFT", 0 - thickness / 2, yOffset - textureHeight / 2)
|
||
|
line:SetPoint("TOPRIGHT", line:GetParent(), "BOTTOMLEFT", plotWidth + thickness / 2, yOffset + textureHeight / 2)
|
||
|
line:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
|
||
|
line:SetDrawLayer("BACKGROUND", 0)
|
||
|
local text = self:_AcquireFontString(Theme.GetFont("TABLE_TABLE1"))
|
||
|
text:SetJustifyH("RIGHT")
|
||
|
local textYOffset = 0
|
||
|
if PLOT_Y_LABEL_HEIGHT / 2 > yOffset then
|
||
|
text:SetJustifyV("BOTTOM")
|
||
|
textYOffset = max(PLOT_Y_LABEL_HEIGHT / 2 - yOffset, 0)
|
||
|
elseif yOffset + PLOT_Y_LABEL_HEIGHT / 2 > plotHeight then
|
||
|
text:SetJustifyV("TOP")
|
||
|
textYOffset = plotHeight - yOffset - PLOT_Y_LABEL_HEIGHT / 2
|
||
|
else
|
||
|
text:SetJustifyV("MIDDLE")
|
||
|
end
|
||
|
text:SetPoint("RIGHT", line, "LEFT", -PLOT_Y_LABEL_MARGIN, textYOffset)
|
||
|
text:SetSize(PLOT_Y_LABEL_WIDTH, PLOT_Y_LABEL_HEIGHT)
|
||
|
text:SetText(self._yFormatFunc(yValue, ySuggestedStep))
|
||
|
end
|
||
|
|
||
|
function Graph._DrawXAxisLine(self, xOffset, xValue, plotWidth, plotHeight, xSuggestedStep)
|
||
|
local line = self:_AcquireLine("ARTWORK")
|
||
|
local thickness = LINE_THICKNESS
|
||
|
local textureHeight = thickness * LINE_THICKNESS_RATIO
|
||
|
-- trim the texture a bit on the left/right since it's not completely filled to the edges which is noticeable on long lines
|
||
|
line:SetTexCoord(0.9, 1, 0.1, 1, 0.9, 0, 0.1, 0)
|
||
|
line:SetPoint("BOTTOMLEFT", xOffset - textureHeight / 2, thickness / 2)
|
||
|
line:SetPoint("TOPRIGHT", line:GetParent(), "BOTTOMLEFT", xOffset + textureHeight / 2, plotHeight + thickness / 2)
|
||
|
line:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
|
||
|
line:SetDrawLayer("BACKGROUND", 0)
|
||
|
local text = self:_AcquireFontString(Theme.GetFont("BODY_BODY3_MEDIUM"))
|
||
|
text:ClearAllPoints()
|
||
|
text:SetJustifyV("TOP")
|
||
|
local textXOffset = 0
|
||
|
if PLOT_X_LABEL_WIDTH / 2 > xOffset then
|
||
|
text:SetJustifyH("LEFT")
|
||
|
textXOffset = max(PLOT_X_LABEL_WIDTH / 2 - xOffset, 0)
|
||
|
elseif xOffset + PLOT_X_LABEL_WIDTH / 2 > plotWidth then
|
||
|
text:SetJustifyH("RIGHT")
|
||
|
textXOffset = plotWidth - xOffset - PLOT_X_LABEL_WIDTH / 2
|
||
|
else
|
||
|
text:SetJustifyH("CENTER")
|
||
|
end
|
||
|
text:SetPoint("TOP", line, "BOTTOM", textXOffset, -PLOT_X_LABEL_MARGIN)
|
||
|
text:SetSize(PLOT_X_LABEL_WIDTH, PLOT_X_LABEL_HEIGHT)
|
||
|
text:SetText(self._xFormatFunc(xValue, xSuggestedStep))
|
||
|
end
|
||
|
|
||
|
function Graph._DrawFillLine(self, xFrom, yFrom, xTo, yTo, thickness, plotHeight, color)
|
||
|
assert(xFrom <= xTo)
|
||
|
local line = self:_AcquireLine("ARTWORK")
|
||
|
local textureHeight = thickness * LINE_THICKNESS_RATIO
|
||
|
local xDiff = xTo - xFrom
|
||
|
local yDiff = yTo - yFrom
|
||
|
local length = sqrt(xDiff * xDiff + yDiff * yDiff)
|
||
|
local sinValue = -yDiff / length
|
||
|
local cosValue = xDiff / length
|
||
|
local sinCosValue = sinValue * cosValue
|
||
|
local aspectRatio = length / textureHeight
|
||
|
local invAspectRatio = textureHeight / length
|
||
|
|
||
|
-- calculate and set tex coords
|
||
|
local LLx, LLy, ULx, ULy, URx, URy, LRx, LRy = nil, nil, nil, nil, nil, nil, nil, nil
|
||
|
if yDiff >= 0 then
|
||
|
LLx = invAspectRatio * sinCosValue
|
||
|
LLy = sinValue * sinValue
|
||
|
LRy = aspectRatio * sinCosValue
|
||
|
LRx = 1 - LLy
|
||
|
ULx = LLy
|
||
|
ULy = 1 - LRy
|
||
|
URx = 1 - LLx
|
||
|
URy = LRx
|
||
|
else
|
||
|
LLx = sinValue * sinValue
|
||
|
LLy = -aspectRatio * sinCosValue
|
||
|
LRx = 1 + invAspectRatio * sinCosValue
|
||
|
LRy = LLx
|
||
|
ULx = 1 - LRx
|
||
|
ULy = 1 - LLx
|
||
|
URy = 1 - LLy
|
||
|
URx = ULy
|
||
|
end
|
||
|
line:SetTexCoord(ULx, ULy, LLx, LLy, URx, URy, LRx, LRy)
|
||
|
|
||
|
-- calculate and set texture anchors
|
||
|
local xCenter = (xFrom + xTo) / 2
|
||
|
local yCenter = (yFrom + yTo) / 2
|
||
|
local halfWidth = (xDiff + invAspectRatio * abs(yDiff) + thickness) / 2
|
||
|
local halfHeight = (abs(yDiff) + invAspectRatio * xDiff + thickness) / 2
|
||
|
line:SetPoint("BOTTOMLEFT", xCenter - halfWidth, yCenter - halfHeight)
|
||
|
line:SetPoint("TOPRIGHT", line:GetParent(), "BOTTOMLEFT", xCenter + halfWidth, yCenter + halfHeight)
|
||
|
|
||
|
local minY = min(yFrom, yTo)
|
||
|
local maxY = max(yFrom, yTo)
|
||
|
local r, g, b, a = color:GetFractionalRGBA()
|
||
|
local barMaxAlpha = Math.Scale(minY, 0, plotHeight, 0, MAX_FILL_ALPHA * a)
|
||
|
local topMaxAlpha = Math.Scale(maxY, 0, plotHeight, 0, MAX_FILL_ALPHA * a)
|
||
|
line:SetVertexColor(r, g, b, a)
|
||
|
|
||
|
local fillTop = self:_AcquireTexture("ARTWORK", -1)
|
||
|
fillTop:SetTexture("Interface\\AddOns\\TradeSkillMaster\\Media\\triangle")
|
||
|
if yFrom < yTo then
|
||
|
fillTop:SetTexCoord(0, 0, 0, 1, 1, 0, 1, 1)
|
||
|
else
|
||
|
fillTop:SetTexCoord(1, 0, 1, 1, 0, 0, 0, 1)
|
||
|
end
|
||
|
fillTop:SetGradientAlpha("VERTICAL", r, g, b, barMaxAlpha, r, g, b, topMaxAlpha)
|
||
|
fillTop:SetPoint("BOTTOMLEFT", xFrom, minY)
|
||
|
fillTop:SetPoint("TOPRIGHT", fillTop:GetParent(), "BOTTOMLEFT", xTo, maxY)
|
||
|
|
||
|
local fillBar = self:_AcquireTexture("ARTWORK", -1)
|
||
|
fillBar:SetTexture("Interface\\Buttons\\WHITE8X8")
|
||
|
fillBar:SetGradientAlpha("VERTICAL", r, g, b, 0, r, g, b, barMaxAlpha)
|
||
|
fillBar:SetPoint("BOTTOMLEFT", xFrom, 0)
|
||
|
fillBar:SetPoint("TOPRIGHT", fillBar:GetParent(), "BOTTOMLEFT", xTo, minY)
|
||
|
|
||
|
return line
|
||
|
end
|
||
|
|
||
|
function Graph._AcquireLine(self, layer, subLayer)
|
||
|
local line = self:_AcquireTexture(layer, subLayer)
|
||
|
line:SetTexture("Interface\\AddOns\\TradeSkillMaster\\Media\\line.tga")
|
||
|
return line
|
||
|
end
|
||
|
|
||
|
function Graph._AcquireTexture(self, layer, subLayer)
|
||
|
local plot = self:_GetBaseFrame().plot
|
||
|
local result = tremove(self._freeTextures) or plot:CreateTexture()
|
||
|
tinsert(self._usedTextures, result)
|
||
|
result:SetParent(plot)
|
||
|
result:Show()
|
||
|
result:SetDrawLayer(layer, subLayer)
|
||
|
return result
|
||
|
end
|
||
|
|
||
|
function Graph._ReleaseAllTextures(self)
|
||
|
while #self._usedTextures > 0 do
|
||
|
local texture = tremove(self._usedTextures)
|
||
|
texture:SetTexture(nil)
|
||
|
texture:SetVertexColor(0, 0, 0, 0)
|
||
|
texture:SetTexCoord(0, 0, 0, 1, 1, 0, 1, 1)
|
||
|
texture:SetWidth(0)
|
||
|
texture:SetHeight(0)
|
||
|
texture:ClearAllPoints()
|
||
|
texture:Hide()
|
||
|
tinsert(self._freeTextures, texture)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function Graph._AcquireFontString(self, font)
|
||
|
local plot = self:_GetBaseFrame().plot
|
||
|
local result = tremove(self._freeFontStrings) or plot:CreateFontString()
|
||
|
tinsert(self._usedFontStrings, result)
|
||
|
result:SetParent(plot)
|
||
|
result:Show()
|
||
|
result:SetFont(font:GetWowFont())
|
||
|
result:SetTextColor(Theme.GetColor("TEXT"):GetFractionalRGBA())
|
||
|
return result
|
||
|
end
|
||
|
|
||
|
function Graph._ReleaseAllFontStrings(self)
|
||
|
while #self._usedFontStrings > 0 do
|
||
|
local fontString = tremove(self._usedFontStrings)
|
||
|
fontString:SetWidth(0)
|
||
|
fontString:SetHeight(0)
|
||
|
fontString:ClearAllPoints()
|
||
|
fontString:Hide()
|
||
|
tinsert(self._freeFontStrings, fontString)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function Graph._GetCursorClosestPoint(self)
|
||
|
local plotFrame = self:_GetBaseFrame().plot
|
||
|
local xPos = GetCursorPosition() / plotFrame:GetEffectiveScale()
|
||
|
local fromMin = plotFrame:GetLeft()
|
||
|
local fromMax = plotFrame:GetRight()
|
||
|
-- Convert the cursor position to be relative to the plotted x values
|
||
|
xPos = Math.Scale(Math.Bound(xPos, fromMin, fromMax), fromMin, fromMax, self._zoomStart, self._zoomEnd)
|
||
|
-- Find the closest point to the cursor (based on the x distance)
|
||
|
local closestX, closestY = nil, nil
|
||
|
for _, x in ipairs(self._xValuesFiltered) do
|
||
|
local y = self._yLookup[x]
|
||
|
local xDist = abs(x - xPos)
|
||
|
if not closestX or xDist < abs(closestX - xPos) then
|
||
|
closestX = x
|
||
|
closestY = y
|
||
|
end
|
||
|
end
|
||
|
assert(closestY)
|
||
|
return closestX, closestY
|
||
|
end
|
||
|
|
||
|
function Graph._XValueToPlotCoord(self, xValue)
|
||
|
local plotFrame = self:_GetBaseFrame().plot
|
||
|
return Math.Scale(xValue, self._zoomStart, self._zoomEnd, 0, plotFrame:GetWidth())
|
||
|
end
|
||
|
|
||
|
|
||
|
|
||
|
-- ============================================================================
|
||
|
-- Private Helper Functions
|
||
|
-- ============================================================================
|
||
|
|
||
|
function private.PlotFrameOnEnter(self)
|
||
|
self._isMouseOver = true
|
||
|
self:Draw()
|
||
|
local plotFrame = self:_GetBaseFrame().plot
|
||
|
ScriptWrapper.Set(plotFrame, "OnUpdate", private.PlotFrameOnUpdate, self)
|
||
|
end
|
||
|
|
||
|
function private.PlotFrameOnLeave(self)
|
||
|
self._isMouseOver = false
|
||
|
end
|
||
|
|
||
|
function private.PlotFrameOnUpdate(self)
|
||
|
local plotFrame = self:_GetBaseFrame().plot
|
||
|
local closestX, closestY = self:_GetCursorClosestPoint()
|
||
|
local xCoord = self:_XValueToPlotCoord(closestX)
|
||
|
local yCoord = Math.Scale(closestY, self._yMin, self._yMax, 0, plotFrame:GetHeight())
|
||
|
|
||
|
if self._isMouseOver then
|
||
|
plotFrame.dot:Show()
|
||
|
plotFrame.dot:ClearAllPoints()
|
||
|
plotFrame.dot:SetPoint("CENTER", plotFrame, "BOTTOMLEFT", xCoord, yCoord)
|
||
|
|
||
|
plotFrame.hoverLine:Show()
|
||
|
plotFrame.hoverLine:SetColorTexture(Theme.GetColor("INDICATOR_ALT"):GetFractionalRGBA())
|
||
|
plotFrame.hoverLine:ClearAllPoints()
|
||
|
plotFrame.hoverLine:SetPoint("TOP", plotFrame, "TOPLEFT", xCoord, 0)
|
||
|
plotFrame.hoverLine:SetPoint("BOTTOM", plotFrame, "BOTTOMLEFT", xCoord, 0)
|
||
|
|
||
|
plotFrame.hoverText:Show()
|
||
|
plotFrame.hoverText:SetWidth(1000)
|
||
|
plotFrame.hoverText:SetText(self._yFormatFunc(closestY, nil, true))
|
||
|
local textWidth = plotFrame.hoverText:GetStringWidth()
|
||
|
plotFrame.hoverText:SetWidth(textWidth)
|
||
|
plotFrame.hoverText:ClearAllPoints()
|
||
|
if xCoord - textWidth / 2 < 0 then
|
||
|
plotFrame.hoverText:SetPoint("BOTTOMLEFT", plotFrame, "TOPLEFT", 0, PLOT_Y_MARGIN)
|
||
|
elseif textWidth / 2 + xCoord > plotFrame:GetWidth() then
|
||
|
plotFrame.hoverText:SetPoint("BOTTOMRIGHT", plotFrame, "TOPRIGHT", 0, PLOT_Y_MARGIN)
|
||
|
else
|
||
|
plotFrame.hoverText:SetPoint("BOTTOM", plotFrame, "TOPLEFT", xCoord, PLOT_Y_MARGIN)
|
||
|
end
|
||
|
else
|
||
|
plotFrame.dot:Hide()
|
||
|
plotFrame.hoverLine:Hide()
|
||
|
plotFrame.hoverText:Hide()
|
||
|
end
|
||
|
|
||
|
if self._selectionStartX then
|
||
|
local startXCoord = self:_XValueToPlotCoord(self._selectionStartX)
|
||
|
local selectionMinX = min(startXCoord, xCoord)
|
||
|
local selectionMaxX = max(startXCoord, xCoord)
|
||
|
plotFrame.selectionBox:Show()
|
||
|
local r, g, b, a = Theme.GetColor("INDICATOR_ALT"):GetFractionalRGBA()
|
||
|
assert(a == 1)
|
||
|
plotFrame.selectionBox:SetColorTexture(r, g, b, SELECTION_ALPHA)
|
||
|
plotFrame.selectionBox:ClearAllPoints()
|
||
|
plotFrame.selectionBox:SetPoint("TOPLEFT", plotFrame, selectionMinX, 0)
|
||
|
plotFrame.selectionBox:SetPoint("BOTTOMRIGHT", plotFrame, "BOTTOMLEFT", selectionMaxX, 0)
|
||
|
else
|
||
|
plotFrame.selectionBox:Hide()
|
||
|
end
|
||
|
|
||
|
local isHovered = self._isMouseOver or self._selectionStartX
|
||
|
if not isHovered then
|
||
|
self:Draw()
|
||
|
ScriptWrapper.Clear(plotFrame, "OnUpdate")
|
||
|
end
|
||
|
if self._onHoverUpdate then
|
||
|
self:_onHoverUpdate(isHovered and closestX or nil)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function private.PlotFrameOnMouseDown(self, mouseButton)
|
||
|
if mouseButton ~= "LeftButton" then
|
||
|
return
|
||
|
end
|
||
|
assert(self._isMouseOver)
|
||
|
self._selectionStartX = self:_GetCursorClosestPoint()
|
||
|
end
|
||
|
|
||
|
function private.PlotFrameOnMouseUp(self, mouseButton)
|
||
|
if mouseButton ~= "LeftButton" then
|
||
|
return
|
||
|
end
|
||
|
local currentX = self:_GetCursorClosestPoint()
|
||
|
local startX = min(self._selectionStartX, currentX)
|
||
|
local endX = max(self._selectionStartX, currentX)
|
||
|
self._selectionStartX = nil
|
||
|
local plotFrame = self:_GetBaseFrame().plot
|
||
|
plotFrame.selectionBox:Hide()
|
||
|
|
||
|
if startX ~= endX and (startX ~= self._zoomStart or endX ~= self._zoomEnd) then
|
||
|
self._zoomStart = startX
|
||
|
self._zoomEnd = endX
|
||
|
self:Draw()
|
||
|
if self._onZoomChanged then
|
||
|
self:_onZoomChanged()
|
||
|
end
|
||
|
end
|
||
|
end
|