【Unity】绘制折线图和柱状图

绘制折线图和柱状图,主要包括如下效果:

背景网格的绘制;

折线和拐点的绘制;

长方形柱的绘制(柱宽可以修改);

X/Y轴的标签绘制(标签的单位可以修改、X轴的间距可以修改);

鼠标移动到折线拐点/长方形柱是显示对应数值Tooltip;

成果展示

Scene部分

脚本部分

定义折线图和柱状图的接口

//折线和拐点、长方形柱相关
private interface IGraphVisual
{
    public IGraphVisualObject CreateGraphVisualObject(Vector2 graphPosition, float graphPositionWidth, string tooltipText);
}

//更新数据、显示Tooltip|、删除等
private interface IGraphVisualObject
{
    void SetGraphVisualObjectInfo(Vector2 graphPosition, float graphPositionWidth, string tooltipText);

    void CleanUp();
}

折线图的类

折线图的特殊之处是需要知道上一个的值和当前值,才能连成线。

private class LineChartVisual : IGraphVisual
{
    private RectTransform graphContainer;
    private Sprite dotSprite;
    private LineGraphVisualObject lastVisualObject;
    private Color dotColor;
    private Color dotConnectColor;

    public LineChartVisual(RectTransform graphContainer, Sprite dotSprite, Color dotColor, Color dotConnectColor)
    {
        this.graphContainer = graphContainer;
        this.lastVisualObject = null;
        this.dotSprite = dotSprite;
        this.dotColor = dotColor;
        this.dotConnectColor = dotConnectColor;
    }

    //绘制单个结点
    public IGraphVisualObject CreateGraphVisualObject(Vector2 graphPosition, float graphPositionWidth, string tooltipText)
    {
        List<GameObject> gameObjectList = new List<GameObject>();
        GameObject dotGameObject = CreateDot(graphPosition);
        gameObjectList.Add(dotGameObject);

        GameObject dotConnectionGameObject = null;
        if (lastVisualObject != null)
        {
            dotConnectionGameObject = CreateDotConnection(lastVisualObject.GetGraphPosition(), dotGameObject.GetComponent<RectTransform>().anchoredPosition);
            gameObjectList.Add(dotConnectionGameObject);
        }

        LineGraphVisualObject lineGraphVisualObject = new LineGraphVisualObject(dotGameObject, dotConnectionGameObject, lastVisualObject);
        lineGraphVisualObject.SetGraphVisualObjectInfo(graphPosition, graphPositionWidth, tooltipText);

        lastVisualObject = lineGraphVisualObject;
        return lineGraphVisualObject;
    }

    private GameObject CreateDot(Vector2 anchoredPosition)
    {
        GameObject gameObject = new GameObject("dot", typeof(Image));
        gameObject.transform.SetParent(graphContainer, false);
        gameObject.GetComponent<Image>().sprite = dotSprite;
        gameObject.GetComponent<Image>().color = dotColor;
        RectTransform rectTransform = gameObject.GetComponent<RectTransform>();
        rectTransform.anchoredPosition = anchoredPosition;
        rectTransform.sizeDelta = new Vector2(7, 7);
        rectTransform.anchorMin = new Vector2(0, 0);
        rectTransform.anchorMax = new Vector2(0, 0);

        gameObject.AddComponent<Button_UI>();
        return gameObject;
    }

    private GameObject CreateDotConnection(Vector2 dotPositionA, Vector2 dotPositionB)
    {
        GameObject gameObject = new GameObject("dotConnection", typeof(Image));
        gameObject.transform.SetParent(graphContainer, false);
        gameObject.GetComponent<Image>().color = dotConnectColor;
        RectTransform rectTransform = gameObject.GetComponent<RectTransform>();
        Vector2 dir = (dotPositionB - dotPositionA).normalized;
        float distance = Vector2.Distance(dotPositionA, dotPositionB);
        rectTransform.anchorMin = new Vector2(0, 0);
        rectTransform.anchorMax = new Vector2(0, 0);
        rectTransform.sizeDelta = new Vector2(distance, 2f);
        rectTransform.anchoredPosition = dotPositionA + dir * distance * .5f;
        rectTransform.localEulerAngles = new Vector3(0, 0, UtilsClass.GetAngleFromVectorFloat(dir));
        return gameObject;
    }

    public class LineGraphVisualObject : IGraphVisualObject
    {
        private event EventHandler OnChangedGraphVisualObjectInfo;

        private GameObject dotGameObject;
        private GameObject dotConnectionGameObject;
        private LineGraphVisualObject lastVisualObject;
        public LineGraphVisualObject(GameObject dotGameObject, GameObject dotConnectionObject, LineGraphVisualObject lastVisualObject)
        {
            this.dotGameObject = dotGameObject;
            this.dotConnectionGameObject = dotConnectionObject;
            this.lastVisualObject = lastVisualObject;

            if (lastVisualObject != null)
            {
                lastVisualObject.OnChangedGraphVisualObjectInfo += LastVisualObject_OnChangedGraphVisualObjectInfo;
            }
        }

