Консольная диаграмма

Мне нужен способ нарисовать Dictionary<int,int> в консольном приложении, например

Dictionary<int, int> chartList = new Dictionary<int, int>()
{
        {50,31}, // x = 50, y = 31
        {71,87},
        {25,66},
        {94,15},
        {33,94}
};
DrawChart(chartList);

должно получиться нечто вроде

введите описание изображения здесь

Я зашел так далеко, но я застрял в методе IsHit, который определяет, следует ли в текущих координатах установить точку или нет. Может ли кто-нибудь помочь мне в этот момент? Он всегда возвращает true.

public static void DrawChart(Dictionary<int, int> dict)
{
    int consoleWidth = 78;
    int consoleHeight = 20;

    Console.WriteLine(dict.Max(x => x.Key).ToString());

    Func<int, int, bool> IsHit = (hx, hy) => dict.Any(dct => dct.Key / dict.Max(x => x.Key) == hx / dict.Max(x => x.Key) && dct.Value / dict.Max(x => x.Value) == hy / dict.Max(x => x.Value));

    for (int i = 0; i < consoleHeight; i++)
    {
        Console.Write(i == 0 ? '┌' : '│');
        for (int j = 0; j < consoleWidth; j++)
        {
            int actualheight = i * 2;

            if (IsHit(j, actualheight) && IsHit(j, actualheight + 1))
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.BackgroundColor = ConsoleColor.Black;
                Console.Write('█');
            }
            else if (IsHit(j, actualheight))
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.BackgroundColor = ConsoleColor.Black;
                Console.Write('▀');
            }
            else if (IsHit(j, actualheight + 1))
            {
                Console.ForegroundColor = ConsoleColor.Black;
                Console.BackgroundColor = ConsoleColor.Red;
                Console.Write('▀');
            }
        }
        Console.ResetColor();
        Console.WriteLine();
    }
    Console.WriteLine('└' + new string('─', (consoleWidth / 2) - 1) + '┴' + new string('─', (consoleWidth / 2) - 1) + '┘');
    Console.Write((dict.Min(x => x.Key) + "/" + dict.Min(x => x.Value)).PadRight(consoleWidth / 3));
    Console.Write((dict.Max(x => x.Value) / 2).ToString().PadLeft(consoleWidth / 3 / 2).PadRight(consoleWidth / 3));
    Console.WriteLine(dict.Max(x => x.Value).ToString().PadLeft(consoleWidth / 3));
}

Ответ 1

Код ниже должен дать вам некоторую идею. Сначала нужно ввести Point, потому что работа с параметрами Dictionary и Key и Value вместо обычных имен, таких как X и Y, является кошмаром. Кроме того, в словаре вы не можете хранить несколько точек с одинаковой координатой X, что мало смысла.

public struct Point {
    public Point(int x, int y) {
       this.X = x;
        this.Y = y;
    }

    public int X { get; }
    public int Y { get; }
}

Затем немного изменен DrawChart:

    public static void DrawChart(List<Point> dict)
    {
        int consoleWidth = 78;
        int consoleHeight = 20;
        int actualConsoleHeight = consoleHeight * 2;
        var minX = dict.Min(c => c.X);
        var minY = dict.Min(c => c.Y);            
        var maxX = dict.Max(c => c.X);
        var maxY = dict.Max(c => c.Y);

        Console.WriteLine(maxX);
        // normalize points to new coordinates
        var normalized = dict.
            Select(c => new Point(c.X - minX, c.Y - minY)).
            Select(c => new Point((int)Math.Round((float) (c.X) / (maxX - minX) * (consoleWidth - 1)), (int)Math.Round((float) (c.Y) / (maxY - minY) * (actualConsoleHeight - 1)))).ToArray();
        Func<int, int, bool> IsHit = (hx, hy) => {
            return normalized.Any(c => c.X == hx && c.Y == hy);
        };

        for (int y = actualConsoleHeight - 1; y > 0; y -= 2)
        {
            Console.Write(y == actualConsoleHeight - 1 ? '┌' : '│');
            for (int x = 0; x < consoleWidth; x++)
            {
                bool hitTop = IsHit(x, y);
                bool hitBottom = IsHit(x, y - 1);                    
                if (hitBottom && hitTop)
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.BackgroundColor = ConsoleColor.Black;
                    Console.Write('█');
                }
                else if (hitTop)
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.BackgroundColor = ConsoleColor.Black;
                    Console.Write('▀');
                }
                else if (hitBottom)
                {
                    Console.ForegroundColor = ConsoleColor.Black;
                    Console.BackgroundColor = ConsoleColor.Red;
                    Console.Write('▀');
                }
                else
                {
                    Console.ForegroundColor = ConsoleColor.Black;
                    Console.BackgroundColor = ConsoleColor.Black;
                    Console.Write('▀');
                }                    
            }                
            Console.ResetColor();
            Console.WriteLine();
        }
        Console.WriteLine('└' + new string('─', (consoleWidth / 2) - 1) + '┴' + new string('─', (consoleWidth / 2) - 1) + '┘');
        Console.Write((dict.Min(x => x.X) + "/" + dict.Min(x => x.Y)).PadRight(consoleWidth / 3));
        Console.Write((dict.Max(x => x.Y) / 2).ToString().PadLeft(consoleWidth / 3 / 2).PadRight(consoleWidth / 3));
        Console.WriteLine(dict.Max(x => x.Y).ToString().PadLeft(consoleWidth / 3));
    }

