Software Programming

Kunuk Nykjaer

Percent distribution of numbers with C#

leave a comment »


You have a list of n integers and you want to display the percentage distribution of the values.
The requirement:
You must round the numbers such the percent sum is 100 and the percent values must be integer values.
The percent values must be fair distributed according to the values.

How would you do it?
If you calculate the distribution and round the numbers the sum might not be equal to 100.

Here is one way to do it.

program.cs (algorithm)

 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

class Program
{
    const string DrawFilePath = @"C:\temp\";

    static void Main(string[] args)
    {
        // Insert test data here
        var list = new List<int> { 30, 70, 10, 90 };

        var result = GetValuesMapped(list);
        GenerateJavascriptDrawFile(result);

        Console.WriteLine("Open canvas.html to watch the result\npress a key to exit ..");
        Console.ReadKey();
    }

    static List<Data> GetValuesMapped(List<int> list)
    {
        const int MAPTO = 100; // 100 %
        const int min = 0;
        const int max = 10000;

        var mapped = new List<MapData>();
        var noresult = new Data[list.Count].ToList();
        double sumList = list.Sum();

        // No value
        if (sumList <= 0)
        {
            return noresult;
        }

        // Sanitize data
        for (int i = 0; i < list.Count; i++)
        {
            if (list[i] < min) { list[i] = min; }
            if (list[i] > max) { list[i] = max; }
        }

        // Map data
        int id = 0;
        foreach (var i in list)
        {
            var percent = Map(i, 0, sumList, 0, MAPTO);
            mapped.Add(new MapData
            {
                Id = id++,
                Modulus = percent - Math.Truncate(percent),
                Divisor = (int)percent
            });
        }

        // Adjustment is needed
        // Iterate increment by 1 until sum is correct

        // sort by decimal values
        var sorted = mapped.OrderByDescending(i => i.Modulus).ToList(); 
        int sumFloor = mapped.Sum(i => i.Divisor);

        int addsNeeded = MAPTO - sumFloor;

        // Add need values until
        for (int i = 0; i < addsNeeded; i++)
        {
            sorted[i % sorted.Count].Divisor++;
        }

        // Sort back by id
        var ordered = sorted.OrderBy(i => i.Id).ToList();

        // Populate data
        var result = list.Select((t, i) => 
            new Data { Value = t, Percent = ordered[i].Divisor }).ToList();

        return result;
    }

    internal class MapData
    {
        public int Id { get; set; }
        public double Modulus { get; set; }
        public int Divisor { get; set; }
    }

    internal class Data
    {
        public int Value { get; set; }
        public int Percent { get; set; }
    }

    /// <summary>        
    /// Value x in range [a;b] is mapped to a new value in range [c;d]
    /// </summary>                
    static double Map(double x, double a, double b, double c, double d)
    {
        var r = (x - a) / (b - a) * (d - c) + c;
        return r;
    }

    internal static class FileUtil
    {
        public static void WriteFile(string data, FileInfo fileInfo)
        {
            try
            {
                using (StreamWriter streamWriter = File.CreateText(fileInfo.FullName))
                {
                    streamWriter.Write(data);
                }
            }
            catch
            {
                throw;
            }
        }
    }