        private void LastVisualObject_OnChangedGraphVisualObjectInfo(object sender, EventArgs e)
        {
            UpdateDotConnection();
        }

        public void SetGraphVisualObjectInfo(Vector2 graphPosition, float graphPositionWidth, string tooltipText)
        {
            RectTransform dotRectTransform = dotGameObject.GetComponent<RectTransform>();
            dotRectTransform.anchoredPosition = graphPosition;

            UpdateDotConnection();

            Button_UI lineBtnUi = dotGameObject.GetComponent<Button_UI>();
            lineBtnUi.MouseOverOnceFunc = () =>
            {
                ShowTooltip_Static(tooltipText, graphPosition);
            };

            lineBtnUi.MouseOutOnceFunc = () =>
            {
                HideTooltip_Static();
            };

            if (OnChangedGraphVisualObjectInfo != null) OnChangedGraphVisualObjectInfo(this, EventArgs.Empty);
        }

        public void CleanUp()
        {
            Destroy(dotConnectionGameObject);
            Destroy(dotGameObject);
        }

        public Vector2 GetGraphPosition()
        {
            RectTransform rectTransform = dotGameObject.GetComponent<RectTransform>();
            return rectTransform.anchoredPosition;
        }

        private void UpdateDotConnection()
        {
            if (dotConnectionGameObject != null)
            {
                RectTransform dotConnectionRectTransform = dotConnectionGameObject.GetComponent<RectTransform>();
                Vector2 dir = (lastVisualObject.GetGraphPosition() - GetGraphPosition()).normalized;
                float distance = Vector2.Distance(GetGraphPosition(), lastVisualObject.GetGraphPosition());
                dotConnectionRectTransform.anchorMin = new Vector2(0, 0);
                dotConnectionRectTransform.anchorMax = new Vector2(0, 0);
                dotConnectionRectTransform.sizeDelta = new Vector2(distance, 2f);
                dotConnectionRectTransform.anchoredPosition = GetGraphPosition() + dir * distance * .5f;
                dotConnectionRectTransform.localEulerAngles = new Vector3(0, 0, UtilsClass.GetAngleFromVectorFloat(dir));
            }
        }
    }
}

柱状图的类

柱状图中需要的是长方形柱的宽度

private class BarChartVisual : IGraphVisual
{

    private RectTransform graphContainer;
    private Color barColor;
    private float barWidthMultiplier;
    public BarChartVisual(RectTransform graphContainer, Color barColor, float barWidthMultipliter)
    {
        this.graphContainer = graphContainer;
        this.barColor = barColor;
        this.barWidthMultiplier = barWidthMultipliter;
    }

    public IGraphVisualObject CreateGraphVisualObject(Vector2 graphPosition, float graphPositionWidth, string tooltipText)
    {
        GameObject barGameObject = CreateBar(graphPosition, graphPositionWidth);

        BarChartVisualObject barChartVisualObject = new BarChartVisualObject(barGameObject, barWidthMultiplier);
        barChartVisualObject.SetGraphVisualObjectInfo(graphPosition, graphPositionWidth, tooltipText);

        return barChartVisualObject;
    }

    private GameObject CreateBar(Vector2 graphPosition, float barWidth)
    {
        GameObject gameObject = new GameObject("bar", typeof(Image));
        gameObject.transform.SetParent(graphContainer, false);
        gameObject.GetComponent<Image>().color = barColor;
        RectTransform rectTransform = gameObject.GetComponent<RectTransform>();
        rectTransform.anchoredPosition = new Vector2(graphPosition.x, 0f);
        rectTransform.sizeDelta = new Vector2(barWidth * barWidthMultiplier, graphPosition.y);
        rectTransform.anchorMin = new Vector2(0, 0);
        rectTransform.anchorMax = new Vector2(0, 0);
        rectTransform.pivot = new Vector2(.5f, 0f);

        gameObject.AddComponent<Button_UI>();
        return gameObject;
    }

    public class BarChartVisualObject : IGraphVisualObject
    {

        private GameObject barGameObject;
        private float barWidthMultiplier;

        public BarChartVisualObject(GameObject barGameObject, float barWidthMultiplier)
        {
            this.barGameObject = barGameObject;
            this.barWidthMultiplier = barWidthMultiplier;
        }
        public void SetGraphVisualObjectInfo(Vector2 graphPosition, float graphPositionWidth, string tooltipText)
        {
            RectTransform rectTransform = barGameObject.GetComponent<RectTransform>();
            rectTransform.anchoredPosition = new Vector2(graphPosition.x, 0f);
            rectTransform.sizeDelta = new Vector2(graphPositionWidth * barWidthMultiplier, graphPosition.y);

            Button_UI barBtnUi = barGameObject.GetComponent<Button_UI>();
            barBtnUi.MouseOverOnceFunc = () =>
            {
                ShowTooltip_Static(tooltipText, graphPosition);
            };
            barBtnUi.MouseOutOnceFunc = () =>
            {
                HideTooltip_Static();
            };
        }
        public void CleanUp()
        {
            Destroy(barGameObject);
        }
    }
}

绘制完整的图表

