local super = require "View"

Graph = super:new()

Graph.horizontalOrientation = "horizontal"
Graph.verticalOrientation = "vertical"

local handles = {
    ButtonHandle:new{
        text = Hook:new(
            function(self)
                local title = self:getProperty('title')
                if not ispresent(title) and not self._editingTitle then
                    return 'Add Title'
                end
            end,
            function(self, text)
            end),
        location = Hook:new(
            function(self)
                local rect = self:rect()
                local contentRect = self:getContentRect()
                return contentRect:midx(), rect.top, 0.5, 1, 0, -3
            end,
            function(self, x, y, valign, halign, dx, dy)
            end),
        isVertical = Hook:new(false),
        inspector = Hook:new(
            function(self)
                return self:getTitleComponent()
            end,
            function(self, inspector)
            end),
    },
    ButtonHandle:new{
        text = Hook:new(
            function(self)
                local title = self:getHorizontalAxis():getProperty('title')
                if not ispresent(title) and not self._editingHorizontalAxisTitle then
                    return 'Add Label'
                end
            end,
            function(self, text)
            end),
        location = Hook:new(
            function(self)
                local rect = self:rect()
                local contentRect = self:getContentRect()
                return contentRect:midx(), rect.bottom, 0.5, 0, 0, 3
            end,
            function(self, x, y, valign, halign, dx, dy)
            end),
        isVertical = Hook:new(false),
        inspector = Hook:new(
            function(self)
                return self:getHorizontalAxisTitleComponent()
            end,
            function(self, inspector)
            end),
    },
    ButtonHandle:new{
        text = Hook:new(
            function(self)
                local title = self:getVerticalAxis():getProperty('title')
                if not ispresent(title) and not self._editingVerticalAxisTitle then
                    return 'Add Label'
                end
            end,
            function(self, text)
            end),
        location = Hook:new(
            function(self)
                local rect = self:rect()
                local contentRect = self:getContentRect()
                return rect.left, contentRect:midy(), 0, 0.5, 3, 0
            end,
            function(self, x, y, valign, halign, dx, dy)
            end),
        isVertical = Hook:new(true),
        inspector = Hook:new(
            function(self)
                return self:getVerticalAxisTitleComponent()
            end,
            function(self, inspector)
            end),
    },
}

local defaults = {
    labelDistance = 5,
    frame = false,
    horizontalGrid = false,
    verticalGrid = false,
    gridDots = false,
}

local nilDefaults = {
    'title',
    'horizontalAxis',
    'verticalAxis',
}

function Graph:new()
    self = super.new(self)
    
    for k, v in pairs(defaults) do
        self:addProperty(k, v)
    end
    for _, k in pairs(nilDefaults) do
        self:addProperty(k)
    end
    
    self._drawCountHook = PropertyHook:new(0)
        :setUndoable(false)
    
    self._editingTitle = false
    self._editingHorizontalAxisTitle = false
    self._editingVerticalAxisTitle = false
    
    self._layers = List:new()
        :setUndoable(true)
        :addObserver(self)
    
    self._addLayerObserver = function(item)
        item:setColorSchemeHook(self:getPropertyHook('colorScheme'))
        item:setTypographySchemeHook(self:getPropertyHook('typographyScheme'))
        item:setParent(self)
        item:addObserver(self)
    end
    self._layers:addEventObserver('add', self._addLayerObserver)
    
    self._contentRect = nil
    self._contentRectInvalidator = function()
        self._contentRect = nil
    end
    self:addObserver(self._contentRectInvalidator)
    
    self._overlays = {}
    
    return self
end

function Graph:unarchiveLayers(archived)
    self._layers:setUndoable(false)
    for index = 1, #archived do
        local layer = unarchive(archived[index])
        if Object.isa(layer, GraphLayer) then
            self._layers:add(layer)
        end
    end
    self._layers:setUndoable(true)
end

function Graph:unarchiveOverlays(archived)
    for index = 1, #archived do
        local overlay = unarchive(archived[index])
        if Object.isa(overlay, Overlay) then
            local overlayName = overlay:getName()
            if overlayName then
                self:setOverlay(overlayName, nil, nil, nil, overlay:getPosition())
            end
        end
    end
end

