fortplot_spec_config_apply.f90 Source File


Source Code

module fortplot_spec_config_apply
    !! Applies config_t values to figure_state_t for rendering.

    use, intrinsic :: iso_fortran_env, only: wp => real64
    use fortplot_spec_config_types, only: config_t, padding_t
    use fortplot_spec_config_defaults, only: mpl_default_config, &
        vegalite_default_config
    use fortplot_spec_types, only: spec_t
    use fortplot_figure_initialization, only: figure_state_t
    use fortplot_colors, only: parse_color
    use fortplot_text_fonts, only: set_preferred_font
    implicit none

    private
    public :: apply_config_to_state, apply_padding_to_margins
    public :: apply_style_defaults
    public :: set_legend_position_from_orient

contains

    subroutine apply_style_defaults(style_name, spec, dpi, force)
        !! Fill spec%config from built-in style.
        !! When force=.true., overrides any JSON-provided config.
        !! When force=.false. (default), only fills when absent.
        character(len=*), intent(in) :: style_name
        type(spec_t), intent(inout) :: spec
        real(wp), intent(in) :: dpi
        logical, intent(in), optional :: force

        logical :: do_force

        do_force = .false.
        if (present(force)) do_force = force

        if (spec%config%defined .and. .not. do_force) return

        select case (trim(style_name))
        case ('mpl', 'matplotlib')
            spec%config = mpl_default_config(dpi)
        case ('vegalite', 'vega-lite', 'vega')
            spec%config = vegalite_default_config(dpi)
        end select
    end subroutine apply_style_defaults

    subroutine apply_config_to_state(cfg, state)
        !! Translate config_t into figure_state_t fields.
        type(config_t), intent(in) :: cfg
        type(figure_state_t), intent(inout) :: state

        if (.not. cfg%defined) return

        call apply_color_palette(cfg, state)
        call apply_mark_defaults(cfg, state)
        call apply_axis_config(cfg, state)
        call apply_title_config(cfg, state)
        call apply_legend_config(cfg, state)
        call apply_font_preference(cfg)
    end subroutine apply_config_to_state

    subroutine apply_color_palette(cfg, state)
        type(config_t), intent(in) :: cfg
        type(figure_state_t), intent(inout) :: state

        real(wp) :: rgb(3)
        logical :: ok
        integer :: i, n

        n = cfg%category_color_count
        if (n <= 0) return

        do i = 1, min(n, size(state%colors, 2))
            call parse_color(trim(cfg%category_colors(i)), rgb, ok)
            if (ok) state%colors(:, i) = rgb
        end do
    end subroutine apply_color_palette

    subroutine apply_mark_defaults(cfg, state)
        type(config_t), intent(in) :: cfg
        type(figure_state_t), intent(inout) :: state

        if (.not. cfg%mark%defined) return

        if (cfg%mark%line_stroke_width >= 0.0_wp) then
            state%current_line_width = &
                cfg%mark%line_stroke_width
        end if
    end subroutine apply_mark_defaults

    subroutine apply_axis_config(cfg, state)
        type(config_t), intent(in) :: cfg
        type(figure_state_t), intent(inout) :: state

        if (.not. cfg%axis%defined) return

        if (cfg%axis%grid_set) then
            state%grid_enabled = cfg%axis%grid
        end if
        if (cfg%axis%grid_opacity >= 0.0_wp) then
            state%grid_alpha = cfg%axis%grid_opacity
        end if
        if (cfg%axis%grid_color_set) then
            state%grid_color = cfg%axis%grid_color
        end if
        if (cfg%axis%label_font_size >= 0.0_wp) then
            state%label_font_size = cfg%axis%label_font_size
            state%tick_font_size = cfg%axis%label_font_size
        end if
    end subroutine apply_axis_config

    subroutine apply_title_config(cfg, state)
        type(config_t), intent(in) :: cfg
        type(figure_state_t), intent(inout) :: state

        if (.not. cfg%title_config%defined) return

        if (cfg%title_config%font_size >= 0.0_wp) then
            state%title_font_size = cfg%title_config%font_size
        end if
    end subroutine apply_title_config

    subroutine apply_legend_config(cfg, state)
        !! Map config legend orient string to legend position integer.
        type(config_t), intent(in) :: cfg
        type(figure_state_t), intent(inout) :: state

        if (.not. cfg%legend%defined) return
        if (.not. cfg%legend%orient_set) return

        call set_legend_position_from_orient( &
            cfg%legend%orient, state%legend_data%position)
    end subroutine apply_legend_config

    subroutine set_legend_position_from_orient(orient, position)
        !! Single source of truth for orient string -> position int.
        character(len=*), intent(in) :: orient
        integer, intent(inout) :: position

        select case (trim(orient))
        case ('top-left')
            position = 1
        case ('top-right')
            position = 2
        case ('bottom-left')
            position = 3
        case ('bottom-right')
            position = 4
        end select
    end subroutine set_legend_position_from_orient

    subroutine apply_font_preference(cfg)
        !! Set the preferred font based on config axis label font.
        !! Maps common CSS font families to system font names.
        type(config_t), intent(in) :: cfg

        character(len=40) :: font_name

        if (.not. cfg%axis%defined) return
        if (.not. cfg%axis%label_font_set) return

        font_name = cfg%axis%label_font
        ! Map CSS font family strings to font discovery names
        if (index(font_name, 'DejaVu') > 0) then
            call set_preferred_font('DejaVu Sans')
        else if (index(font_name, 'Arial') > 0) then
            call set_preferred_font('Arial')
        else if (index(font_name, 'Helvetica') > 0) then
            call set_preferred_font('Helvetica')
        else if (index(font_name, 'Liberation') > 0) then
            call set_preferred_font('Liberation Sans')
        end if
    end subroutine apply_font_preference

    subroutine apply_padding_to_margins(pad, state, autosize_type)
        !! Convert Vega-Lite padding to figure_state_t margins.
        !!
        !! When autosize is "none" with "contains": "padding", the spec
        !! width/height is the data rectangle and padding is added around
        !! it. We expand the canvas to total = data + padding and compute
        !! margins as fractions of that total.
        type(padding_t), intent(in) :: pad
        type(figure_state_t), intent(inout) :: state
        character(len=*), intent(in), optional :: autosize_type

        integer :: pl, pr, pt, pb
        real(wp) :: tw, th

        if (.not. pad%defined) return

        pl = max(pad%left, 0)
        pr = max(pad%right, 0)
        pt = max(pad%top, 0)
        pb = max(pad%bottom, 0)

        ! Expand canvas: total = data_rect + padding
        tw = real(state%width + pl + pr, wp)
        th = real(state%height + pt + pb, wp)

        if (tw <= 0.0_wp .or. th <= 0.0_wp) return

        state%width = nint(tw)
        state%height = nint(th)
        state%margin_left = real(pl, wp) / tw
        state%margin_right = real(pr, wp) / tw
        state%margin_top = real(pt, wp) / th
        state%margin_bottom = real(pb, wp) / th

        ! Reinitialize backend with new dimensions
        call reinit_backend_dimensions(state)
    end subroutine apply_padding_to_margins

    subroutine reinit_backend_dimensions(state)
        use fortplot_utils, only: initialize_backend
        type(figure_state_t), intent(inout) :: state

        call initialize_backend(state%backend, &
            trim(state%backend_name), &
            state%width, state%height, state%dpi)
    end subroutine reinit_backend_dimensions

end module fortplot_spec_config_apply