graphContainer = transform.Find("graphContainer").GetComponent<RectTransform>();
labelTemplateX = graphContainer.Find("labelTemplateX").GetComponent<RectTransform>();
labelTemplateY = graphContainer.Find("labelTemplateY").GetComponent<RectTransform>();

dashTemplateX = graphContainer.Find("dashTemplateX").GetComponent<RectTransform>();
dashTemplateY = graphContainer.Find("dashTemplateY").GetComponent<RectTransform>();

tooltipGameObject = graphContainer.Find("tooltip").gameObject;

startYScaleZero = true;

gameObjectList = new List<GameObject>();
graphVisualObjectList = new List<IGraphVisualObject>();

//绘制折线图
IGraphVisual lineChartVisual = new LineChartVisual(
    graphContainer, dotSprite, Color.green, new Color(1, 1, 1, .5f));
List<int> valueList = new List<int>() { 5, 98, 56, 45, 30, 22, 17, 15, 13, 17, 25, 37, 40, 36, 33, 5, 75, 75, 24, 88, 98, 55, 3, 46, 99 };
ShowGraph(valueList, lineChartVisual, valueList.Count, (_i) => "D" + (_i + 1), (_f) => "$" + Mathf.RoundToInt(_f));

//绘制柱状图
IGraphVisual barChartVisual = new BarChartVisual(graphContainer, Color.green, .8f);
List<int> valueList = new List<int>() { 5, 98, 56, 45, 30, 22, 17, 15, 13, 17, 25, 37, 40, 36, 33, 5, 75, 75, 24, 88, 98, 55, 3, 46, 99 };
ShowGraph(valueList, barChartVisual, valueList.Count, (_i) => "D" + (_i + 1), (_f) => "$" + Mathf.RoundToInt(_f));

需要图表数据List<int> valueList

图表的类IGraphVisual graphVisual

显示在页面中的数值个数maxVisibleValueAmount:页面中可以不显示全部的数据,此数据影响到X轴方向的间隔宽度

x/y轴文字标签的格式Func<int, string> getAxisLabelX Func<float, string> getAxisLabelY

private void ShowGraph(List<int> valueList, IGraphVisual graphVisual, int maxVisibleValueAmount = -1, Func<int, string> getAxisLabelX = null, Func<float, string> getAxisLabelY = null)
{
    this.valueList = valueList;
    this.graphVisual = graphVisual;

    this.getAxisLabelX = getAxisLabelX;
    this.getAxisLabelY = getAxisLabelY;

    //1、删除所有列表中的物体(网格、柱、点、线、标签等)
    foreach (var item in gameObjectList)
    {
        Destroy(item);
    }
    gameObjectList.Clear();

    foreach (var item in graphVisualObjectList)
    {
        item.CleanUp();
    }
    graphVisualObjectList.Clear();

    //2、定义默认状态下X/Y标签的格式
    if (getAxisLabelX == null)
    {
        getAxisLabelX = delegate (int _i) { return _i.ToString(); };
    }
    if (getAxisLabelY == null)
    {
        getAxisLabelY = delegate (float _f) { return Mathf.RoundToInt(_f).ToString(); };
    }

    //3、定义页面中显示的数据个数
    if (maxVisibleValueAmount <= 0)
    {
        maxVisibleValueAmount = valueList.Count;
    }
    if (maxVisibleValueAmount > valueList.Count)
    {
        maxVisibleValueAmount = valueList.Count;
    }
    this.maxVisibleValueAmount = maxVisibleValueAmount;

    //4、计算x/y取值范围和X轴的间隔宽度
    float graphWeight = graphContainer.sizeDelta.x;
    float graphHeight = graphContainer.sizeDelta.y;

    float yMaximum, yMinimum;
    CalculateYScale(out yMinimum, out yMaximum);

    xSize = graphWeight / (maxVisibleValueAmount + 1);

    //5、绘制Y轴背景网格、标签
    int xIndex = 0;
    //固定间隔显示Y轴
    int separatorCount = 10;
    for (int i = 0; i <= separatorCount; i++)
    {
        //标签
        RectTransform labelY = Instantiate(labelTemplateY);
        labelY.SetParent(graphContainer);
        labelY.gameObject.SetActive(true);
        float normalizedValue = i * 1f / separatorCount;
        labelY.anchoredPosition = new Vector2(-12.6f, normalizedValue * graphHeight);

        labelY.GetComponent<TextMeshProUGUI>().SetText(getAxisLabelY(yMinimum + normalizedValue * (yMaximum - yMinimum)));
        gameObjectList.Add(labelY.gameObject);

        //网格
        RectTransform dashY = Instantiate(dashTemplateY);
        dashY.SetParent(graphContainer);
        dashY.gameObject.SetActive(true);
        dashY.anchoredPosition = new Vector2(228.9f, normalizedValue * graphHeight);
        gameObjectList.Add(dashY.gameObject);
    }

    //5、绘制X轴背景网格、标签和柱/折线元素
    for (int i = Mathf.Max(valueList.Count - maxVisibleValueAmount, 0); i < valueList.Count; i++)
    {
        float xPosition = xSize + xIndex * xSize;
        float yPosition = ((valueList[i] - yMinimum) / (yMaximum - yMinimum)) * graphHeight;

        //标签
        RectTransform labelX = Instantiate(labelTemplateX);
        labelX.SetParent(graphContainer);
        labelX.gameObject.SetActive(true);
        labelX.anchoredPosition = new Vector2(xPosition, -4f);
        labelX.GetComponent<TextMeshProUGUI>().SetText(getAxisLabelX(i));
        gameObjectList.Add(labelX.gameObject);

        //网格
        RectTransform dashX = Instantiate(dashTemplateX);
        dashX.SetParent(graphContainer);
        dashX.gameObject.SetActive(true);
        dashX.anchoredPosition = new Vector2(xPosition, 123.6f);
        gameObjectList.Add(dashX.gameObject);

        //柱、拐点和线元素
        string tooltipText = getAxisLabelY(valueList[i]);
        graphVisualObjectList.Add(graphVisual.CreateGraphVisualObject(new Vector2(xPosition, yPosition), xSize, tooltipText));

        xIndex++;
    }
}