function Graph:unarchiveHorizontalAxis(archived)
    local axis = unarchive(archived)
    if Object.isa(axis, Axis) then
        self:setHorizontalAxis(axis)
    end
end

function Graph:unarchiveVerticalAxis(archived)
    local axis = unarchive(archived)
    if Object.isa(axis, Axis) then
        self:setVerticalAxis(axis)
    end
end

function Graph:unarchiveTitleFont(archived)
    -- NOTE: Version 1.4.2 and earlier stored graph title fonts without a typography scheme.
    self._legacyTitleFont = unarchive(archived)
end

function Graph:unarchiveAxisFont(archived)
    -- NOTE: Version 1.4.2 and earlier stored graph axis title fonts without a typography scheme.
    self._legacySubtitleFont = unarchive(archived)
end

function Graph:unarchiveLabelFont(archived)
    -- NOTE: Version 1.4.2 and earlier stored graph axis value fonts without a typography scheme.
    self._legacyQuantityFont = unarchive(archived)
    self._legacyCategoryFont = unarchive(archived)
end

function Graph:unarchiveLayerFont(archived)
    -- NOTE: Version 1.4.2 and earlier stored label layer fonts without a typography scheme.
    self._legacyLabelFont = unarchive(archived)
end

function Graph:unarchived()
    if self._legacyTitleFont then
        self:setFont(TypographyScheme.titleFont, self._legacyTitleFont)
    end
    if self._legacySubtitleFont then
        self:setFont(TypographyScheme.subtitleFont, self._legacySubtitleFont)
    end
    if self._legacyLabelFont then
        self:setFont(TypographyScheme.labelFont, self._legacyLabelFont)
    end
    if self._legacyCategoryFont then
        self:setFont(TypographyScheme.categoryFont, self._legacyCategoryFont)
    end
    if self._legacyQuantityFont then
        self:setFont(TypographyScheme.quantityFont, self._legacyQuantityFont)
    end
    super.unarchived(self)
end