    // Create canvas data in draw.js file
    static void GenerateJavascriptDrawFile(List<Data> list)
    {
        var sb = new StringBuilder("function drawData(ctx) {\n");

        const int xbeg = 30; // start x coord
        const int ybeg = 100;
        const int lenMultiply = 5; // percentage length display
        const int yline = 40; // next line offset
        const int txtX = 50; // text display offset
        const int txtY = 4;
        const int colorMin = 20; // min color range
        const int colorMax = 200;
        const int topY = -70;
        const string textCount = "count";
        const string textPercent = "%";

        if (list != null && list.Count > 0)
        {
            // draw top
            sb.Append("\t// top\n");
            sb.AppendFormat("\tctx.fillText('{0}{1}', {2}, {3});\n", 
                100, textPercent, xbeg, topY + ybeg - txtY);
            sb.AppendFormat("\tdrawLine({0}, {1}, 'rgb(0,0,0)', {2}, ctx);\n", 
                xbeg, ybeg + topY, (100 * lenMultiply) + 1);

            // draw lines
            var rand = new Random();
            sb.Append("\n\t// lines\n");
            for (int i = 0; i < list.Count(); i++)
            {
                var color = string.Format("'rgb({0},{1},{2})'", 
                    rand.Next(colorMin, colorMax), rand.Next(colorMin, colorMax), rand.Next(colorMin, colorMax));
                sb.AppendFormat("\tdrawLine({0}, {1}, {2}, {3}, ctx);\n", 
                    xbeg, (ybeg + i * yline), color, (list[i].Percent * lenMultiply) + 1);
            }

            // draw text
            sb.Append("\n\t// text\n");
            sb.Append("\tctx.fillStyle = 'rgb(0,0,0)';\n");
            for (int i = 0; i < list.Count(); i++)
            {
                sb.AppendFormat("\tctx.fillText('{0}{1}', {2}, {3});\n", 
                    list[i].Percent, textPercent, xbeg, i * yline + ybeg - txtY);
                sb.AppendFormat("\tctx.fillText('{0}: {1}', {2}, {3});\n", 
                    textCount, list[i].Value, xbeg + txtX, i * yline + ybeg - txtY);
            }
        }

        sb.Append("}");
        var path = new FileInfo(string.Concat(DrawFilePath, "draw.js"));
        FileUtil.WriteFile(sb.ToString(), path);
    }
}

Put both the html and javascript file in the c:\temp folder.
The C# program creates a new draw.js file in the c:\temp folder.
Open the html file with a modern browser that support canvas, like Firefox, Chrome or Internet Explorer 10+.

canvas.html (visualization)

 
<html>
<head>
    <title>Canvas</title>    
    <script type="text/javascript" src="draw.js?v=1"></script>    
    <script type="text/javascript">                        
        function drawLine(x, y, color, len, ctx) {
            ctx.lineWidth = 2;
            ctx.strokeStyle = color;
            ctx.strokeRect(x, y, len, 2);
        }

        window.onload = function draw() {
            var canvas = document.getElementById('canvas');
            if (canvas != null && canvas != undefined && canvas.getContext) {
                var ctx = canvas.getContext('2d');
                ctx.strokeStyle = "black";
                ctx.font = "10pt Arial";
                drawData(ctx);
            }
        }
    </script>
    <style type="text/css">
        body
        {
            margin-left: 10px;
            margin-top: 10px;
        }
        canvas
        {
            border: 1px solid red;
        }
    </style>
</head>
<body>    
    <canvas id="canvas" width="600" height="400">
        <p>
Your browser doesn't support canvas. 
Try Firefox, Chrome, Internet Explorer 10+ or an another modern browser.
       </p>
    </canvas>
</body>
</html>

The javascript file will be overwritten when you run the C# code.
draw.js (data)

 
function drawData(ctx) {
	// top
	ctx.fillText('100%', 30, 26);
	drawLine(30, 30, 'rgb(0,0,0)', 501, ctx);

	// lines
	drawLine(30, 100, 'rgb(101,78,132)', 76, ctx);
	drawLine(30, 140, 'rgb(32,101,168)', 176, ctx);
	drawLine(30, 180, 'rgb(139,151,71)', 26, ctx);
	drawLine(30, 220, 'rgb(163,73,143)', 226, ctx);

	// text
	ctx.fillStyle = 'rgb(0,0,0)';
	ctx.fillText('15%', 30, 96);
	ctx.fillText('count: 30', 80, 96);
	ctx.fillText('35%', 30, 136);
	ctx.fillText('count: 70', 80, 136);
	ctx.fillText('5%', 30, 176);
	ctx.fillText('count: 10', 80, 176);
	ctx.fillText('45%', 30, 216);
	ctx.fillText('count: 90', 80, 216);
}

For the list = 30, 70, 10, 90
gives this result:

Advertisements

Written by kunuk Nykjaer

December 19, 2012 at 9:04 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: