// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using JetBrains.Annotations; using System; using System.Linq; using osu.Game.Graphics.Sprites; using osu.Framework.Utils; using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Framework.Graphics.Shapes; using osuTK; using static osu.Game.Users.User; namespace osu.Game.Overlays.Profile.Sections.Historical { public class ProfileLineChart : CompositeDrawable { private UserHistoryCount[] values; [NotNull] public UserHistoryCount[] Values { get => values; set { if (value.Length == 0) throw new ArgumentException("At least one value expected!", nameof(value)); graph.Values = values = value; createRowTicks(); createColumnTicks(); } } private readonly UserHistoryGraph graph; private readonly Container rowTicksContainer; private readonly Container columnTicksContainer; private readonly Container rowLinesContainer; private readonly Container columnLinesContainer; public ProfileLineChart() { RelativeSizeAxes = Axes.X; Height = 250; InternalChild = new GridContainer { RelativeSizeAxes = Axes.Both, ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension() }, RowDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.AutoSize) }, Content = new[] { new Drawable[] { rowTicksContainer = new Container { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X }, new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { new Container { RelativeSizeAxes = Axes.Both, Children = new[] { rowLinesContainer = new Container { RelativeSizeAxes = Axes.Both }, columnLinesContainer = new Container { RelativeSizeAxes = Axes.Both } } }, graph = new UserHistoryGraph { RelativeSizeAxes = Axes.Both } } } }, new[] { Empty(), columnTicksContainer = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Top = 10 } } } } }; } private void createRowTicks() { rowTicksContainer.Clear(); rowLinesContainer.Clear(); var min = values.Select(v => v.Count).Min(); var max = values.Select(v => v.Count).Max(); var tick = getTick(max - min, 6); // Prevent infinite loop in case if tick is zero if (tick == 0) tick = 1; double rollingRow = 0; while (rollingRow <= max) { if (rollingRow >= min) { var y = -Interpolation.ValueAt(rollingRow, 0, 1f, min, max); addRowTick(y, rollingRow); } rollingRow += tick; } } private void createColumnTicks() { columnTicksContainer.Clear(); columnLinesContainer.Clear(); var totalMonths = values.Length; int monthsPerTick = 1; if (totalMonths > 80) monthsPerTick = 12; else if (totalMonths >= 45) monthsPerTick = 3; else if (totalMonths > 20) monthsPerTick = 2; for (int i = 0; i < totalMonths; i += monthsPerTick) { var x = (float)i / (totalMonths - 1); addColumnTick(x, values[i].Date); } } private void addRowTick(float y, double value) { rowTicksContainer.Add(new TickText { Anchor = Anchor.BottomRight, Origin = Anchor.CentreRight, RelativePositionAxes = Axes.Y, Margin = new MarginPadding { Right = 3 }, Text = value.ToString("N0"), Font = OsuFont.GetFont(size: 12), Y = y }); rowLinesContainer.Add(new TickLine { Anchor = Anchor.BottomRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.X, RelativePositionAxes = Axes.Y, Height = 0.1f, EdgeSmoothness = Vector2.One, Y = y }); } private void addColumnTick(float x, DateTime value) { columnTicksContainer.Add(new TickText { Origin = Anchor.CentreLeft, RelativePositionAxes = Axes.X, Text = value.ToString("MMM yyyy"), Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Rotation = 45, X = x }); columnLinesContainer.Add(new TickLine { Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, RelativePositionAxes = Axes.X, Width = 0.1f, EdgeSmoothness = Vector2.One, X = x }); } private long getTick(long range, int maxTicksCount) { var value = (float)range / (maxTicksCount - 1); var exponent = Math.Floor(Math.Log10(value)); var fraction = value / Math.Pow(10, exponent); double niceFraction; if (fraction < 1.5) niceFraction = 1.0; else if (fraction < 3) niceFraction = 2.0; else if (fraction < 7) niceFraction = 5.0; else niceFraction = 10.0; return (long)(niceFraction * Math.Pow(10, exponent)); } private class TickText : OsuSpriteText { [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { Colour = colourProvider.Foreground1; } } private class TickLine : Box { [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { Colour = colourProvider.Background6; } } } }