#-----------------------------------------------------------------------------# Copyright (c) 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 importsfromtypingimport(TYPE_CHECKING,Sequence,Union,cast,)# External importsimportnumpyasnp# Bokeh importsfrom..core.property_mixinsimportFillProps,HatchProps,LinePropsfrom..models.glyphsimportMultiLine,MultiPolygonsfrom..models.renderersimportContourRenderer,GlyphRendererfrom..models.sourcesimportColumnDataSourcefrom..palettesimportinterp_palettefrom..plotting._rendererimport_process_sequence_literalsfrom..util.dataclassesimportdataclass,entriesifTYPE_CHECKING:fromcontourpy._contourpyimportFillReturn_OuterOffset,LineReturn_ChunkCombinedNanfromnumpy.typingimportArrayLike,NDArrayfromtyping_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 and hatch_color.visuals["fill_color"]=_color(visuals["fill_color"],nlevels-1)if"hatch_color"invisuals:visuals["hatch_color"]=_color(visuals["hatch_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):return_palette_from_collection(color,n)ifisinstance(color,Sequence)andnotisinstance(color,(bytes,str))andlen(color)!=n:returninterp_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.ChunkCombinedNan,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])# This is guaranteed by use of fill_type=FillType.OuterOffset in contour_generator call.filled=cast("FillReturn_OuterOffset",filled)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)# This is guaranteed by use of line_type=LineType.ChunkCombinedNan in contour_generator call.lines=cast("LineReturn_ChunkCombinedNan",lines)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:FillReturn_OuterOffset)->SingleFillCoords:# Processes polygon data returned from a single call to# contourpy.ContourGenerator.filled(lower_level, upper_level)# ContourPy filled data format is FillType.OuterOffset.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:LineReturn_ChunkCombinedNan)->SingleLineCoords:# Processes line data returned from a single call to# contourpy.ContourGenerator.lines(level).# ContourPy line data format is LineType.ChunkCombinedNan.points=lines[0][0]ifpointsisNone:empty=np.empty(0)returnSingleLineCoords(empty,empty)xs=points[:,0]ys=points[:,1]returnSingleLineCoords(xs,ys)def_palette_from_collection(collection:PaletteCollection,n:int)->Palette:# Return palette of length n from the specified palette collection, which# is a dict[int, Palette]. If the required length palette is in the# collection then return that. If the required length is bigger than the# longest palette then interpolate that. If the required length is smaller# than the shortest palette then interpolate that.iflen(collection)<1:raiseValueError("PaletteCollection is empty")palette=collection.get(n,None)ifpaletteisnotNone:returnpalettemax_key=max(collection.keys())ifisinstance(max_key,int)andn>max_key:returninterp_palette(collection[max_key],n)min_key=min(collection.keys())ifisinstance(min_key,int)andn<min_key:returninterp_palette(collection[min_key],n)raiseValueError(f"Unable to extract or interpolate palette of length {n} from PaletteCollection")def_validate_levels(levels:ArrayLike|None)->NDArray[float]: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