private void CalculateYScale(out float yMinimum, out float yMaximum)
{
    yMaximum = valueList[0];
    yMinimum = valueList[0];

    for (int i = Mathf.Max(valueList.Count - maxVisibleValueAmount, 0); i < valueList.Count; i++)
    {
        int value = valueList[i];
        if (value > yMaximum)
        {
            yMaximum = value;
        }
        if (value < yMinimum)
        {
            yMinimum = value;
        }
    }
    float yDifference = yMaximum - yMinimum;
    if (yDifference <= 0)
    {
        yDifference = 5;
    }

    yMaximum = yMaximum + (yDifference * 0.2f);
    yMinimum = yMinimum - (yDifference * 0.2f);

    if (startYScaleZero)
    {
        yMinimum = 0f;
    }
}

按钮切换柱状图/折线图

transform.Find("barChartBtn").GetComponent<Button_UI>().ClickFunc = () =>
{
    SetGraphVisual(barChartVisual);
};

transform.Find("lineChartBtn").GetComponent<Button_UI>().ClickFunc = () =>
{
    SetGraphVisual(lineChartVisual);
};
private void SetGraphVisual(IGraphVisual graphVisual)
{
    ShowGraph(this.valueList, graphVisual, this.maxVisibleValueAmount, this.getAxisLabelX, this.getAxisLabelY);
}

按钮放大缩小图表

transform.Find("decreaseVisibleAmountBtn").GetComponent<Button_UI>().ClickFunc = () =>
{
    DecreaseVisibleAmount();
};
transform.Find("increaseVisibleAmountBtn").GetComponent<Button_UI>().ClickFunc = () =>
{
    IncreaseVisibleAmount();
};
private void IncreaseVisibleAmount()
{
    ShowGraph(this.valueList, this.graphVisual, this.maxVisibleValueAmount + 1, this.getAxisLabelX, this.getAxisLabelY);
}
private void DecreaseVisibleAmount()
{
    ShowGraph(this.valueList, this.graphVisual, this.maxVisibleValueAmount - 1, this.getAxisLabelX, this.getAxisLabelY);
}

按钮改变Y标签单位

transform.Find("dollorBtn").GetComponent<Button_UI>().ClickFunc = () =>
{
    SetAxisLabelY((_f) => "$" + Mathf.RoundToInt(_f));
};
transform.Find("rnbBtn").GetComponent<Button_UI>().ClickFunc = () =>
{
    SetAxisLabelY((_f) => "¥" + Mathf.RoundToInt(_f));
};
private void SetAxisLabelY(Func<float, string> getAxisLabelY)
{
    ShowGraph(this.valueList, this.graphVisual, this.maxVisibleValueAmount, this.getAxisLabelX, getAxisLabelY);
}

tooltip的显示和隐藏

private static void ShowTooltip_Static(string tooltipText, Vector2 anchoredPosition)
{
    instance.ShowTooltip(tooltipText, anchoredPosition);
}

private void ShowTooltip(string tooltipText, Vector2 anchoredPosition)
{
    tooltipGameObject.SetActive(true);

    tooltipGameObject.GetComponent<RectTransform>().anchoredPosition = anchoredPosition;

    TextMeshProUGUI tooltipUIText = tooltipGameObject.transform.Find("text").GetComponent<TextMeshProUGUI>();
    tooltipUIText.SetText(tooltipText);

    float textPaddingSize = 4f;
    Vector2 backgroundSize = tooltipUIText.GetPreferredValues() + 2 * (new Vector2(textPaddingSize, textPaddingSize));

    tooltipGameObject.transform.Find("background").GetComponent<RectTransform>().sizeDelta = backgroundSize;

    tooltipGameObject.transform.SetAsLastSibling();
}

private static void HideTooltip_Static()
{
    instance.HideTooltip();
}

private void HideTooltip()
{
    tooltipGameObject.SetActive(false);
}

完整代码

public class Window_Graph : MonoBehaviour
{
    private static Window_Graph instance;

