module fortplot_markers !! Shared marker utilities following DRY principles !! Eliminates code duplication between PNG and PDF backends use, intrinsic :: iso_fortran_env, only: wp => real64 implicit none private public :: get_marker_size, validate_marker_style, get_default_marker public :: marker_size_scale, DEFAULT_SCATTER_AREA public :: MARKER_CIRCLE, MARKER_SQUARE, MARKER_DIAMOND, MARKER_CROSS, MARKER_PLUS, MARKER_STAR public :: MARKER_TRIANGLE_UP, MARKER_TRIANGLE_DOWN, MARKER_PENTAGON, MARKER_HEXAGON public :: MARKER_DIAMOND_SMALL ! Marker style constants - pyplot compatible character(len=*), parameter :: MARKER_CIRCLE = 'o' character(len=*), parameter :: MARKER_SQUARE = 's' character(len=*), parameter :: MARKER_DIAMOND = 'D' character(len=*), parameter :: MARKER_DIAMOND_SMALL = 'd' character(len=*), parameter :: MARKER_CROSS = 'x' character(len=*), parameter :: MARKER_PLUS = '+' character(len=*), parameter :: MARKER_STAR = '*' character(len=*), parameter :: MARKER_TRIANGLE_UP = '^' character(len=*), parameter :: MARKER_TRIANGLE_DOWN = 'v' character(len=*), parameter :: MARKER_PENTAGON = 'p' character(len=*), parameter :: MARKER_HEXAGON = 'h' ! Marker size constants - centralized for consistency. The circle value is ! the fill radius; the other shapes are sized so the RENDERED filled-pixel ! areas match matplotlib's default scatter, where a single size s renders the ! square and diamond slightly larger than the circle and roughly equal to one ! another (square ~1.17x circle area, diamond ~1.23x circle area). The factors ! below are calibrated against measured rasterized areas (see ! test/marker/test_marker_equal_area.f90), not ideal geometry: the circle ! antialiasing window and the supersampled quad fill use the same tight edge ! convention, so equal-geometry constants would render unequal areas. For the ! raster backend get_marker_size is interpreted as: circle -> radius, square ! -> full side, diamond -> full diagonal, cross/plus -> full extent. real(wp), parameter :: SIZE_CIRCLE = 3.7_wp real(wp), parameter :: SIZE_SQUARE = 1.911_wp*SIZE_CIRCLE ! calibrated side real(wp), parameter :: SIZE_DIAMOND = 2.744_wp*SIZE_CIRCLE ! calibrated diagonal real(wp), parameter :: SIZE_CROSS = 2.0_wp*SIZE_CIRCLE ! extent = diameter real(wp), parameter :: SIZE_PLUS = 2.0_wp*SIZE_CIRCLE ! extent = diameter real(wp), parameter :: SIZE_STAR = 2.0_wp*SIZE_CIRCLE real(wp), parameter :: SIZE_TRIANGLE = 2.0_wp*SIZE_CIRCLE real(wp), parameter :: SIZE_PENTAGON = 2.0_wp*SIZE_CIRCLE real(wp), parameter :: SIZE_HEXAGON = 2.0_wp*SIZE_CIRCLE ! matplotlib's default scatter area s (points^2). The fixed marker sizes ! above reproduce this area, so an explicit s == DEFAULT_SCATTER_AREA leaves ! the rendered size unchanged. Marker radius scales with sqrt(s). real(wp), parameter :: DEFAULT_SCATTER_AREA = 20.0_wp contains pure function marker_size_scale(area) result(scale) !! Linear radius scale factor for a matplotlib scatter area `s`. !! s is an area (points^2); radius scales with sqrt(s). Normalized so !! that area == DEFAULT_SCATTER_AREA returns 1 (today's default size). real(wp), intent(in) :: area real(wp) :: scale if (area <= 0.0_wp) then scale = 1.0_wp else scale = sqrt(area/DEFAULT_SCATTER_AREA) end if end function marker_size_scale pure function get_marker_size(style) result(size) !! Get standardized marker size for given style !! Eliminates magic number duplication across backends character(len=*), intent(in) :: style real(wp) :: size select case (trim(style)) case (MARKER_CIRCLE) size = SIZE_CIRCLE case (MARKER_SQUARE) size = SIZE_SQUARE case (MARKER_DIAMOND, MARKER_DIAMOND_SMALL) size = SIZE_DIAMOND case (MARKER_CROSS) size = SIZE_CROSS case (MARKER_PLUS) size = SIZE_PLUS case (MARKER_STAR) size = SIZE_STAR case (MARKER_TRIANGLE_UP, MARKER_TRIANGLE_DOWN) size = SIZE_TRIANGLE case (MARKER_PENTAGON) size = SIZE_PENTAGON case (MARKER_HEXAGON) size = SIZE_HEXAGON case default size = SIZE_CIRCLE ! Default fallback end select end function get_marker_size pure function validate_marker_style(style) result(is_valid) !! Validate if marker style is supported character(len=*), intent(in) :: style logical :: is_valid select case (trim(style)) case (MARKER_CIRCLE, MARKER_SQUARE, MARKER_DIAMOND, MARKER_DIAMOND_SMALL, MARKER_CROSS, & MARKER_PLUS, MARKER_STAR, MARKER_TRIANGLE_UP, MARKER_TRIANGLE_DOWN, & MARKER_PENTAGON, MARKER_HEXAGON) is_valid = .true. case default is_valid = .false. end select end function validate_marker_style pure function get_default_marker() result(marker) !! Get default marker style character(len=1) :: marker marker = MARKER_CIRCLE end function get_default_marker end module fortplot_markers