#-----------------------------------------------------------------------------# Copyright (c) 2012 - 2023, Anaconda, Inc., and Bokeh Contributors.# All rights reserved.## The full license is in the file LICENSE.txt, distributed with this software.#-----------------------------------------------------------------------------#-----------------------------------------------------------------------------# Boilerplate#-----------------------------------------------------------------------------from__future__importannotationsimportlogging# isort:skiplog=logging.getLogger(__name__)#-----------------------------------------------------------------------------# Imports#-----------------------------------------------------------------------------# Standard library importsfromtypingimportTYPE_CHECKING,Sequence,Union# External importsimportnumpyasnp# Bokeh importsfrom..core.property_mixinsimportFillProps,HatchProps,LinePropsfrom..models.glyphsimportMultiLine,MultiPolygonsfrom..models.renderersimportContourRenderer,GlyphRendererfrom..models.sourcesimportColumnDataSourcefrom..palettesimportlinear_palettefrom..plotting._rendererimport_process_sequence_literalsfrom..util.dataclassesimportdataclass,entriesifTYPE_CHECKING:fromnumpy.typingimportArrayLikefromtyping_extensionsimportTypeAliasfrom..palettesimportPalette,PaletteCollectionfrom..transformimportColorLikeContourColor:TypeAlias=Union[ColorLike,Sequence[ColorLike]]ContourColorOrPalette:TypeAlias=Union[ContourColor,Palette,PaletteCollection,ContourColor]#-----------------------------------------------------------------------------# Globals and constants#-----------------------------------------------------------------------------__all__=('contour_data','from_contour',)#-----------------------------------------------------------------------------# General API#-----------------------------------------------------------------------------@dataclass(frozen=True)classFillCoords:''' Coordinates for all filled polygons over a whole sequence of contour levels. '''xs:list[list[list[np.ndarray]]]ys:list[list[list[np.ndarray]]]@dataclass(frozen=True)classLineCoords:''' Coordinates for all contour lines over a whole sequence of contour levels. '''xs:list[np.ndarray]ys:list[np.ndarray]@dataclass(frozen=True)classContourCoords:''' Combined filled and line contours over a whole sequence of contour levels. '''fill_coords:FillCoords|Noneline_coords:LineCoords|None@dataclass(frozen=True)classFillData(FillCoords):''' Complete geometry data for filled polygons over a whole sequence of contour levels. '''lower_levels:ArrayLikeupper_levels:ArrayLikedefasdict(self):# Convert to dict using shallow copy. dataclasses.asdict uses deep copy.returndict(entries(self))@dataclass(frozen=True)classLineData(LineCoords):''' Complete geometry data for contour lines over a whole sequence of contour levels. '''levels:ArrayLikedefasdict(self):# Convert to dict using shallow copy. dataclasses.asdict uses deep copy.returndict(entries(self))@dataclass(frozen=True)classContourData:''' Complete geometry data for filled polygons and/or contour lines over a whole sequence of contour levels. :func:`~bokeh.plotting.contour.contour_data` returns an object of this class that can then be passed to :func:`bokeh.models.ContourRenderer.set_data`. '''fill_data:FillData|Noneline_data:LineData|None
[docs]defcontour_data(x:ArrayLike|None=None,y:ArrayLike|None=None,z:ArrayLike|np.ma.MaskedArray|None=None,levels:ArrayLike|None=None,*,want_fill:bool=True,want_line:bool=True,)->ContourData:''' Return the contour data of filled and/or line contours that can be passed to :func:`bokeh.models.ContourRenderer.set_data` '''levels=_validate_levels(levels)iflen(levels)<2:want_fill=Falseifnotwant_fillandnotwant_line:raiseValueError("Neither fill nor line requested in contour_data")coords=_contour_coords(x,y,z,levels,want_fill,want_line)fill_data=Noneifcoords.fill_coords:fill_coords=coords.fill_coordsfill_data=FillData(xs=fill_coords.xs,ys=fill_coords.ys,lower_levels=levels[:-1],upper_levels=levels[1:])line_data=Noneifcoords.line_coords:line_coords=coords.line_coordsline_data=LineData(xs=line_coords.xs,ys=line_coords.ys,levels=levels)returnContourData(fill_data,line_data)
[docs]deffrom_contour(x:ArrayLike|None=None,y:ArrayLike|None=None,z:ArrayLike|np.ma.MaskedArray|None=None,levels:ArrayLike|None=None,**visuals,# This is union of LineProps, FillProps and HatchProps)->ContourRenderer:''' Creates a :class:`bokeh.models.ContourRenderer` containing filled polygons and/or contour lines. Usually it is preferable to call :func:`~bokeh.plotting.figure.contour` instead of this function. Filled contour polygons are calculated if ``fill_color`` is set, contour lines if ``line_color`` is set. Args: x (array-like[float] of shape (ny, nx) or (nx,), optional) : The x-coordinates of the ``z`` values. May be 2D with the same shape as ``z.shape``, or 1D with length ``nx = z.shape[1]``. If not specified are assumed to be ``np.arange(nx)``. Must be ordered monotonically. y (array-like[float] of shape (ny, nx) or (ny,), optional) : The y-coordinates of the ``z`` values. May be 2D with the same shape as ``z.shape``, or 1D with length ``ny = z.shape[0]``. If not specified are assumed to be ``np.arange(ny)``. Must be ordered monotonically. z (array-like[float] of shape (ny, nx)) : A 2D NumPy array of gridded values to calculate the contours of. May be a masked array, and any invalid values (``np.inf`` or ``np.nan``) will also be masked out. levels (array-like[float]) : The z-levels to calculate the contours at, must be increasing. Contour lines are calculated at each level and filled contours are calculated between each adjacent pair of levels so the number of sets of contour lines is ``len(levels)`` and the number of sets of filled contour polygons is ``len(levels)-1``. **visuals: |fill properties|, |hatch properties| and |line properties| Fill and hatch properties are used for filled contours, line properties for line contours. If using vectorized properties then the correct number must be used, ``len(levels)`` for line properties and ``len(levels)-1`` for fill and hatch properties. ``fill_color`` and ``line_color`` are more flexible in that they will accept longer sequences and interpolate them to the required number using :func:`~bokeh.palettes.linear_palette`, and also accept palette collections (dictionaries mapping from integer length to color sequence) such as `bokeh.palettes.Cividis`. '''levels=_validate_levels(levels)iflen(levels)<2:want_fill=Falsenlevels=len(levels)want_line=visuals.get("line_color",None)isnotNoneifwant_line:# Handle possible callback or interpolation for line_color.visuals["line_color"]=_color(visuals["line_color"],nlevels)line_cds=ColumnDataSource()_process_sequence_literals(MultiLine,visuals,line_cds,False)# Remove line visuals identified from visuals dict.line_visuals={}fornameinLineProps.properties():prop=visuals.pop(name,None)ifpropisnotNone:line_visuals[name]=propelse:visuals.pop("line_color",None)want_fill=visuals.get("fill_color",None)isnotNoneifwant_fill:# Handle possible callback or interpolation for fill_color.visuals["fill_color"]=_color(visuals["fill_color"],nlevels-1)fill_cds=ColumnDataSource()_process_sequence_literals(MultiPolygons,visuals,fill_cds,False)else:visuals.pop("fill_color",None)# Check for extra unknown kwargs.unknown=visuals.keys()-FillProps.properties()-HatchProps.properties()ifunknown:raiseValueError(f"Unknown keyword arguments in 'from_contour': {', '.join(unknown)}")new_contour_data=contour_data(x=x,y=y,z=z,levels=levels,want_fill=want_fill,want_line=want_line)# Will be other possibilities here like logarithmic....contour_renderer=ContourRenderer(fill_renderer=GlyphRenderer(glyph=MultiPolygons(),data_source=ColumnDataSource()),line_renderer=GlyphRenderer(glyph=MultiLine(),data_source=ColumnDataSource()),levels=list(levels))contour_renderer.set_data(new_contour_data)ifnew_contour_data.fill_data:glyph=contour_renderer.fill_renderer.glyphforname,valueinvisuals.items():setattr(glyph,name,value)cds=contour_renderer.fill_renderer.data_sourceforname,valueinfill_cds.data.items():cds.add(value,name)glyph.line_alpha=0# Don't display lines around fill.glyph.line_width=0ifnew_contour_data.line_data:glyph=contour_renderer.line_renderer.glyphforname,valueinline_visuals.items():setattr(glyph,name,value)cds=contour_renderer.line_renderer.data_sourceforname,valueinline_cds.data.items():cds.add(value,name)returncontour_renderer
#-----------------------------------------------------------------------------# Private API#-----------------------------------------------------------------------------@dataclass(frozen=True)classSingleFillCoords:''' Coordinates for filled contour polygons between a lower and upper level. The first list contains a list for each polygon. The second list contains a separate NumPy array for each boundary of that polygon; the first array is always the outer boundary, subsequent arrays are holes. '''xs:list[list[np.ndarray]]ys:list[list[np.ndarray]]@dataclass(frozen=True)classSingleLineCoords:''' Coordinates for contour lines at a single contour level. The x and y coordinates are stored in a single NumPy array each, with a np.nan separating each line. '''xs:np.ndarrayys:np.ndarraydef_color(color:ContourColorOrPalette,n:int)->ContourColor:# Dict to sequence of colors such as palettes.cividisifisinstance(color,dict):color=color.get(n,None)ifnotcolororlen(color)!=n:raiseValueError(f"Dict of colors does not contain a key of {n}")ifisinstance(color,Sequence)andnotisinstance(color,(bytes,str)):iflen(color)<n:raiseValueError("Insufficient number of colors")eliflen(color)>n:color=linear_palette(color,n)returncolordef_contour_coords(x:ArrayLike|None,y:ArrayLike|None,z:ArrayLike|np.ma.MaskedArray|None,levels:ArrayLike,want_fill:bool,want_line:bool,)->ContourCoords:''' Return the (xs, ys) coords of filled and/or line contours. '''ifnotwant_fillandnotwant_line:raiseRuntimeError("Neither fill nor line requested in _contour_coords")fromcontourpyimportFillType,LineType,contour_generatorcont_gen=contour_generator(x,y,z,line_type=LineType.ChunkCombinedOffset,fill_type=FillType.OuterOffset)fill_coords=Noneifwant_fill:all_xs=[]all_ys=[]foriinrange(len(levels)-1):filled=cont_gen.filled(levels[i],levels[i+1])coords=_filled_to_coords(filled)all_xs.append(coords.xs)all_ys.append(coords.ys)fill_coords=FillCoords(all_xs,all_ys)line_coords=Noneifwant_line:all_xs=[]all_ys=[]forlevelinlevels:lines=cont_gen.lines(level)coords=_lines_to_coords(lines)all_xs.append(coords.xs)all_ys.append(coords.ys)line_coords=LineCoords(all_xs,all_ys)returnContourCoords(fill_coords,line_coords)def_filled_to_coords(filled)->SingleFillCoords:# Processes polygon data returned from a single call to# contourpy.ContourGenerator.filled(lower_level, upper_level)# ContourPy filled data format is FillType.OuterOffset.# 'filled' type awaits type annotations in contourpy.xs=[]ys=[]forpoints,offsetsinzip(*filled):# Polygon with outer boundary and zero or more holes.n=len(offsets)-1xs.append([points[offsets[i]:offsets[i+1],0]foriinrange(n)])ys.append([points[offsets[i]:offsets[i+1],1]foriinrange(n)])returnSingleFillCoords(xs,ys)def_lines_to_coords(lines)->SingleLineCoords:# Processes line data returned from a single call to# contourpy.ContourGenerator.lines(level).# ContourPy line data format is LineType.ChunkCombinedOffset.# 'lines' type awaits type annotations in contourpy.points=lines[0][0]ifpointsisNone:empty=np.empty(0)returnSingleLineCoords(empty,empty)offsets=lines[1][0]npoints=len(points)nlines=len(offsets)-1xs=np.empty(npoints+nlines-1)ys=np.empty(npoints+nlines-1)foriinrange(nlines):start=offsets[i]end=offsets[i+1]ifi>0:xs[start+i-1]=np.nanys[start+i-1]=np.nanxs[start+i:end+i]=points[start:end,0]ys[start+i:end+i]=points[start:end,1]returnSingleLineCoords(xs,ys)def_validate_levels(levels:ArrayLike|None):levels=np.asarray(levels)iflevels.ndim==0orlen(levels)==0:raiseValueError("No contour levels specified")iflen(levels)>1andnp.diff(levels).min()<=0.0:raiseValueError("Contour levels must be increasing")returnlevels