    [SerializeField] private Sprite dotSprite;
    private RectTransform graphContainer;
    private RectTransform labelTemplateX;
    private RectTransform labelTemplateY;
    private RectTransform dashTemplateX;
    private RectTransform dashTemplateY;

    private GameObject tooltipGameObject;

    private List<GameObject> gameObjectList;
    private List<IGraphVisualObject> graphVisualObjectList;

    private List<int> valueList;
    private IGraphVisual graphVisual;
    private int maxVisibleValueAmount;
    private Func<int, string> getAxisLabelX;
    private Func<float, string> getAxisLabelY;

    private float xSize;
    private bool startYScaleZero;
    private void Awake()
    {
        instance = this;

        graphContainer = transform.Find("graphContainer").GetComponent<RectTransform>();

        labelTemplateX = graphContainer.Find("labelTemplateX").GetComponent<RectTransform>();
        labelTemplateY = graphContainer.Find("labelTemplateY").GetComponent<RectTransform>();

        dashTemplateX = graphContainer.Find("dashTemplateX").GetComponent<RectTransform>();
        dashTemplateY = graphContainer.Find("dashTemplateY").GetComponent<RectTransform>();

        tooltipGameObject = graphContainer.Find("tooltip").gameObject;

        startYScaleZero = true;

        gameObjectList = new List<GameObject>();
        graphVisualObjectList = new List<IGraphVisualObject>();
        IGraphVisual lineChartVisual = new LineChartVisual(graphContainer, dotSprite, Color.green, new Color(1, 1, 1, .5f));
        IGraphVisual barChartVisual = new BarChartVisual(graphContainer, Color.green, .8f);
        List<int> valueList = new List<int>() { 5, 98, 56, 45, 30, 22, 17, 15, 13, 17, 25, 37, 40, 36, 33, 5, 75, 75, 24, 88, 98, 55, 3, 46, 99 };
        ShowGraph(valueList, barChartVisual, valueList.Count, (_i) => "D" + (_i + 1), (_f) => "$" + Mathf.RoundToInt(_f));
        transform.Find("barChartBtn").GetComponent<Button_UI>().ClickFunc = () =>
        {
            SetGraphVisual(barChartVisual);
        };

        transform.Find("lineChartBtn").GetComponent<Button_UI>().ClickFunc = () =>
        {
            SetGraphVisual(lineChartVisual);
        };

        //decreaseVisibleAmountBtn
        transform.Find("decreaseVisibleAmountBtn").GetComponent<Button_UI>().ClickFunc = () =>
        {
            DecreaseVisibleAmount();
        };
        transform.Find("increaseVisibleAmountBtn").GetComponent<Button_UI>().ClickFunc = () =>
        {
            IncreaseVisibleAmount();
        };

        transform.Find("dollorBtn").GetComponent<Button_UI>().ClickFunc = () =>
        {
            SetAxisLabelY((_f) => "$" + Mathf.RoundToInt(_f));
        };
        transform.Find("rnbBtn").GetComponent<Button_UI>().ClickFunc = () =>
        {
            SetAxisLabelY((_f) => "¥" + Mathf.RoundToInt(_f));
        };
    }

    private static void ShowTooltip_Static(string tooltipText, Vector2 anchoredPosition)
    {
        instance.ShowTooltip(tooltipText, anchoredPosition);
    }

    private void ShowTooltip(string tooltipText, Vector2 anchoredPosition)
    {
        tooltipGameObject.SetActive(true);

        tooltipGameObject.GetComponent<RectTransform>().anchoredPosition = anchoredPosition;

        TextMeshProUGUI tooltipUIText = tooltipGameObject.transform.Find("text").GetComponent<TextMeshProUGUI>();
        tooltipUIText.SetText(tooltipText);

        float textPaddingSize = 4f;
        Vector2 backgroundSize = tooltipUIText.GetPreferredValues() + 2 * (new Vector2(textPaddingSize, textPaddingSize));

        tooltipGameObject.transform.Find("background").GetComponent<RectTransform>().sizeDelta = backgroundSize;

        tooltipGameObject.transform.SetAsLastSibling();
    }

    private static void HideTooltip_Static()
    {
        instance.HideTooltip();
    }

    private void HideTooltip()
    {
        tooltipGameObject.SetActive(false);
    }

    private void SetAxisLabelX(Func<int, string> getAxisLabelX)
    {
        ShowGraph(this.valueList, this.graphVisual, this.maxVisibleValueAmount, getAxisLabelX, this.getAxisLabelY);
    }

    private void SetAxisLabelY(Func<float, string> getAxisLabelY)
    {
        ShowGraph(this.valueList, this.graphVisual, this.maxVisibleValueAmount, this.getAxisLabelX, getAxisLabelY);
    }

    private void IncreaseVisibleAmount()
    {
        ShowGraph(this.valueList, this.graphVisual, this.maxVisibleValueAmount + 1, this.getAxisLabelX, this.getAxisLabelY);
    }
    private void DecreaseVisibleAmount()
    {
        ShowGraph(this.valueList, this.graphVisual, this.maxVisibleValueAmount - 1, this.getAxisLabelX, this.getAxisLabelY);
    }