И использование:

static void Main(string[] args) {
    var chartList = new List<Point> {
        new Point(50, 31), // x = 50, y = 31
        new Point(71, 87),
        new Point(71, 89),
        new Point(25, 66),
        new Point(94, 15),
        new Point(33, 94)
    };
    DrawChart(chartList);
    Console.ReadKey();
}

Результат:

Результат

Ответ 2

Реальная ошибка имеет очень длинную однострочную функцию. Это затрудняет чтение, и это создает проблемы даже для умного человека, подобного вам.

IsHit() - довольно сложная функция, поэтому разложите ее...

Вам нужно масштабировать координаты, чтобы они вписывались в окно консоли.

Я предположил, что координаты окна (1,1) - (consoleWidth, consoleHeight)

    private static bool IsConsoleHit(Dictionary<int, int> dict, 
                                     int consoleWidth, 
                                     int consoleHeight, 
                                     int hx, 
                                     int hy)
    {
        int minX = dict.Min(x => x.Key);
        int maxX = dict.Max(x => x.Key);
        int minY = dict.Min(x => x.Value);
        int maxY = dict.Max(x => x.Value);

        foreach (KeyValuePair<int, int> pair in dict)
        {
            // (x,y) starting at (0,0)
            int x = pair.Key   - minX;
            int y = pair.Value - minY;

            // convert to (0..1.0, 0..1.0)
            double dx = x / Math.Max(maxX - minX, 1.0);
            double dy = y / Math.Max(maxY - minY, 1.0);

            // Scale to (1,1) upto (consoleWidth, consoleHeight)
            int sx = (int)(dx * (consoleWidth  - 1)) + 1;
            int sy = (int)(dy * (consoleHeight - 1)) + 1;

            if (hx == sx && hy == sy)
            {
                return true;        // Its a hit
            }
        }

        return false;
    }

И затем сделать из него лямбда-функцию:

  Func<int, int, bool> IsHit = ((hx, hy) => IsConsoleHit(dict, consoleWidth, consoleHeight, hx, hy);

Итак, в будущем я бы никогда не использовал однострочные функции для вычисления вещей. Используйте обычный метод, предоставляя вам пространство для распространения и работы.

Ответ 3

По крайней мере, у меня нет ответа, который решает вашу проблему, но несколько советов, которые могут вызвать проблемы:

  • Ваша система координат ошибочна. Метод IsHit() получает для верхнего левого угла (где вы начинаете рисовать) координаты 0/0, но он должен быть 0/19 и для последнего элемента 77/19, но он должен быть 77/0.
  • Для расчета вы делите целые числа, что приводит к тому, что все дроби теряются, и ваш запрос всегда возвращает true.

Возможно, вам следует написать метод перевода, который будет переводиться с позиции курсора на значение x/y, а затем проверять на переведенную позицию вместо того, чтобы пытаться сделать оба за один шаг. Я думаю, используя этот подход, вы сможете решить проблему самостоятельно.