WITH params AS (
    -- Parameters for down stream queries
    SELECT
       15 AS bucket_count,
       80 AS max_bars
),
numbers AS (
    -- Change this query to select real data.
    -- For now we make random set of numbers.
    SELECT
        floor(random() * 100)::int AS num    
    FROM params,
         generate_series(1, 10000)
),
overall AS (
    SELECT
        MAX(num) max_num
    FROM numbers
),
buckets AS (
    -- Build list of bucket ranges
    SELECT
        bucket,
        floor((max_num::numeric / bucket_count) * bucket)::int AS min_range,
        floor((max_num::numeric / bucket_count) * (bucket + 1) - 1)::int AS max_range
    FROM params,
         overall,
         generate_series(0, bucket_count - 1) AS t(bucket)
),
counts AS (
    -- Join numbers with buckets and count up how many fall between the ranges
    SELECT
        bucket,
        min_range,
        max_range,
        COUNT(num) AS count_num
    FROM numbers
    JOIN buckets ON numbers.num BETWEEN min_range AND max_range
    GROUP BY bucket, min_range, max_range
    ORDER BY bucket
),
count_ranges AS (
    -- Figure out the min/max counts for each range.
    -- This is use to normalize the width of the graph.
    SELECT
        MIN(count_num) min_count_num,
        MAX(count_num) max_count_num,
        SUM(count_num) sum_count_num
    FROM counts
),
percentages AS (
    -- Calculate how close count_num is to the max count for the entire graph.
    SELECT
        counts.*,
        count_num::numeric / max_count_num AS bar_pct
    FROM params, counts, count_ranges
),
graph AS (
    -- Render the final chart
    SELECT
        bucket,
        min_range,
        max_range,
        count_num,
        repeat('0', (bar_pct * max_bars)::int) AS chart
    FROM params,
         percentages
)
-- Select which part of the query to display by changing the `FROM` target
SELECT * FROM graph
;