    private void SetGraphVisual(IGraphVisual graphVisual)
    {
        ShowGraph(this.valueList, graphVisual, this.maxVisibleValueAmount, this.getAxisLabelX, this.getAxisLabelY);
    }

    private void ShowGraph(List<int> valueList, IGraphVisual graphVisual, int maxVisibleValueAmount = -1, Func<int, string> getAxisLabelX = null, Func<float, string> getAxisLabelY = null)
    {
        this.valueList = valueList;
        this.graphVisual = graphVisual;

        this.getAxisLabelX = getAxisLabelX;
        this.getAxisLabelY = getAxisLabelY;

        foreach (var item in gameObjectList)
        {
            Destroy(item);
        }
        gameObjectList.Clear();

        foreach (var item in graphVisualObjectList)
        {
            item.CleanUp();
        }
        graphVisualObjectList.Clear();

        if (getAxisLabelX == null)
        {
            getAxisLabelX = delegate (int _i) { return _i.ToString(); };
        }
        if (getAxisLabelY == null)
        {
            getAxisLabelY = delegate (float _f) { return Mathf.RoundToInt(_f).ToString(); };
        }

        if (maxVisibleValueAmount <= 0)
        {
            maxVisibleValueAmount = valueList.Count;
        }
        if (maxVisibleValueAmount > valueList.Count)
        {
            maxVisibleValueAmount = valueList.Count;
        }
        this.maxVisibleValueAmount = maxVisibleValueAmount;

        float graphWeight = graphContainer.sizeDelta.x;
        float graphHeight = graphContainer.sizeDelta.y;

        float yMaximum, yMinimum;
        CalculateYScale(out yMinimum, out yMaximum);

        xSize = graphWeight / (maxVisibleValueAmount + 1);

        int xIndex = 0;
        //固定间隔显示Y轴
        int separatorCount = 10;
        for (int i = 0; i <= separatorCount; i++)
        {
            RectTransform labelY = Instantiate(labelTemplateY);
            labelY.SetParent(graphContainer);
            labelY.gameObject.SetActive(true);
            float normalizedValue = i * 1f / separatorCount;
            labelY.anchoredPosition = new Vector2(-12.6f, normalizedValue * graphHeight);

            labelY.GetComponent<TextMeshProUGUI>().SetText(getAxisLabelY(yMinimum + normalizedValue * (yMaximum - yMinimum)));
            gameObjectList.Add(labelY.gameObject);

            RectTransform dashY = Instantiate(dashTemplateY);
            dashY.SetParent(graphContainer);
            dashY.gameObject.SetActive(true);
            dashY.anchoredPosition = new Vector2(228.9f, normalizedValue * graphHeight);
            gameObjectList.Add(dashY.gameObject);
        }

        for (int i = Mathf.Max(valueList.Count - maxVisibleValueAmount, 0); i < valueList.Count; i++)
        {
            float xPosition = xSize + xIndex * xSize;
            float yPosition = ((valueList[i] - yMinimum) / (yMaximum - yMinimum)) * graphHeight;

            RectTransform labelX = Instantiate(labelTemplateX);
            labelX.SetParent(graphContainer);
            labelX.gameObject.SetActive(true);
            labelX.anchoredPosition = new Vector2(xPosition, -4f);
            labelX.GetComponent<TextMeshProUGUI>().SetText(getAxisLabelX(i));
            gameObjectList.Add(labelX.gameObject);

            RectTransform dashX = Instantiate(dashTemplateX);
            dashX.SetParent(graphContainer);
            dashX.gameObject.SetActive(true);
            dashX.anchoredPosition = new Vector2(xPosition, 123.6f);
            gameObjectList.Add(dashX.gameObject);

            string tooltipText = getAxisLabelY(valueList[i]);
            graphVisualObjectList.Add(graphVisual.CreateGraphVisualObject(new Vector2(xPosition, yPosition), xSize, tooltipText));

            xIndex++;
        }
    }

    private void UpdateValue(int index, int value)
    {
        valueList[index] = value;

        float graphWeight = graphContainer.sizeDelta.x;
        float graphHeight = graphContainer.sizeDelta.y;

        float yMaximum, yMinimum;
        CalculateYScale(out yMinimum, out yMaximum);

        int xIndex = 0;
        for (int i = Mathf.Max(valueList.Count - maxVisibleValueAmount, 0); i < valueList.Count; i++)
        {
            float xPosition = xSize + xIndex * xSize;
            float yPosition = ((valueList[i] - yMinimum) / (yMaximum - yMinimum)) * graphHeight;
            string tooltipText = getAxisLabelY(valueList[i]);

            graphVisualObjectList[index].SetGraphVisualObjectInfo(new Vector2(xPosition, yPosition), xSize, tooltipText);

            xIndex++;
        }


    }