function Graph:archive()
    local typeName, properties = super.archive(self)
    local layers = {}
    for layer in self._layers:iter() do
        layers[#layers + 1] = layer
    end
    properties.layers = layers
    local overlays = {}
    for _, overlay in pairs(self._overlays) do
        overlays[#overlays + 1] = overlay
    end
    properties.overlays = overlays
    return typeName, properties
end

function Graph:getHandles()
    local allHandles = appendtables({}, super.getHandles(self))
    for _, overlay in pairs(self._overlays) do
        local stamp = overlay:getStamp()
        local width, height = overlay:getSize()
        if stamp and width and height then
            allHandles[#allHandles + 1] = BoundedDraggingHandle:new{
                actionName = "Move Labels",
                bounds = Hook:new(
                    function(self)
                        local rect = self:getContentRect()
                        local padding = 4
                        return rect.left + padding, rect.bottom + padding, rect.right - padding, rect.top - padding
                    end,
                    function(self, left, bottom, right, top)
                    end),
                location = Hook:new(
                    function(self)
                        local contentRect = self:getContentRect()
                        local width, height = overlay:getSize()
                        local dx, dy = math.min(width, contentRect:width()) / 2, math.min(height, contentRect:height()) / 2
                        local availableRect = contentRect:insetXY(dx, dy)
                        local x, y = availableRect:mapPoint(overlay:getPosition())
                        local padding = 4
                        return x - dx + padding, y - dy + padding, x + dx - padding, y + dy - padding
                    end,
                    function(self, left, bottom, right, top)
                        local x, y = (left + right) / 2, (bottom + top) / 2
                        local contentRect = self:getContentRect()
                        local width, height = overlay:getSize()
                        local dx, dy = math.min(width, contentRect:width()) / 2, math.min(height, contentRect:height()) / 2
                        local availableRect = contentRect:insetXY(dx, dy)
                        if availableRect:width() == 0 then
                            x = 0.5
                        else
                            x = (x - availableRect:minx()) / availableRect:width()
                        end
                        if availableRect:height() == 0 then
                            y = 0.5
                        else
                            y = (y - availableRect:miny()) / availableRect:height()
                        end
                        overlay:setPosition(x, y)
                    end),
            }
        end
    end
    return appendtables(allHandles, handles)
end

function Graph:getInspectors()
    local list = super.getInspectors(self)

    local inspector = Inspector:new{
        title = 'Colors',
        type = 'Popup',
        icon = 'Colors',
        target = function()
            return self:getColorSchemeInspectors()
        end,
    }
    list:add(inspector)

    local inspector = Inspector:new{
        title = 'Fonts',
        type = 'Popup',
        icon = 'Fonts',
        target = function()
            return self:getTypographySchemeInspectors()
        end,
    }
    list:add(inspector)

    local inspector = Inspector:new{
        title = 'Axes',
        type = 'Popup',
        icon = 'Axes',
        target = function()
            return self:getAxisInspectors()
        end,
    }
    local xaxisHook = self:getPropertyHook('horizontalAxis')
    local yaxisHook = self:getPropertyHook('verticalAxis')
    -- TODO: keep these functions from being collected too soon, but do it less awkwardly.
    inspector.__x = function(sender)
        if sender == xaxisHook or sender == xaxisHook:getValue() then
            inspector:invalidate(inspector)
        end
    end
    inspector.__y = function(sender)
        if sender == yaxisHook or sender == yaxisHook:getValue() then
            inspector:invalidate(inspector)
        end
    end
    xaxisHook:addObserver(inspector.__x)
    yaxisHook:addObserver(inspector.__y)
    list:add(inspector)

    local inspector = Inspector:new{
        title = 'Series',
        target = self._layers,
        type = 'List.Sort',
        constraint = function()
            return GraphLayer:getSubclasses()
        end,
    }
    list:add(inspector)

    return list
end

function Graph:getColorInspectors()
    local list = super.getColorInspectors(self)
    list:add(self:createColorInspector(ColorScheme.titlePaint, 'Titles'))
    list:add(self:createColorInspector(ColorScheme.backgroundPaint, 'Background'))
    list:add(self:createColorInspector(ColorScheme.strokePaint, 'Axes'))
    list:add(self:createColorInspector(ColorScheme.gridPaint, 'Grid'))
    list:add(self:createColorInspector(ColorScheme.labelPaint, 'Labels'))
    return list
end

function Graph:getDataColorInspectors()
    local list = super.getDataColorInspectors(self)
    for index = 1, self:getColorScheme():getDataPaintCount() do
        list:add(self:createDataColorInspector(index, 'Series ' .. index))
    end
    return list
end

function Graph:getFontInspectors()
    local list = super.getFontInspectors(self)
    list:add(self:createFontInspector(TypographyScheme.titleFont, 'Title'))
    list:add(self:createFontInspector(TypographyScheme.subtitleFont, 'Axis Titles'))
    return list
end

function Graph:getAxisInspectors()
    local list = List:new()
    list:add(self:createInspector('Label', {}, 'Axis Titles'))
    list:join(self:getAxisTitleInspectors())
    list:add(self:createInspector('NumericAxis.labels', {}, 'Axis Values'))
    list:join(self:getAxisValueInspectors())
    list:add(self:createInspector('Label', {}, 'Axis Labels'))
    list:join(self:getAxisLabelInspectors())
    return list
end

function Graph:getAxisTitleInspectors()
    local list = List:new()
    list:join(self:getHorizontalAxis():getTitleInspectors(self, self:getHorizontalAxisDescription()))
    list:join(self:getVerticalAxis():getTitleInspectors(self, self:getVerticalAxisDescription()))
    return list
end

function Graph:getAxisValueInspectors()
    local list = List:new()
    list:join(self:getHorizontalAxis():getValueInspectors(self, self:getHorizontalAxisDescription()))
    list:join(self:getVerticalAxis():getValueInspectors(self, self:getVerticalAxisDescription()))
    return list
end

function Graph:getAxisLabelInspectors()
    local list = List:new()
    list:join(self:getHorizontalAxis():getLabelInspectors(self, self:getHorizontalAxisDescription()))
    list:join(self:getVerticalAxis():getLabelInspectors(self, self:getVerticalAxisDescription()))
    return list
end

function Graph:getHorizontalAxisDescription()
    return 'Horizontal'
end

function Graph:getVerticalAxisDescription()
    return 'Vertical'
end

function Graph:getLayerList()
    return self._layers
end

function Graph:getLayerPaint(layer)
    local count = 0
    local index = 0
    for listLayer in self._layers:iter() do
        if listLayer:usesLayerPaint() then
            count = count + 1
        end
        if listLayer == layer then
            index = count
        end
    end
    local colorScheme = self:getColorScheme()
    return (index > 0 and count > 0 and colorScheme:getDataSeriesPaint(index, count)) or Color.black
end

function Graph:getLayerLabelPaint()
    return self:getPaint(ColorScheme.labelPaint)
end

function Graph:getLayerLabelFont()
    return self:getFont(TypographyScheme.labelFont)
end

function Graph:getLayerHighlightPaint()
    return self:getPaint(ColorScheme.highlightPaint)
end

function Graph:isOrientable()
    return false
end

function Graph:setHorizontalAxis(axis)
    axis:setOrientation(Graph.horizontalOrientation)
    self:setProperty('horizontalAxis', axis)
end

function Graph:getHorizontalAxis()
    return self:getProperty('horizontalAxis')
end

function Graph:setVerticalAxis(axis)
    axis:setOrientation(Graph.verticalOrientation)
    self:setProperty('verticalAxis', axis)
end

function Graph:getVerticalAxis()
    return self:getProperty('verticalAxis')
end

function Graph:getBackgroundPaint()
    return self:getPaint(ColorScheme.backgroundPaint)
end

function Graph:getAxisPaint()
    return self:getPaint(ColorScheme.strokePaint)
end

function Graph:getAxisValueFont()
    return self:getFont(TypographyScheme.quantityFont)
end

function Graph:getAxisLabelPaint()
    return self:getPaint(ColorScheme.labelPaint)
end

function Graph:getAxisLabelDistance()
    return self:getProperty('labelDistance')
end

function Graph:getOverlay(overlayName)
    local overlay = self._overlays[overlayName]
    if overlay then
        return overlay:getStamp(), overlay:getSize()
    end
end

function Graph:setOverlay(overlayName, stamp, width, height, defaultX, defaultY)
    local overlay = self._overlays[overlayName]
    if not overlay then
        overlay = Overlay:new()
        overlay:setName(overlayName)
        overlay:addObserver(self)
    end
    overlay:setStamp(stamp)
    overlay:setSize(width, height)
    local x, y = overlay:getPosition()
    x, y = x or defaultX or 0.5, y or defaultY or 0.5
    overlay:setPosition(x, y)
    self._overlays[overlayName] = overlay
end

function Graph:resetOverlays()
    for _, overlay in pairs(self._overlays) do
        overlay:setStamp(nil)
        overlay:setSize(nil, nil)
    end
end

function Graph:padding()
    local baseSize = self:getBaseSize()
    return {
        left = 2 * baseSize,
        bottom = 2 * baseSize,
        right = 2 * baseSize,
        top = 2 * baseSize,
    }
end

local function makeTitleStyledString(title, font)
    if ispresent(title) then
        return StyledString.new(title, { font = font })
    end
end

function Graph:getContentRect()
    if self._contentRect then
        return self._contentRect
    end
    
    local contentRect = self:rect()
    local xaxis       = self:getHorizontalAxis()
    local yaxis       = self:getVerticalAxis()
    local titleFont   = self:getFont(TypographyScheme.titleFont)
    local axisFont    = self:getFont(TypographyScheme.subtitleFont)
    local title       = makeTitleStyledString(self:getProperty('title'), titleFont)
    local xaxisTitle  = makeTitleStyledString(xaxis:getProperty('title'), axisFont)
    local yaxisTitle  = makeTitleStyledString(yaxis:getProperty('title'), axisFont)

    -- TODO move this automatic
    xaxis:setLabelSize(xaxis:getLabelFont(self):height())
    yaxis:setLabelSize(yaxis:getLabelFont(self):height())

    Profiler.time(self:class() .. ':getContentRect set axes', function()
        self:updateValueRange(xaxis, Graph.horizontalOrientation)
        self:updateValueRange(yaxis, Graph.verticalOrientation)
    end)
    
    -- title positions
    local inset = Rect:new{left = 4, bottom = 4, right = 4, top = 4}
    if title or self._editingTitle then
        inset.top = titleFont:height() * 2
    end
    if xaxisTitle or self._editingHorizontalAxisTitle then
        inset.bottom = axisFont:height() * 2
    end
    if yaxisTitle or self._editingVerticalAxisTitle then
        inset.left = axisFont:height() * 2
    end
    contentRect = contentRect:inset(inset)
    
    -- axis positions
    local minAxisPadding = {left = 0, bottom = 0, right = 0, top = 0}
    local maxAxisPadding = {left = 0, bottom = 0, right = 0, top = 0}
    
    local xaxisPadding = xaxis:padding(self)
    minAxisPadding.left = xaxisPadding.left
    minAxisPadding.right = xaxisPadding.right
    maxAxisPadding.bottom = xaxisPadding.bottom
    maxAxisPadding.top = xaxisPadding.top
    
    local yaxisPadding = yaxis:padding(self)
    minAxisPadding.bottom = yaxisPadding.bottom
    minAxisPadding.top = yaxisPadding.top
    maxAxisPadding.left = yaxisPadding.left
    maxAxisPadding.right = yaxisPadding.right
    
    local minPaddingRect = contentRect:inset(minAxisPadding)
    local maxPaddingRect = contentRect:inset(maxAxisPadding)
    
    local xPosition = math._mid(minPaddingRect:miny(), yaxis:scale(minPaddingRect, yaxis:origin()), minPaddingRect:maxy())
    local xFraction = (xPosition - minPaddingRect:miny()) / minPaddingRect:height()
    if xPosition < maxPaddingRect.bottom then
        if xPosition < minPaddingRect.top then
            minPaddingRect.bottom = (maxPaddingRect.bottom - xFraction * minPaddingRect.top) / (1 - xFraction)
        end
    elseif xPosition > maxPaddingRect.top then
        if xPosition > minPaddingRect.bottom then
            minPaddingRect.top = (maxPaddingRect.top - (1 - xFraction) * minPaddingRect.bottom) / xFraction
        end
    end
    
    local yPosition = math._mid(minPaddingRect:minx(), xaxis:scale(minPaddingRect, xaxis:origin()), minPaddingRect:maxx())
    local yFraction = (yPosition - minPaddingRect:minx()) / minPaddingRect:width()
    if yPosition < maxPaddingRect.left then
        if yPosition < minPaddingRect.right then
            minPaddingRect.left = (maxPaddingRect.left - yFraction * minPaddingRect.right) / (1 - yFraction)
        end
    elseif yPosition > maxPaddingRect.right then
        if yPosition > minPaddingRect.left then
            minPaddingRect.right = (maxPaddingRect.right - (1 - yFraction) * minPaddingRect.left) / yFraction
        end
    end
    
    contentRect = contentRect:intersection(minPaddingRect)
    
    self._contentRect = contentRect
    
    return contentRect
end

function Graph:getTitleRect(contentRect)
    contentRect = contentRect or self:getContentRect()
    local rect = self:rect()
    local font = self:getFont(TypographyScheme.titleFont)
    local titleHeight = font:height()
    return Rect:new{
        left = contentRect.left,
        bottom = rect.top - 1.5 * titleHeight,
        right = contentRect.right,
        top = rect.top - 0.5 * titleHeight,
    }
end

function Graph:getHorizontalAxisTitleRect(contentRect)
    contentRect = contentRect or self:getContentRect()
    local rect = self:rect()
    local font = self:getFont(TypographyScheme.subtitleFont)
    local titleHeight = font:height()
    return Rect:new{
        left = contentRect.left,
        bottom = rect.bottom + 0.5 * titleHeight,
        right = contentRect.right,
        top = rect.bottom + 1.5 * titleHeight,
    }
end

function Graph:getVerticalAxisTitleRect(contentRect)
    contentRect = contentRect or self:getContentRect()
    local rect = self:rect()
    local font = self:getFont(TypographyScheme.subtitleFont)
    local titleHeight = font:height()
    return Rect:new{
        left = rect.left + 0.5 * titleHeight,
        bottom = contentRect.bottom,
        right = rect.left + 1.5 * titleHeight,
        top = contentRect.top,
    }
end

function Graph:updateValueRange(axis, orientation)
    if axis and axis.updateAutoRange then
        axis:updateAutoRange(function(valueRangeFunctionPairGetter)
            local intralayerStates = {}
            for layer in self._layers:iter() do
                local valuePutter, rangeGetter = valueRangeFunctionPairGetter()
                local min, max = layer:cachedValueRange(orientation)
                if min and max then
                    valuePutter(min)
                    valuePutter(max)
                else
                    local layerClass = layer:class()
                    intralayerStates[layerClass] = ezpcall(function()
                        return layer:iterateValues(orientation, valuePutter, intralayerStates[layerClass])
                    end)
                    min, max = rangeGetter()
                    layer:cacheValueRange(orientation, min, max)
                end
            end
        end)
    end
end

function Graph:drawLayers(canvas, contentRect, layerList)
    local xaxis, yaxis = self:getHorizontalAxis(), self:getVerticalAxis()
    local xScaler, yScaler = xaxis:getScaler(contentRect), yaxis:getScaler(contentRect)
    local intralayerStates = {}
    for layer in layerList:iter() do
        local layerClass = layer:class()
        Profiler.time(layerClass .. ":draw", function()
            intralayerStates[layerClass] = canvas:pcall(function()
                local propertySequence = layer:makePropertySequence()
                return layer:draw(canvas, contentRect:copy(), propertySequence, xScaler, yScaler, intralayerStates[layerClass] or {})
            end)
        end)
    end
end

function Graph:drawOverlays(canvas)
    local contentRect = self:getContentRect()
    for overlayName, overlay in pairs(self._overlays) do
        local stamp = overlay:getStamp()
        local width, height = overlay:getSize()
        if stamp and width and height then
            local dx, dy = math.min(width, contentRect:width()) / 2, math.min(height, contentRect:height()) / 2
            local availableRect = contentRect:insetXY(dx, dy)
            local x, y = availableRect:mapPoint(overlay:getPosition())
            local rect = Rect:new{
                left   = x - dx,
                bottom = y - dy,
                right  = x + dx,
                top    = y + dy,
            }
            canvas:pcall(function()
                stamp(canvas, rect)
            end)
        end
    end
end

math._mid = function(min, value, max)
    if min <= max then
        if value < min then
            return min
        elseif value > max then
            return max
        end
    end
    return value
end

function Graph:draw(canvas)
    -- drawn location
    local rect = self:rect()
    if canvas:isHitTest() then
        canvas:fill(Path.rect(rect))
        return
    end
    self:resetOverlays()
    local baseSize = self:getBaseSize()
    
    -- graph axes
    local xaxis = self:getHorizontalAxis()
    local yaxis = self:getVerticalAxis()
    if not xaxis or not yaxis then
        return super.draw(self, canvas)
    end
    
    -- colors
    local color = {
        fill  = self:getPaint(ColorScheme.backgroundPaint),
        grid  = self:getPaint(ColorScheme.gridPaint),
        title = self:getPaint(ColorScheme.titlePaint),
        axis  = self:getPaint(ColorScheme.strokePaint),
        label = self:getPaint(ColorScheme.labelPaint),
    }
    local titleFont  = self:getFont(TypographyScheme.titleFont)
    local axisFont   = self:getFont(TypographyScheme.subtitleFont)
    local title      = makeTitleStyledString(self:getProperty('title'), titleFont)
    local xaxisTitle = makeTitleStyledString(xaxis:getProperty('title'), axisFont)
    local yaxisTitle = makeTitleStyledString(yaxis:getProperty('title'), axisFont)
    local hasDots    = self:getProperty('gridDots')
    local hasHgrid   = yaxis:getProperty('grid')
    local hasVgrid   = xaxis:getProperty('grid')
    
    -- content location
    local contentRect = self:getContentRect()
    
    local xPosition = math._mid(contentRect:miny(), yaxis:scale(contentRect, yaxis:origin()), contentRect:maxy())
    local xFraction = (xPosition - contentRect:miny()) / contentRect:height()
    local yPosition = math._mid(contentRect:minx(), xaxis:scale(contentRect, xaxis:origin()), contentRect:maxx())
    local yFraction = (yPosition - contentRect:minx()) / contentRect:width()
    local xDrawState, yDrawState
    do
        local xRect = Rect:new{
            left = contentRect.left,
            bottom = xPosition,
            right = contentRect.right,
            top = xPosition,
        }
        local crossing
        if xFraction ~= 0 then
            if yFraction == 0 or yFraction == 1 then
                crossing = xaxis:scaled(contentRect, yPosition)
            else
                crossing = xaxis:origin()
            end
        end
        xDrawState = xaxis:prepareDraw(xRect, self, crossing)
    end
    do
        local yRect = Rect:new{
            left = yPosition,
            bottom = contentRect.bottom,
            right = yPosition,
            top = contentRect.top,
        }
        local crossing
        if yFraction ~= 0 then
            if xFraction == 0 or xFraction == 1 then
                crossing = yaxis:scaled(contentRect, xPosition)
            else
                crossing = yaxis:origin()
            end
        end
        yDrawState = yaxis:prepareDraw(yRect, self, crossing)
    end
    local xmajorPositions = xDrawState.tickPositions
    local ymajorPositions = yDrawState.tickPositions
    
    -- draw titles
    if title then
        local titleRect = self:getTitleRect(contentRect)
        TruncatedStyledStringStamp(canvas, titleRect, title, color.title, 0.5, 0.5)
    end
    if xaxisTitle then
        local titleRect = self:getHorizontalAxisTitleRect(contentRect)
        TruncatedStyledStringStamp(canvas, titleRect, xaxisTitle, color.title, 0.5, 0.5)
    end
    if yaxisTitle then
        local titleRect = self:getVerticalAxisTitleRect(contentRect)
        local transformation = Transformation:identity():rotate(math.pi / 2)
        StyledStringPointStamp(canvas, titleRect:midx(), titleRect:midy(), yaxisTitle:truncate(titleRect:height()), color.title, 0.5, 0.5, transformation)
    end
    
    -- fill frame
    local frame = Path.rect(contentRect)
    canvas:setPaint(color.fill):fill(frame)
    
    -- draw grid
    if hasHgrid then
        canvas:setPaint(color.grid):setThickness(1 * baseSize)
        for i = 1, #ymajorPositions do
            local y = ymajorPositions[i]
            local line = Path.line{x1 = contentRect.left, y1 = y, x2 = contentRect.right, y2 = y}
            canvas:stroke(line)
        end
    end
    if hasVgrid then
        canvas:setPaint(color.grid):setThickness(1 * baseSize)
        for i = 1, #xmajorPositions do
            local x = xmajorPositions[i]
            local line = Path.line{x1 = x, y1 = contentRect.bottom, x2 = x, y2 = contentRect.top}
            canvas:stroke(line)
        end
    end
    if hasDots and xmajorPositions and ymajorPositions then
        local dots = Path.point{x = 0, y = 0}
        local dotSize = 1.25 * baseSize
        for ix = 1, #xmajorPositions do
            for iy = 1, #ymajorPositions do
                local x = xmajorPositions[ix]
                local y = ymajorPositions[iy]
                dots:addOval{
                    left = x - dotSize,
                    bottom = y - dotSize,
                    right = x + dotSize,
                    top = y + dotSize,
                }
            end
        end
        canvas:setPaint(color.grid):fill(dots)
    end
    
    -- stroke frame
    if self:getProperty('frame') then
        canvas:setPaint(color.axis):setThickness(1 * baseSize):stroke(frame)
    end
    
    -- draw axes
    Profiler.time(self:class() .. ':draw axes', function()
        if xDrawState then
            xaxis:drawBackground(canvas, xDrawState)
        end
        if yDrawState then
            yaxis:drawBackground(canvas, yDrawState)
        end
    end)
    
    -- draw content layers
    Profiler.time(self:class() .. ':draw contents', function()
        self:drawLayers(canvas, contentRect, self._layers)
    end)
    
    -- draw axis labels
    if xDrawState then
        xaxis:drawForeground(canvas, xDrawState)
    end
    if yDrawState then
        yaxis:drawForeground(canvas, yDrawState)
    end
    
    -- draw overlays
    Profiler.time(self:class() .. ':draw overlays', function()
        self:drawOverlays(canvas)
    end)
end

function Graph:getTitleComponent()
    local inspector, hook
    inspector = self:createInspector('string', { text = 'title' }, 'Title')
    inspector:addHook(self:getPaintHook(ColorScheme.titlePaint), 'paint')
    inspector:addHook(self:getFontHook(TypographyScheme.titleFont), 'font')
    inspector:addHook(Hook:new(0.5), 'halign')
    inspector:addHook(Hook:new(0.5), 'valign')
    hook = Hook:new(
        function()
            return self:getTitleRect()
                :insetXY(0, -3)
        end,
        function(value) end)
    self:addDidDrawObserver(hook)
    inspector:addHook(hook, 'rect')
    inspector:addHook(Hook:new(false), 'multiline')
    hook = Hook:new(
        function() end,
        function(value)
            self._editingTitle = value
            self:invalidate(self)
        end)
    inspector:addHook(hook, 'editing')
    return inspector
end

function Graph:getHorizontalAxisTitleComponent()
    local axis = self:getHorizontalAxis()
    local inspector, hook
    inspector = self:createInspector('string', {}, 'Title')
    hook = Hook:new(
        function()
            return self:getHorizontalAxis():getProperty('title')
        end,
        function(value)
            self:getHorizontalAxis():setProperty('title', value)
        end)
    inspector:addHook(hook, 'text')
    inspector:addHook(self:getPaintHook(ColorScheme.titlePaint), 'paint')
    inspector:addHook(self:getFontHook(TypographyScheme.subtitleFont), 'font')
    inspector:addHook(Hook:new(0.5), 'halign')
    inspector:addHook(Hook:new(0.5), 'valign')
    hook = Hook:new(
        function()
            if self:getHorizontalAxis() == axis then
                return self:getHorizontalAxisTitleRect()
                    :insetXY(0, -3)
            end
        end,
        function(value) end)
    self:addDidDrawObserver(hook)
    inspector:addHook(hook, 'rect')
    inspector:addHook(Hook:new(false), 'multiline')
    hook = Hook:new(
        function() end,
        function(value)
            self._editingHorizontalAxisTitle = value
            self:invalidate(self)
        end)
    inspector:addHook(hook, 'editing')
    return inspector
end

function Graph:getVerticalAxisTitleComponent()
    local axis = self:getVerticalAxis()
    local inspector, hook
    inspector = self:createInspector('string', {}, 'Title')
    hook = Hook:new(
        function()
            return self:getVerticalAxis():getProperty('title')
        end,
        function(value)
            self:getVerticalAxis():setProperty('title', value)
        end)
    inspector:addHook(hook, 'text')
    inspector:addHook(self:getPaintHook(ColorScheme.titlePaint), 'paint')
    inspector:addHook(self:getFontHook(TypographyScheme.subtitleFont), 'font')
    inspector:addHook(Hook:new(0.5), 'halign')
    inspector:addHook(Hook:new(0.5), 'valign')
    hook = Hook:new(
        function()
            if self:getVerticalAxis() == axis then
                return self:getVerticalAxisTitleRect()
                    :insetXY(-3, 0)
            end
        end,
        function(value) end)
    self:addDidDrawObserver(hook)
    inspector:addHook(hook, 'rect')
    inspector:addHook(Hook:new(false), 'multiline')
    inspector:addHook(Hook:new(true), 'isVertical')
    hook = Hook:new(
        function() end,
        function(value)
            self._editingVerticalAxisTitle = value
            self:invalidate(self)
        end)
    inspector:addHook(hook, 'editing')
    return inspector
end

function Graph:getEditableComponent(x, y)
    if not (x and y) then
        return
    end
    local contentRect = self:getContentRect()
    if ispresent(self:getProperty('title')) and self:getTitleRect(contentRect):contains(x, y) then
        return self:getTitleComponent()
    elseif ispresent(self:getHorizontalAxis():getProperty('title')) and self:getHorizontalAxisTitleRect(contentRect):contains(x, y) then
        return self:getHorizontalAxisTitleComponent()
    elseif ispresent(self:getVerticalAxis():getProperty('title')) and self:getVerticalAxisTitleRect(contentRect):contains(x, y) then
        return self:getVerticalAxisTitleComponent()
    end
end

return Graph
