diff --git a/Manifest.toml b/Manifest.toml index ae3e78a..7b38006 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -2,7 +2,7 @@ julia_version = "1.11.4" manifest_format = "2.0" -project_hash = "0a18a0873213e0e8660a5eb202a1fa9f1621a652" +project_hash = "ec502e8d73a739a6239b751344af945fa59e7806" [[deps.AbstractFFTs]] deps = ["LinearAlgebra"] diff --git a/Project.toml b/Project.toml index 9661941..a619579 100644 --- a/Project.toml +++ b/Project.toml @@ -7,10 +7,12 @@ version = "0.1.0" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" Luxor = "ae8d54c2-7ccd-5906-9d76-62fc9837b5bc" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" VideoIO = "d6d074c3-1acf-5d4c-9a43-ef38773959a2" [compat] FileIO = "1.17.0" Images = "0.26.2" Luxor = "4.2.0" +Statistics = "1.11.1" VideoIO = "1.1.1" diff --git a/src/image.jl b/src/image.jl new file mode 100644 index 0000000..3eaf2f9 --- /dev/null +++ b/src/image.jl @@ -0,0 +1,8 @@ +using Images + +""" +Converts image to grayscale +""" +function greyscale(c::Images.RGB)::Real + return Real(c.r) +end diff --git a/src/stock_index_composition.jl b/src/stock_index_composition.jl new file mode 100644 index 0000000..f27ce2a --- /dev/null +++ b/src/stock_index_composition.jl @@ -0,0 +1,103 @@ +using Base +import Luxor as L + +""" +Colour of a component given its fluctuations +""" +function colour_of_diff(diff::Real)::Tuple{Real,Real,Real} + if diff > 0 + return (0.5,1,0.5) + else + return (1,0.5,0.5) + end +end + +component_text_size = 20 + +@kwdef struct Component + pos::Tuple{UInt, UInt} + size::Tuple{UInt, UInt} + name::String + diff::Real +end + +sector_title_font_size = 16 +sector_title_height = 20 + +@kwdef struct Sector + name::String + pos::Tuple{UInt, UInt} + size::Tuple{UInt, UInt} + components::Array{Component} +end + +@kwdef struct CompositionDiagram + sectors::Array{Sector} +end + +""" +Draws stock index composition chart +""" +function draw(diagram::CompositionDiagram, size=(800, 600)) + L.Drawing(size[1], size[2], :png) + L.background("black") + for sector in diagram.sectors + L.sethue("gray") + L.rect(L.Point(sector.pos), sector.size[1], sector.size[2], action = :fillpreserve) + L.setline(2) + L.strokepath() + L.sethue("white") + L.setfont("Helvetica", sector_title_font_size) + L.settext( + sector.name, + L.Point(sector.pos), + halign="left", + valign="top", + ) + L.setfont("Helvetica", component_text_size) + for component in sector.components + pos = sector.pos .+ component.pos + L.setcolor(colour_of_diff(component.diff)) + L.rect(L.Point(pos), component.size[1], component.size[2], action = :fillpreserve) + L.sethue("white") + L.setline(5) + L.strokepath() + L.settext( + component.name, + L.Point(pos .+ component.size ./ 2), + halign="center", + valign="bottom", + ) + L.settext( + "$(component.diff)%", + L.Point(pos .+ component.size ./ 2), + halign="center", + valign="top", + ) + end + end + L.finish() +end + +diagram = CompositionDiagram( + sectors=[ + Sector( + name="technology", + pos=(0,0), + size=(200,300), + components=[ + Component(pos=(0,20), size=(200,200),name="AAPL", diff=0.5), + Component(pos=(0,200), size=(200,100),name="GOOG", diff=-0.3), + ], + ), + Sector( + name="resources", + pos=(200,0), + size=(200,200), + components=[ + Component(pos=(0,20), size=(200,150),name="OIL", diff=0.1), + ], + ), + ], +) +draw(diagram) diff --git a/src/tdpacking.jl b/src/tdpacking.jl new file mode 100644 index 0000000..bb5959a --- /dev/null +++ b/src/tdpacking.jl @@ -0,0 +1,132 @@ +using Base +using Statistics, Images +import Luxor as L + +@kwdef struct Region + pos::Tuple{Int,Int} + size::Tuple{Int,Int} + # If true, this region increases from x to y. This direction variable steers + # the direction of cut. + dir::Bool +end + +# Time-dependent packing +@kwdef struct Rect + pos::Tuple{Int,Int} + size::Tuple{Int,Int} + color::Real +end + +function area(r::Rect)::Int + return r.size[1] * r.size[2] +end +function aspect_ratio(r::Rect)::Real + return r.size[1] / r.size[2] +end + +""" + divide_image(img::Matrix{}, size_min::Int=16, size_max::Int=256) + +Divides an image into rectangles of the same colour. The rectangles are not +allowed to have extreme aspect ratios. There is a minimal size of each retangle. +""" +function divide_image(img::Matrix{}; size_min::Int=16, size_max::Int=128, threshold_aspect::Real=3.0, threshold_std::Real=0.4)::Array{Rect} + remainder = [Region(pos=(1,1), size=size(img), dir=false)] + result = Rect[] + + # Loop until the entire field is filled + while length(remainder) > 0 + section = pop!(remainder) + + (px, py) = section.pos + (sx, sy) = section.size + + @assert px + sx - 1 <= size(img)[1] + @assert py + sy - 1 <= size(img)[2] + + width = 1 + height = 1 + + width_max = min(sx, size_max) + height_max = min(sy, size_max) + + # Try to expand this rectangle to the maximum size + + while width < width_max || height < height_max + subimg = img[px:px+width-1, py:py+height-1] + if std(subimg) >= threshold_std + break + end + colour = mean(subimg) + ar = width / height + + if width < width_max && ar < threshold_aspect + width += 1 + elseif height < height_max && ar > 1/threshold_aspect + height += 1 + else + break + end + end + @assert width <= sx + @assert height <= sy + + subimg = img[px:px+width-1, py:py+height-1] + rect = Rect( + pos=section.pos, + size=(width, height), + color=mean(subimg), + ) + push!(result, rect) + + if section.dir + if width < sx + push!(remainder, Region( + pos=(px + width, py), + size=(sx - width, height), + dir=false, + )) + end + if height < sy + push!(remainder, Region( + pos=(px, py + height), + size=(sx, sy - height), + dir=true, + )) + end + else + if height < sy + push!(remainder, Region( + pos=(px, py + height), + size=(width, sy - height), + dir=true, + )) + end + if width < sx + push!(remainder, Region( + pos=(px + width, py), + size=(sx - width, sy), + dir=false, + )) + end + end + end + + return result +end + +""" + draw_rect(size::Tuple{Int, Int}, rects::Array{Rect}) + +Displays the divided image in greyscale. +""" +function draw_rect(size::Tuple{Int, Int}, rects::Array{Rect}) + L.Drawing(size[1], size[2], :png) + L.background("black") + for rect in rects + c = N0f8(rect.color) + setcolor(c, c, c) + L.rect(L.Point(rect.pos), rect.size[1], rect.size[2], action = :fill) + end + L.finish() +end