    private void CalculateYScale(out float yMinimum, out float yMaximum)
    {
        yMaximum = valueList[0];
        yMinimum = valueList[0];

        for (int i = Mathf.Max(valueList.Count - maxVisibleValueAmount, 0); i < valueList.Count; i++)
        {
            int value = valueList[i];
            if (value > yMaximum)
            {
                yMaximum = value;
            }
            if (value < yMinimum)
            {
                yMinimum = value;
            }
        }
        float yDifference = yMaximum - yMinimum;
        if (yDifference <= 0)
        {
            yDifference = 5;
        }

        yMaximum = yMaximum + (yDifference * 0.2f);
        yMinimum = yMinimum - (yDifference * 0.2f);

        if (startYScaleZero)
        {
            yMinimum = 0f;
        }
    }

    private interface IGraphVisual
    {
        public IGraphVisualObject CreateGraphVisualObject(Vector2 graphPosition, float graphPositionWidth, string tooltipText);
    }

    private interface IGraphVisualObject
    {
        void SetGraphVisualObjectInfo(Vector2 graphPosition, float graphPositionWidth, string tooltipText);

        void CleanUp();
    }

    private class LineChartVisual : IGraphVisual
    {

        private RectTransform graphContainer;
        private Sprite dotSprite;
        private LineGraphVisualObject lastVisualObject;
        private Color dotColor;
        private Color dotConnectColor;

        public LineChartVisual(RectTransform graphContainer, Sprite dotSprite, Color dotColor, Color dotConnectColor)
        {
            this.graphContainer = graphContainer;
            this.lastVisualObject = null;
            this.dotSprite = dotSprite;
            this.dotColor = dotColor;
            this.dotConnectColor = dotConnectColor;
        }

        public IGraphVisualObject CreateGraphVisualObject(Vector2 graphPosition, float graphPositionWidth, string tooltipText)
        {
            List<GameObject> gameObjectList = new List<GameObject>();
            GameObject dotGameObject = CreateDot(graphPosition);
            gameObjectList.Add(dotGameObject);

            GameObject dotConnectionGameObject = null;
            if (lastVisualObject != null)
            {
                dotConnectionGameObject = CreateDotConnection(lastVisualObject.GetGraphPosition(), dotGameObject.GetComponent<RectTransform>().anchoredPosition);
                gameObjectList.Add(dotConnectionGameObject);
            }

            LineGraphVisualObject lineGraphVisualObject = new LineGraphVisualObject(dotGameObject, dotConnectionGameObject, lastVisualObject);
            lineGraphVisualObject.SetGraphVisualObjectInfo(graphPosition, graphPositionWidth, tooltipText);

            lastVisualObject = lineGraphVisualObject;
            return lineGraphVisualObject;
        }

        private GameObject CreateDot(Vector2 anchoredPosition)
        {
            GameObject gameObject = new GameObject("dot", typeof(Image));
            gameObject.transform.SetParent(graphContainer, false);
            gameObject.GetComponent<Image>().sprite = dotSprite;
            gameObject.GetComponent<Image>().color = dotColor;
            RectTransform rectTransform = gameObject.GetComponent<RectTransform>();
            rectTransform.anchoredPosition = anchoredPosition;
            rectTransform.sizeDelta = new Vector2(7, 7);
            rectTransform.anchorMin = new Vector2(0, 0);
            rectTransform.anchorMax = new Vector2(0, 0);

            gameObject.AddComponent<Button_UI>();
            return gameObject;
        }

        private GameObject CreateDotConnection(Vector2 dotPositionA, Vector2 dotPositionB)
        {
            GameObject gameObject = new GameObject("dotConnection", typeof(Image));
            gameObject.transform.SetParent(graphContainer, false);
            gameObject.GetComponent<Image>().color = dotConnectColor;
            RectTransform rectTransform = gameObject.GetComponent<RectTransform>();
            Vector2 dir = (dotPositionB - dotPositionA).normalized;
            float distance = Vector2.Distance(dotPositionA, dotPositionB);
            rectTransform.anchorMin = new Vector2(0, 0);
            rectTransform.anchorMax = new Vector2(0, 0);
            rectTransform.sizeDelta = new Vector2(distance, 2f);
            rectTransform.anchoredPosition = dotPositionA + dir * distance * .5f;
            rectTransform.localEulerAngles = new Vector3(0, 0, UtilsClass.GetAngleFromVectorFloat(dir));
            return gameObject;
        }

        public class LineGraphVisualObject : IGraphVisualObject
        {
            private event EventHandler OnChangedGraphVisualObjectInfo;

            private GameObject dotGameObject;
            private GameObject dotConnectionGameObject;
            private LineGraphVisualObject lastVisualObject;
            public LineGraphVisualObject(GameObject dotGameObject, GameObject dotConnectionObject, LineGraphVisualObject lastVisualObject)
            {
                this.dotGameObject = dotGameObject;
                this.dotConnectionGameObject = dotConnectionObject;
                this.lastVisualObject = lastVisualObject;

                if (lastVisualObject != null)
                {
                    lastVisualObject.OnChangedGraphVisualObjectInfo += LastVisualObject_OnChangedGraphVisualObjectInfo;
                }
            }

            private void LastVisualObject_OnChangedGraphVisualObjectInfo(object sender, EventArgs e)
            {
                UpdateDotConnection();
            }

            public void SetGraphVisualObjectInfo(Vector2 graphPosition, float graphPositionWidth, string tooltipText)
            {
                RectTransform dotRectTransform = dotGameObject.GetComponent<RectTransform>();
                dotRectTransform.anchoredPosition = graphPosition;

                UpdateDotConnection();

                Button_UI lineBtnUi = dotGameObject.GetComponent<Button_UI>();
                lineBtnUi.MouseOverOnceFunc = () =>
                {
                    ShowTooltip_Static(tooltipText, graphPosition);
                };

                lineBtnUi.MouseOutOnceFunc = () =>
                {
                    HideTooltip_Static();
                };

                if (OnChangedGraphVisualObjectInfo != null) OnChangedGraphVisualObjectInfo(this, EventArgs.Empty);
            }

            public void CleanUp()
            {
                Destroy(dotConnectionGameObject);
                Destroy(dotGameObject);
            }

            public Vector2 GetGraphPosition()
            {
                RectTransform rectTransform = dotGameObject.GetComponent<RectTransform>();
                return rectTransform.anchoredPosition;
            }

            private void UpdateDotConnection()
            {
                if (dotConnectionGameObject != null)
                {
                    RectTransform dotConnectionRectTransform = dotConnectionGameObject.GetComponent<RectTransform>();
                    Vector2 dir = (lastVisualObject.GetGraphPosition() - GetGraphPosition()).normalized;
                    float distance = Vector2.Distance(GetGraphPosition(), lastVisualObject.GetGraphPosition());
                    dotConnectionRectTransform.anchorMin = new Vector2(0, 0);
                    dotConnectionRectTransform.anchorMax = new Vector2(0, 0);
                    dotConnectionRectTransform.sizeDelta = new Vector2(distance, 2f);
                    dotConnectionRectTransform.anchoredPosition = GetGraphPosition() + dir * distance * .5f;
                    dotConnectionRectTransform.localEulerAngles = new Vector3(0, 0, UtilsClass.GetAngleFromVectorFloat(dir));
                }
            }
        }
    }

    private class BarChartVisual : IGraphVisual
    {

        private RectTransform graphContainer;
        private Color barColor;
        private float barWidthMultiplier;
        public BarChartVisual(RectTransform graphContainer, Color barColor, float barWidthMultipliter)
        {
            this.graphContainer = graphContainer;
            this.barColor = barColor;
            this.barWidthMultiplier = barWidthMultipliter;
        }

        public IGraphVisualObject CreateGraphVisualObject(Vector2 graphPosition, float graphPositionWidth, string tooltipText)
        {
            GameObject barGameObject = CreateBar(graphPosition, graphPositionWidth);

            BarChartVisualObject barChartVisualObject = new BarChartVisualObject(barGameObject, barWidthMultiplier);
            barChartVisualObject.SetGraphVisualObjectInfo(graphPosition, graphPositionWidth, tooltipText);

            return barChartVisualObject;
        }

        private GameObject CreateBar(Vector2 graphPosition, float barWidth)
        {
            GameObject gameObject = new GameObject("bar", typeof(Image));
            gameObject.transform.SetParent(graphContainer, false);
            gameObject.GetComponent<Image>().color = barColor;
            RectTransform rectTransform = gameObject.GetComponent<RectTransform>();
            rectTransform.anchoredPosition = new Vector2(graphPosition.x, 0f);
            rectTransform.sizeDelta = new Vector2(barWidth * barWidthMultiplier, graphPosition.y);
            rectTransform.anchorMin = new Vector2(0, 0);
            rectTransform.anchorMax = new Vector2(0, 0);
            rectTransform.pivot = new Vector2(.5f, 0f);

            gameObject.AddComponent<Button_UI>();
            return gameObject;
        }

        public class BarChartVisualObject : IGraphVisualObject
        {

            private GameObject barGameObject;
            private float barWidthMultiplier;

            public BarChartVisualObject(GameObject barGameObject, float barWidthMultiplier)
            {
                this.barGameObject = barGameObject;
                this.barWidthMultiplier = barWidthMultiplier;
            }
            public void SetGraphVisualObjectInfo(Vector2 graphPosition, float graphPositionWidth, string tooltipText)
            {
                RectTransform rectTransform = barGameObject.GetComponent<RectTransform>();
                rectTransform.anchoredPosition = new Vector2(graphPosition.x, 0f);
                rectTransform.sizeDelta = new Vector2(graphPositionWidth * barWidthMultiplier, graphPosition.y);

                Button_UI barBtnUi = barGameObject.GetComponent<Button_UI>();
                barBtnUi.MouseOverOnceFunc = () =>
                {
                    ShowTooltip_Static(tooltipText, graphPosition);
                };
                barBtnUi.MouseOutOnceFunc = () =>
                {
                    HideTooltip_Static();
                };
            }
            public void CleanUp()
            {
                Destroy(barGameObject);
            }
        }


    }
}

posted @ 2024-09-25 09:06  Sitar  阅读(58)  评论(0编辑  收藏  举报