Last active
January 28, 2021 00:47
-
-
Save Meorawr/1b1e667990e1d051acbf7f4e5b86c46b to your computer and use it in GitHub Desktop.
Radar Chart Template
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local VertexCorner = tInvert({ | |
"TopLeft", | |
"BottomLeft", | |
"TopRight", | |
"BottomRight", | |
}); | |
local function CreateLinePool(parent, layer, subLayer) | |
local function CreateLine() | |
return parent:CreateLine(nil, layer, nil, subLayer); | |
end | |
return CreateObjectPool(CreateLine, TexturePool_HideAndClearAnchors); | |
end | |
local function GetLineIntersectionCoordinates(S1x, S1y, E1x, E1y, S2x, S2y, E2x, E2y) | |
local a1 = E1y - S1y; | |
local b1 = S1x - E1x; | |
local c1 = (a1 * S1x) + (b1 * S1y); | |
local a2 = E2y - S2y; | |
local b2 = S2x - E2x; | |
local c2 = (a2 * S2x) + (b2 * S2y); | |
local delta = (a1 * b2) - (a2 * b1); | |
local x = ((b2 * c1) - (b1 * c2)) / delta; | |
local y = ((a1 * c2) - (a2 * c1)) / delta; | |
return x, y; | |
end | |
local function GetPolygonVectorCoordinates(point, count) | |
local angle = (math.pi * 2) * (point - 1); | |
local x = math.sin(angle / count); | |
local y = math.cos(angle / count); | |
return x, y; | |
end | |
RadarChartPointMixin = {}; | |
function RadarChartPointMixin:OnHide() | |
self:SetScript("OnUpdate", nil); | |
end | |
function RadarChartPointMixin:OnMouseDown() | |
self:SetScript("OnUpdate", self.OnUpdate); | |
end | |
function RadarChartPointMixin:OnMouseUp() | |
self:SetScript("OnUpdate", nil); | |
end | |
function RadarChartPointMixin:OnUpdate() | |
local chartFrame = self:GetParent(); | |
local chartScale = chartFrame:GetEffectiveScale(); | |
local chartPoints = chartFrame:GetNumDataPoints(); | |
local chartRadius = chartFrame:GetRadius(); | |
-- This is essentially getting the closest point on a line segment AB | |
-- from point P, where AB is the segment from the centre of the parent | |
-- chart frame to the point at the edge of the radius, and P is the | |
-- mouse cursor. | |
-- | |
-- At the end, the distance represents a [0-1] floating point value for | |
-- how far along the line the closest point is, where 0 would mean we're | |
-- at the center and 1 is at the edge. | |
local Ax, Ay = chartFrame:GetCenter(); | |
local ABx, ABy = Vector2D_ScaleBy(chartRadius, GetPolygonVectorCoordinates(self:GetID(), chartPoints)); | |
local Px, Py = Vector2D_DivideBy(chartScale, GetCursorPosition()); | |
local APx, APy = Vector2D_Subtract(Px, Py, Ax, Ay); | |
local magnitude = Vector2D_GetLengthSquared(ABx, ABy); | |
local product = Vector2D_Dot(APx, APy, ABx, ABy); | |
local distance = Saturate(product / magnitude); | |
chartFrame:SetDataPointValue(self:GetID(), distance); | |
end | |
RadarChartMixin = {}; | |
RadarChartMixin.LabelAnchors = { | |
"BOTTOM", | |
"BOTTOMLEFT", | |
"LEFT", | |
"TOPLEFT", | |
"TOP", | |
"TOPRIGHT", | |
"RIGHT", | |
"BOTTOMRIGHT", | |
}; | |
function RadarChartMixin:OnLoad() | |
self.edgeColor = self.edgeColor or CreateColor(0, 0.4, 0.65, 0.75); | |
self.edgeThickness = self.edgeThickness or 2; | |
self.fillColor = self.fillColor or CreateColor(0.1647, 0.6353, 1, 0.75); | |
self.ringColor = self.ringColor or CreateColor(0.51, 0.38, 0.33, 0.5); | |
self.ringCount = self.ringCount or 5; | |
self.ringThickness = self.ringThickness or 2; | |
self.labelDistance = 16; | |
self.calculatedRadius = 0; | |
self.dataPoints = self.dataPoints or {}; | |
self.dirtyDataIndex = nil; | |
self.isDataDirty = false; | |
self.isRadiusDirty = false; | |
self.isRingDirty = false; | |
self.edgePool = CreateLinePool(self, "OVERLAY", 1); | |
self.fillPool = CreateTexturePool(self, "OVERLAY", 0); | |
self.labelPool = CreateFontStringPool(self, "OVERLAY", 2, self.labelTemplate or "GameFontHighlightOutline"); | |
self.ringPool = CreateLinePool(self, "ARTWORK"); | |
self.thumbPool = CreateFramePool("Button", self, self.thumbTemplate or "RadarChartPointTemplate"); | |
self.metricsString = self:CreateFontString(nil, nil, self.labelTemplate or "GameFontHighlightOutline"); | |
self:MarkVisualizationDirty(); | |
self:SetDataPoints({ | |
{ text = "Strength", value = 1, color = CreateColor(0.6353, 0.1647, 1, 0.75) }, | |
{ text = "Perception", value = 1 }, | |
{ text = "Endurance", value = 1, color = CreateColor(1, 0.1647, 0.6353, 0.75) }, | |
{ text = "Charisma", value = 1 }, | |
{ text = "Intelligence", value = 1, color = CreateColor(1, 0.6353, 0.1647, 0.75) }, | |
{ text = "Agility", value = 1, color = CreateColor(0.1647, 1, 0.6353, 0.75) }, | |
{ text = "Luck", value = 1, color = CreateColor(0.6353, 1, 0.1647, 0.75) }, | |
}) | |
end | |
function RadarChartMixin:OnShow() | |
self:MarkVisualizationDirty(); | |
end | |
function RadarChartMixin:OnSizeChanged() | |
self:MarkVisualizationDirty(); | |
end | |
function RadarChartMixin:OnUpdate() | |
self:UpdateVisualizationIfNecessary(); | |
end | |
function RadarChartMixin:AddDataPoint(pointData) | |
self:InsertDataPoint(#self.dataPoints + 1, pointData); | |
end | |
function RadarChartMixin:AddMultipleDataPoints(points) | |
self:InsertMultipleDataPoints(#self.dataPoints + 1, points); | |
end | |
function RadarChartMixin:GetDataPoint(pointIndex) | |
local point = self.dataPoints[pointIndex]; | |
if not point then | |
return nil; | |
end | |
return { | |
index = pointIndex, | |
text = point.text, | |
value = point.value, | |
color = point.color and CopyTable(point.color) or nil, | |
}; | |
end | |
function RadarChartMixin:GetNumDataPoints() | |
return #self.dataPoints; | |
end | |
function RadarChartMixin:InsertDataPoint(pointIndex, pointData) | |
assert(pointIndex > 0 and pointIndex <= (#self.dataPoints + 1), "point index out of range"); | |
local point = { | |
value = Saturate(pointData.value), | |
color = pointData.color and CopyTable(pointData.color) or nil, | |
text = tostring(pointData.text), | |
edge = self.edgePool:Acquire(), | |
fill = self.fillPool:Acquire(), | |
label = self.labelPool:Acquire(), | |
thumb = self.thumbPool:Acquire(), | |
}; | |
table.insert(self.dataPoints, pointIndex, point); | |
self:MarkVisualizationDirty(); | |
end | |
function RadarChartMixin:RemoveDataPoint(pointIndex) | |
local point = assert(table.remove(self.dataPoints, pointIndex), "point index out of range"); | |
self.edgePool:Release(point.edge); | |
self.fillPool:Release(point.fill); | |
self.labelPool:Release(point.label); | |
self.thumbPool:Release(point.thumb); | |
self:MarkVisualizationDirty(); | |
end | |
function RadarChartMixin:RemoveAllDataPoints() | |
self.dataPoints = {}; | |
self.edgePool:ReleaseAll(); | |
self.fillPool:ReleaseAll(); | |
self.labelPool:ReleaseAll(); | |
self.thumbPool:ReleaseAll(); | |
self:MarkVisualizationDirty(); | |
end | |
function RadarChartMixin:ReplaceDataPoint(pointIndex, pointData) | |
local point = assert(self.dataPoints[pointIndex], "point index out of range"); | |
point.value = Saturate(pointData.value); | |
point.color = pointData.color and CopyTable(pointData.color) or nil; | |
point.text = tostring(pointData.text); | |
self:MarkDataPointDirty(pointIndex); | |
self:MarkRadiusDirty(); | |
end | |
function RadarChartMixin:InsertMultipleDataPoints(pointIndex, points) | |
for i, pointData in ipairs(points) do | |
self:InsertDataPoint(pointIndex + (i - 1), pointData); | |
end | |
end | |
function RadarChartMixin:SetDataPoints(points) | |
self:RemoveAllDataPoints(); | |
self:AddMultipleDataPoints(points); | |
end | |
function RadarChartMixin:SetDataPointColor(pointIndex, pointColor) | |
local point = assert(self.dataPoints[pointIndex], "point index out of range"); | |
if point.color and pointColor and point.color:IsEqualTo(pointColor) then | |
return; | |
end | |
point.color = pointColor and CopyTable(pointColor) or nil; | |
self:MarkDataPointDirty(pointIndex); | |
end | |
function RadarChartMixin:SetDataPointText(pointIndex, pointText) | |
local point = assert(self.dataPoints[pointIndex], "point index out of range"); | |
if point.text == pointText then | |
return; | |
end | |
point.text = tostring(pointText); | |
self:MarkDataPointDirty(pointIndex); | |
self:MarkRadiusDirty(); | |
end | |
function RadarChartMixin:SetDataPointValue(pointIndex, pointValue) | |
local point = assert(self.dataPoints[pointIndex], "point index out of range"); | |
if point.value == pointValue then | |
return; | |
end | |
point.value = Saturate(pointValue); | |
self:MarkDataPointDirty(pointIndex); | |
end | |
function RadarChartMixin:GetEdgeColor() | |
return CopyTable(self.edgeColor); | |
end | |
function RadarChartMixin:SetEdgeColor(edgeColor) | |
self.edgeColor = CopyTable(edgeColor); | |
self:MarkDataDirty(); | |
end | |
function RadarChartMixin:GetEdgeThickness() | |
return self.edgeThickness; | |
end | |
function RadarChartMixin:SetEdgeThickness(edgeThickness) | |
self.edgeThickness = tonumber(edgeThickness) or 2; | |
self:MarkDataDirty(); | |
end | |
function RadarChartMixin:GetFillColor() | |
return CopyTable(self.fillColor); | |
end | |
function RadarChartMixin:SetFillColor(fillColor) | |
self.fillColor = CopyTable(fillColor); | |
self:MarkDataDirty(); | |
end | |
function RadarChartMixin:GetLabelDistance() | |
return self.labelDistance; | |
end | |
function RadarChartMixin:SetLabelDistance(labelDistance) | |
self.labelDistance = tonumber(labelDistance) or 16; | |
self:MarkRadiusDirty(); | |
end | |
function RadarChartMixin:GetRingColor() | |
return CopyTable(self.ringColor); | |
end | |
function RadarChartMixin:SetRingColor(ringColor) | |
self.ringColor = CopyTable(ringColor); | |
self:MarkRingDirty(); | |
end | |
function RadarChartMixin:GetRingThickness() | |
return self.ringThickness; | |
end | |
function RadarChartMixin:SetRingThickness(ringThickness) | |
self.ringThickness = tonumber(ringThickness) or 2; | |
self:MarkRingDirty(); | |
end | |
function RadarChartMixin:GetRingCount() | |
return self.ringCount; | |
end | |
function RadarChartMixin:SetRingCount(ringCount) | |
self.ringCount = tonumber(ringCount) or 5; | |
self:MarkRingDirty(); | |
end | |
function RadarChartMixin:MarkDataDirty() | |
self.dirtyDataIndex = nil; | |
self.isDataDirty = true; | |
end | |
function RadarChartMixin:MarkDataPointDirty(pointIndex) | |
if not self.isDataDirty and not self.dirtyDataIndex then | |
self.dirtyDataIndex = pointIndex; | |
else | |
-- Another point has already been marked as dirty. | |
self:MarkDataDirty(); | |
end | |
end | |
function RadarChartMixin:MarkRadiusDirty() | |
-- Marking the radius as dirty is the same as marking the whole | |
-- visualization dirty, since everything relies upon it. | |
self.isRadiusDirty = true; | |
self.dirtyDataIndex = nil; | |
self.isDataDirty = true; | |
self.isRingDirty = true; | |
end | |
function RadarChartMixin:MarkRingDirty() | |
self.isRingDirty = true; | |
end | |
function RadarChartMixin:MarkVisualizationDirty() | |
self:MarkDataDirty(); | |
self:MarkRadiusDirty(); | |
self:MarkRingDirty(); | |
end | |
function RadarChartMixin:GetRadius() | |
if self.isRadiusDirty then | |
local w, h = self:GetSize(); | |
w, h = Vector2D_DivideBy(2, w, h); | |
w, h = Vector2D_Subtract(w, h, self:CalculateLargestLabelSize()); | |
w, h = Vector2D_Subtract(w, h, self.labelDistance, self.labelDistance); | |
self.calculatedRadius = math.min(w, h); | |
end | |
self.isRadiusDirty = false; | |
return self.calculatedRadius; | |
end | |
function RadarChartMixin:UpdateVisualization() | |
self:UpdateRingVisualization(); | |
self:UpdateAllPointVisualizations(); | |
end | |
function RadarChartMixin:UpdateVisualizationIfNecessary() | |
if self.isRingDirty then | |
self:UpdateRingVisualization(); | |
end | |
if self.isDataDirty then | |
self:UpdateAllPointVisualizations(); | |
elseif self.dirtyDataIndex then | |
self:UpdatePointVisualization(self.dirtyDataIndex); | |
end | |
end | |
function RadarChartMixin:UpdateRingVisualization() | |
local pointCount = #self.dataPoints; | |
local radius = self:GetRadius(); | |
local ringColor = self.ringColor; | |
local ringCount = self.ringCount; | |
local ringThickness = self.ringThickness; | |
self.ringPool:ReleaseAll(); | |
self.isRingDirty = false; | |
-- | |
-- Center-to-Point Lines | |
-- | |
for pointIndex = 1, pointCount do | |
local Ax, Ay = GetPolygonVectorCoordinates(pointIndex, pointCount); | |
local line = self.ringPool:Acquire(); | |
line:SetStartPoint("CENTER"); | |
line:SetEndPoint("CENTER", Vector2D_ScaleBy(radius, Ax, Ay)); | |
line:SetColorTexture(ringColor:GetRGBA()); | |
line:SetThickness(ringThickness); | |
line:Show(); | |
end | |
-- | |
-- Point-to-Point Lines | |
-- | |
for ringIndex = 1, ringCount do | |
local distance = ringIndex / ringCount; | |
for pointIndex = 1, pointCount do | |
local nextIndex = Wrap(pointIndex + 1, pointCount); | |
local Ax, Ay = GetPolygonVectorCoordinates(pointIndex, pointCount); | |
local Bx, By = GetPolygonVectorCoordinates(nextIndex, pointCount); | |
local ring = self.ringPool:Acquire(); | |
ring:SetStartPoint("CENTER", Vector2D_ScaleBy(radius * distance, Ax, Ay)); | |
ring:SetEndPoint("CENTER", Vector2D_ScaleBy(radius * distance, Bx, By)); | |
ring:SetColorTexture(ringColor:GetRGBA()); | |
ring:SetThickness(ringThickness); | |
ring:Show(); | |
end | |
end | |
end | |
function RadarChartMixin:UpdateAllPointVisualizations() | |
self.isDataDirty = false; | |
for pointIndex = 1, #self.dataPoints do | |
self:UpdateSinglePointVisualization(pointIndex); | |
end | |
end | |
function RadarChartMixin:UpdatePointVisualization(pointIndex) | |
local pointCount = #self.dataPoints; | |
local prevIndex = Wrap(pointIndex - 1, pointCount); | |
local nextIndex = Wrap(pointIndex + 1, pointCount); | |
self:UpdateSinglePointVisualization(prevIndex); | |
self:UpdateSinglePointVisualization(pointIndex); | |
self:UpdateSinglePointVisualization(nextIndex); | |
end | |
function RadarChartMixin:UpdateSinglePointVisualization(pointIndex) | |
local pointCount = #self.dataPoints; | |
local point = self.dataPoints[pointIndex]; | |
local prevIndex = Wrap(pointIndex - 1, pointCount); | |
local prevPoint = self.dataPoints[prevIndex]; | |
local nextIndex = Wrap(pointIndex + 1, pointCount); | |
local nextPoint = self.dataPoints[nextIndex]; | |
local radius = self:GetRadius(); | |
if self.dirtyDataIndex == pointIndex then | |
self.dirtyDataIndex = nil; | |
end | |
-- | |
-- Coordinate Calculations | |
-- | |
-- A/B/C represent the vectors for the previous, current, and next | |
-- points respectively. The position of these vectors is relative to | |
-- the center of the chart, and represents the outermost edge for the | |
-- data point - or the border, if you want to think of it that way. | |
local Ax, Ay = GetPolygonVectorCoordinates(prevIndex, pointCount); | |
local Bx, By = GetPolygonVectorCoordinates(pointIndex, pointCount); | |
local Cx, Cy = GetPolygonVectorCoordinates(nextIndex, pointCount); | |
-- AV/BV/CV are the same as A/B/C except these represent the positions | |
-- of the actual value for this data point - and so will lie between the | |
-- center and their respective A/B/C value above. | |
local AVx, AVy = Vector2D_ScaleBy(prevPoint.value, Ax, Ay); | |
local BVx, BVy = Vector2D_ScaleBy(point.value, Bx, By); | |
local CVx, CVy = Vector2D_ScaleBy(nextPoint.value, Cx, Cy); | |
-- AB/BC are imaginary half points between vectors A/B and B/C | |
-- respectively, as if we had twice as many points as we do now. Their | |
-- location will be outside the circumcircle of our own polygon, but we | |
-- don't care as this is later used for a line-line intersection. | |
local ABx, ABy = GetPolygonVectorCoordinates((pointIndex - 1) * 2, pointCount * 2); | |
local BCx, BCy = GetPolygonVectorCoordinates(pointIndex * 2, pointCount * 2); | |
-- D/E represent the point at which the following lines intersect: | |
-- | |
-- Dx, Dy = Intersect(Line(Zero, AB), Line(AV, BV)) | |
-- Ex, Ey = Intersect(Line(Zero, BC), Line(BV, CV)) | |
-- | |
-- This is primarily used for fill coloring. | |
local Dx, Dy = GetLineIntersectionCoordinates(0, 0, ABx, ABy, AVx, AVy, BVx, BVy); | |
local Ex, Ey = GetLineIntersectionCoordinates(0, 0, BCx, BCy, BVx, BVy, CVx, CVy); | |
-- | |
-- Fill Region | |
-- | |
do | |
local fill = point.fill; | |
local color = point.color or self.fillColor; | |
-- Our approach to rendering is to use a quad with vertices offset | |
-- to our desired coordinates. | |
-- | |
-- The vertices need a constant offset applied based on their corner | |
-- in order to reset them to a (0, 0) location initially, which would | |
-- represent the center of the chart. | |
local TLx, TLy = Vector2D_Add( 1, -1, Dx, Dy); | |
local TRx, TRy = Vector2D_Add(-1, -1, BVx, BVy); | |
local BRx, BRy = Vector2D_Add(-1, 1, Ex, Ey); | |
local BLx, BLy = Vector2D_Add( 1, 1, 0, 0); | |
fill:ClearAllPoints(); | |
fill:SetColorTexture(color:GetRGBA()); | |
fill:SetPoint("CENTER"); | |
fill:SetSize(radius * 2, radius * 2); | |
fill:SetVertexOffset(VertexCorner.TopLeft, Vector2D_ScaleBy(radius, TLx, TLy)); | |
fill:SetVertexOffset(VertexCorner.TopRight, Vector2D_ScaleBy(radius, TRx, TRy)); | |
fill:SetVertexOffset(VertexCorner.BottomRight, Vector2D_ScaleBy(radius, BRx, BRy)); | |
fill:SetVertexOffset(VertexCorner.BottomLeft, Vector2D_ScaleBy(radius, BLx, BLy)); | |
fill:Show(); | |
end | |
-- | |
-- Edge Region | |
-- | |
do | |
local edge = point.edge; | |
local color = self.edgeColor; | |
local thickness = self.edgeThickness; | |
edge:SetStartPoint("CENTER", Vector2D_ScaleBy(radius, BVx, BVy)); | |
edge:SetEndPoint("CENTER", Vector2D_ScaleBy(radius, CVx, CVy)); | |
edge:SetColorTexture(color:GetRGBA()); | |
edge:SetThickness(thickness); | |
edge:Show(); | |
end | |
-- | |
-- Label Region | |
-- | |
do | |
local label = point.label; | |
local anchor = self:GetAnchorForPointLabel(pointIndex); | |
local text = point.text; | |
local fudge = 1 + (self.labelDistance / radius); | |
label:ClearAllPoints(); | |
label:SetPoint(anchor, self, "CENTER", Vector2D_ScaleBy(radius * fudge, Bx, By)); | |
label:SetText(text); | |
label:Show(); | |
end | |
-- | |
-- Thumb Region | |
-- | |
do | |
local thumb = point.thumb; | |
thumb:ClearAllPoints(); | |
thumb:SetPoint("CENTER", Vector2D_ScaleBy(radius, BVx, BVy)); | |
thumb:SetID(pointIndex); | |
thumb:Show(); | |
end | |
end | |
function RadarChartMixin:CalculateLargestLabelSize() | |
local metricsString = self.metricsString; | |
local maxWidth = 0; | |
local maxHeight = 0; | |
for _, point in ipairs(self.dataPoints) do | |
metricsString:SetText(point.text); | |
local width = metricsString:GetWrappedWidth(); | |
local height = metricsString:GetHeight(); | |
maxWidth = math.max(width, maxWidth); | |
maxHeight = math.max(height, maxHeight); | |
end | |
return maxWidth, maxHeight; | |
end | |
function RadarChartMixin:GetAnchorForPointLabel(pointIndex) | |
local pointCount = #self.dataPoints; | |
local anchorCount = #self.LabelAnchors; | |
local anchorIndex = math.floor(((pointIndex - 1) * (anchorCount / pointCount))) + 1; | |
return self.LabelAnchors[anchorIndex]; | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/"> | |
<Include file="RadarChart.lua"/> | |
<Frame name="RadarChartPointTemplate" virtual="true" mixin="RadarChartPointMixin" enableMouse="true"> | |
<Size x="16" y="16"/> | |
<Layers> | |
<Layer level="ARTWORK"> | |
<Texture parentKey="Icon" atlas="playerpartyblip" setAllPoints="true"/> | |
</Layer> | |
</Layers> | |
<Scripts> | |
<OnMouseDown method="OnMouseDown"/> | |
<OnMouseUp method="OnMouseUp"/> | |
<OnHide method="OnHide"/> | |
</Scripts> | |
</Frame> | |
<Frame name="RadarChartTemplate" virtual="true" mixin="RadarChartMixin"> | |
<Scripts> | |
<OnLoad method="OnLoad"/> | |
<OnShow method="UpdateVisualization"/> | |
<OnSizeChanged method="UpdateVisualization"/> | |
</Scripts> | |
</Frame> | |
<Frame name="ChartTest" parent="UIParent" toplevel="true" enableMouse="true"> | |
<Size x="459" y="707"/> | |
<Anchors> | |
<Anchor point="CENTER"/> | |
</Anchors> | |
<Layers> | |
<Layer level="BACKGROUND"> | |
<Texture atlas="questbg-oribos" useAtlasSize="true"/> | |
</Layer> | |
</Layers> | |
<Frames> | |
<Frame parentKey="Chart" inherits="RadarChartTemplate" setAllPoints="true"/> | |
</Frames> | |
</Frame> | |
</Ui> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment