5classStory: 6""" 7 Story class: Implements a structure for creating narrative views of data. 8 9 This class extends the functionality of Altair to include narrative elements 10 such as titles, contexts, call-to-action and annotations. It is designed to facilitate 11 the creation of more engaging and informative data visualisations. 12 """ 13 14def__init__(self,data=None,width=600,height=400,font='Arial',base_font_size=16,**kwargs): 15""" 16 Initialise a Story object. 17 18 Parameters: 19 - data: DataFrame or URL for graph data (default: None) 20 - width: Graph width in pixels (default: 600) 21 - height: Height of the graph in pixels (default: 400) 22 - font: Font to be used for all text elements (default: 'Arial') 23 - base_font_size: Basic font size in pixels (default: 16) 24 - **kwargs: Additional parameters to be passed to the constructor of alt.Chart 25 """ 26# Initialising the Altair Chart object with basic parameters 27self.chart=alt.Chart(data,width=width,height=height,**kwargs) 28self.font=font 29self.base_font_size=base_font_size 30self.story_layers=[]# List for storing history layers 31 32# Dictionaries for the sizes and colours of various text elements 33# These values are multipliers for base_font_size 34self.font_sizes={ 35'title':2,# The title will be twice as big as the basic font 36'subtitle':1.5,# The subtitle will be 1.5 times bigger 37'context':1.2,# The context text will be 1.2 times larger 38'nextstep':1.3,# Next-Step text will be 1.3 times larger 39'source':1# The source text will have the basic size 40} 41# Predefined colours for various text elements 42self.colors={ 43'title':'black', 44'subtitle':'gray', 45'context':'black', 46'nextstep':'black', 47'source':'gray' 48} 49self.config={} 50 51def__getattr__(self,name): 52""" 53 Special method to delegate attributes not found to the Altair Chart object. 54 55 This method is crucial for maintaining compatibility with Altair, allowing 56 to call Altair methods directly on the Story object. 57 58 Parameters: 59 - name: Name of the required attribute 60 61 Returns: 62 - Altair attribute if it exists, otherwise raises an AttributeError 63 """ 64# Search for the attribute in Altair's Chart object 65attr=getattr(self.chart,name) 66 67# If the attribute is callable (i.e. it is a method), we create a wrapper 68ifcallable(attr): 69defwrapped(*args,**kwargs): 70# This wrapped function is created dynamically for each Altair method 71# It is used to intercept Altair method calls and handle them correctly 72 73# Let us call Altair's original method 74result=attr(*args,**kwargs) 75 76# If the result is a new Altair Chart object 77ifisinstance(result,alt.Chart): 78# We update the chart attribute of our Story instance 79self.chart=result 80# We return self (the Story instance) to allow method chaining 81returnself 82# If the result is not a Chart, we return it as it is 83returnresult 84 85# We return the wrapped function instead of the original method 86returnwrapped 87 88# If the attribute is not callable (it is a property), we return it directly 89returnattr 90 91defadd_title(self,title,subtitle=None,title_color=None,subtitle_color=None,title_font_size=None,subtitle_font_size=None,dx=None,dy=None,s_dx=None,s_dy=None): 92""" 93 Adds a title layer (and optional subtitle) to the story. 94 95 Parameters: 96 - title: Main title text 97 - subtitle: Subtitle text (optional) 98 - title_color: Custom colour for the title (optional) 99 - subtitle_color: Custom colour for the subtitle (optional)100 - title_font_size: Custom font size for title in pixels (optional)101 - subtitle_font_size: Custom font size for subtitle in pixels (optional)102 - dx: Horizontal offset of the title in pixels (optional)103 - dy: Vertical offset of the title in pixels (optional)104 - s_dx: Horizontal offset of the subtitle in pixels (optional)105 - s_dy: Vertical offset of the subtitle in pixels (optional)106107 returns:108 - self, to allow method chaining109 """110self.story_layers.append({111'type':'title',112'title':title,113'subtitle':subtitle,114'title_color':title_colororself.colors['title'],115'subtitle_color':subtitle_colororself.colors['subtitle'],116'title_font_size':title_font_sizeorself.em_to_px(self.font_sizes['title']),117'subtitle_font_size':subtitle_font_sizeorself.em_to_px(self.font_sizes['subtitle']),118'dx':dxor0,119'dy':dyor0,120's_dx':s_dxor0,121's_dy':s_dyor0122})123returnself124125defadd_context(self,text,position='left',color=None,dx=0,dy=0,font_size=None):126"""127 Adds a context layer to the story.128129 Parameters:130 - text: The context text to be added131 - position: The position of the text (default: ‘left’)132 - colour: Custom text colour (optional)133 - dx: Horizontal offset in pixels from the base position (default: 0)134 - dy: Vertical offset in pixels from the base position (default: 0)135 - font_size: Custom font size in pixels (optional)136137 returns:138 - self, to allow method chaining139 """140self.story_layers.append({141'type':'context',142'text':text,143'position':position,144'color':colororself.colors['context'],145'dx':dx,146'dy':dy,147'font_size':font_sizeorself.em_to_px(self.font_sizes['context'])148149})150returnself151152153defadd_next_steps(self,154#Basic parameters155text=None,156position='bottom',157mode=None,158title="What can we do next?",159160# General styling parameters (used as fallback)161color=None,# general color of elements162font_family='Arial',163font_size=14,164text_color=None,165opacity=None,166width=None,167height=None,168chart_width=None,169chart_height=None,170space=None,171172# List of texts for steps173texts=None,174175# Parameters per button176url=None,177button_width=120,178button_height=40,179button_color='#80C11E',180button_opacity=0.2,181button_corner_radius=5,182button_text_color='black',183button_font_family=None,# If None, use font_family184button_font_size=None,# If None, use font_size185186# Parameters for line_steps187line_steps_rect_width=10,188line_steps_rect_height=10,189line_steps_space=5,190line_steps_chart_width=700,191line_steps_chart_height=100,192line_steps_color='#80C11E',193line_steps_opacity=0.2,194line_steps_text_color='black',195line_steps_font_family=None,# If None, use font_family196line_steps_font_size=None,# If None, use font_size197198# Parameters for stair_steps199stair_steps_rect_width=10,200stair_steps_rect_height=3,201stair_steps_chart_width=700,202stair_steps_chart_height=300,203stair_steps_color='#80C11E',204stair_steps_opacity=0.2,205stair_steps_text_color='black',206stair_steps_font_family=None,# If None, use font_family207stair_steps_font_size=None,# If None, use font_size208209# Title Parameters210title_color='black',211title_font_family=None,# If None, use font_family212title_font_size=None,# If None, use font_size * 1.4213**kwargs):214215216"""217 Adds a next-step element to the visualisation with multiple customisation options.218219 Parameters220 ---------221 text : str Text for the basic version or for the button222 position : str, default=‘bottom’ Position of the element (‘bottom’, ‘top’, ‘left’, ‘right’)223 type : str, optional Display type (‘line_steps’, ‘button’, ‘stair_steps’)224 title : str, default="What can we do next?’ Title for special visualisations225 colour : str, optional Text colour for the basic version226 font_family : str, default=‘Arial’ Default font227 font_size : int, default=14 Default font size228 texts : list of str List of texts for line_steps and stair_steps229 url : str URL for the button230231 Parameters for Button232 ------------------233 button_width : int, default=120 Button width234 button_height : int, default=40 Height of the button235 button_color : str, default=‘#80C11E’ Background colour of the button236 button_opacity : float, default=0.2 Opacity of the button237 button_corner_radius : int, default=5 Corner radius of the button238 button_text_color : str, default=‘black’ Button text colour239 button_font_family : str, optional Font specific to the button240 button_font_size : int, optional Font size for the button241242 Parameters for Line Steps243 ----------------------244 line_steps_rect_width : int, default=10 Rectangle width245 line_steps_rect_height : int, default=10 Height of rectangles246 line_steps_space : int, default=5 Space between rectangles247 line_steps_chart_width : int, default=700 Total width of the chart248 line_steps_chart_height : int, default=100 Total chart height249 line_steps_color : str, default=‘#80C11E’ Colour of rectangles250 line_steps_opacity : float, default=0.2 Opacity of rectangles251 line_steps_text_colour : str, default=‘black’ Text colour252 line_steps_font_family : str, optional Font specific to line steps253 line_steps_font_size : int, optional Font size for line steps254255 Parameters for Stair Steps256 -----------------------257 stair_steps_rect_width : int, default=10 Width of steps258 stair_steps_rect_height : int, default=3 Height of steps259 stair_steps_chart_width : int, default=700 Total width of the chart260 stair_steps_chart_height : int, default=300 Total height of the chart261 stair_steps_color : str, default=‘#80C11E’ Colour of the steps262 stair_steps_opacity : float, default=0.2 Opacity of the steps263 stair_steps_text_colour : str, default=‘black’ Text colour264 stair_steps_font_family : str, Font specific to stair steps265 stair_steps_font_size : int, optional Font size for stair steps266267 Title parameters268 ----------------------269 title_color : str, default=‘black’ Title colour270 title_font_family : str, optional Specific font for the title271 title_font_size : int, optional Font size for the title272273 Returns274 -------275 self : Story object The current instance for method chaining276 """277278279# Apply general parameters to specific parameters if not set280# Colors and styling281button_color=button_colororcolor282button_opacity=button_opacityoropacity283button_text_color=button_text_colorortext_color284button_width=button_widthorwidth285button_height=button_heightorheight286287line_steps_color=line_steps_colororcolor288line_steps_opacity=line_steps_opacityoropacity289line_steps_text_color=line_steps_text_colorortext_color290line_steps_rect_width=line_steps_rect_widthorwidth291line_steps_rect_height=line_steps_rect_heightorheight292line_steps_space=line_steps_spaceorspace293line_steps_chart_width=line_steps_chart_widthorchart_width294line_steps_chart_height=line_steps_chart_heightorchart_height295296stair_steps_color=stair_steps_colororcolor297stair_steps_opacity=stair_steps_opacityoropacity298stair_steps_text_color=stair_steps_text_colorortext_color299stair_steps_rect_width=stair_steps_rect_widthorwidth300stair_steps_rect_height=stair_steps_rect_heightorheight301stair_steps_chart_width=stair_steps_chart_widthorchart_width302stair_steps_chart_height=stair_steps_chart_heightorchart_height303304title_color=title_colorortext_color305306307308# Basic version (text only)309ifmodeisNone:310iftextisNone:311raiseValueError("The parameter ‘text’ is required for the basic version")312self.story_layers.append({313'type':'cta',314'text':text,315'position':position,316'color':colororself.colors['cta']317})318returnself319320# Mode validation321valid_types=['line_steps','button','stair_steps']322ifmodenotinvalid_types:323raiseValueError(f"Invalid type. Use one of: {', '.join(valid_types)}")324325# Chart creation by mode326ifmode=='button':327iftextisNone:328raiseValueError("The parameter ‘text’ is required for the button type")329ifurlisNone:330raiseValueError("The ‘url’ parameter is required for the button type")331332# Button chart creation333df=pd.DataFrame([{334'text':text,335'url':url,336'x':0,337'y':0338}])339340base=alt.Chart(df).encode(341x=alt.X('x:Q',axis=None),342y=alt.Y('y:Q',axis=None)343).properties(344width=button_width,345height=button_height346)347348button_bg=base.mark_rect(349color=button_color,350opacity=button_opacity,351cornerRadius=button_corner_radius,352width=button_width,353height=button_height354).encode(355href='url:N'356)357358button_text=base.mark_text(359fontSize=button_font_sizeorfont_size,360font=button_font_familyorfont_family,361align='center',362baseline='middle',363color=button_text_color364).encode(365text='text'366)367368chart=alt.layer(button_bg,button_text)369370elifmode=='line_steps':371ifnottextsornotisinstance(texts,list):372raiseValueError("It is necessary to provide a list of texts for line_steps")373iflen(texts)>5:374raiseValueError("Maximum number of steps is 5")375iflen(texts)<1:376raiseValueError("Must provide at least one step")377378# Create DataFrame for rectangles and text379N=len(texts)380x=[i*(line_steps_rect_width+line_steps_space)foriinrange(N)]381y=[0for_inrange(N)]382x2=[(i+1)*line_steps_rect_width+i*line_steps_spaceforiinrange(N)]383y2=[line_steps_rect_heightfor_inrange(N)]384385df_rect=pd.DataFrame({386'x':x,'y':y,'x2':x2,'y2':y2,'text':texts387})388389# Create rectangles390rect=alt.Chart(df_rect).mark_rect(391color=line_steps_color,392opacity=line_steps_opacity393).encode(394x=alt.X('x:Q',axis=None),395y=alt.Y('y:Q',axis=None),396x2='x2:Q',397y2='y2:Q'398).properties(399width=line_steps_chart_width,400height=line_steps_chart_height401)402403# Add text labels404text=alt.Chart(df_rect).mark_text(405fontSize=line_steps_font_sizeorfont_size,406font=line_steps_font_familyorfont_family,407align='left',408dx=10,409lineHeight=18,410color=line_steps_text_color411).encode(412text='text:N',413x=alt.X('x:Q',axis=None),414y=alt.Y('y_half:Q',axis=None),415).transform_calculate(416y_half='datum.y2/2'417)418419ifN>1:420df_line=pd.DataFrame({421'x':[line_steps_rect_width*i+line_steps_space*(i-1)foriinrange(1,N)],422'y':[line_steps_rect_height/2for_inrange(N-1)],423'x2':[(line_steps_rect_width+line_steps_space)*iforiinrange(1,N)],424'y2':[line_steps_rect_height/2for_inrange(N-1)]425})426427line=alt.Chart(df_line).mark_line(428point=True,429strokeWidth=2430).encode(431x=alt.X('x:Q',axis=None),432y=alt.Y('y:Q',axis=None),433x2='x2:Q',434y2='y2:Q'435)436437chart=alt.layer(rect,line,text)438else:439chart=alt.layer(rect,text)440441elifmode=='stair_steps':442ifnottextsornotisinstance(texts,list):443raiseValueError("You must provide a list of texts for stair_steps")444iflen(texts)>5:445raiseValueError("Maximum number of steps is 5")446iflen(texts)<1:447raiseValueError("Must provide at least one step")448449# Create DataFrame for rectangles and text450N=len(texts)451x=[i*stair_steps_rect_widthforiinrange(N)]452y=[i*stair_steps_rect_heightforiinrange(N)]453x2=[(i+1)*stair_steps_rect_widthforiinrange(N)]454y2=[(i+1)*stair_steps_rect_heightforiinrange(N)]455456df_rect=pd.DataFrame({457'x':x,'y':y,'x2':x2,'y2':y2,'text':texts458})459460# Create rectangles461rect=alt.Chart(df_rect).mark_rect(462color=stair_steps_color,463opacity=stair_steps_opacity464).encode(465x=alt.X('x:Q',axis=None),466y=alt.Y('y:Q',axis=None,scale=alt.Scale(domain=[0,N*stair_steps_rect_height])),467x2='x2:Q',468y2='y2:Q'469).properties(470width=stair_steps_chart_width,471height=stair_steps_chart_height472)473474# Add text labels475text=alt.Chart(df_rect).mark_text(476fontSize=stair_steps_font_sizeorfont_size,477font=stair_steps_font_familyorfont_family,478align='left',479dx=10,480dy=0,481color=stair_steps_text_color482).encode(483text=alt.Text('text'),484x=alt.X('x:Q',axis=None),485y=alt.Y('y_mid:Q',axis=None),486).transform_calculate(487y_mid='(datum.y + datum.y2)/2'488)489490ifN>1:491line_data=[]492foriinrange(N-1):493line_data.append({494'x':x2[i],495'y':y2[i],496'x2':x[i+1],497'y2':y[i+1]498})499500df_line=pd.DataFrame(line_data)501502line=alt.Chart(df_line).mark_line(503point=True,504strokeWidth=2505).encode(506x=alt.X('x:Q',axis=None),507y=alt.Y('y:Q',axis=None),508x2='x2:Q',509y2='y2:Q'510)511512chart=alt.layer(rect,line,text)513else:514chart=alt.layer(rect,text)515516# Addition of title with customisable font517iftitle:518chart=chart.properties(519title=alt.TitleParams(520text=[title],521fontSize=title_font_sizeor(font_size*1.4),522font=title_font_familyorfont_family,523color=title_color,524offset=10525)526)527528# Addition to layer529self.story_layers.append({530'type':'special_cta',531'chart':chart,532'position':position533})534535returnself536537defadd_source(self,text,position='bottom',vertical=False,color=None,dx=None,dy=None,font_size=None):538"""539 Add a source layer to the story.540541 Parameters:542 - text: The source text543 - position: The position of the text (default: 'bottom')544 - vertical: If True, the text will be rotated 90 degrees (default: False)545 - color: Custom text color (optional)546 - dx: Horizontal movement of the text (optional)547 - dy: Vertical movement of the text (optional)548 - font_size: Custom font size (optional)549550 Ritorna:551 - self, to allow the method chaining552 """553self.story_layers.append({554'type':'source',555'text':text,556'position':position,557'vertical':vertical,558'color':colororself.colors['source'],559'dx':dxor0,560'dy':dyor0,561'font_size':font_sizeorself.em_to_px(self.font_sizes['source'])562563})564returnself565566defadd_annotation(self,x_point,y_point,annotation_text="Point of interest",567arrow_direction='right',arrow_color='blue',arrow_size=40,568label_color='black',label_size=12,569show_point=True,point_color='red',point_size=60,570arrow_dx=0,arrow_dy=-45,571label_dx=37,label_dy=-37):572"""573 Create an arrow annotation on the graph.574575 This method is essential for highlighting specific points in the graph and adding576 contextual explanations. It is particularly useful for data narration, allowing577 to guide the observer's attention to relevant aspects of the visualisation.578579 Parameters:580 - x_point, y_point: Coordinates of the point to be annotated581 - annotation_text: Text of the annotation (default: ‘Point of interest’)582 - arrow_direction: Direction of the arrow (default: ‘right’)583 - arrow_color, arrow_size: Arrow colour and size584 - label_colour, label_size: Colour and size of the annotation text585 - show_point: If True, shows a point at the annotation location586 - point_color, point_size: Colour and size of the point587 - arrow_dx, arrow_dy: Distances in pixels to be added to the arrow position (default:0, -45)588 - label_dx, label_dy: Distances in pixels to be added to the label position (default:37, -37)589590 returns:591 - self, to allow method chaining592 """593594# Dictionary that maps arrow directions to corresponding Unicode symbols595arrow_symbols={596'left':'←','right':'→','up':'↑','down':'↓',597'upleft':'↖','upright':'↗','downleft':'↙','downright':'↘',598'leftup':'↰','leftdown':'↲','rightup':'↱','rightdown':'↳',599'upleftcurve':'↺','uprightcurve':'↻'600}601602# Check that the direction of the arrow is valid603ifarrow_directionnotinarrow_symbols:604raiseValueError(f"Invalid arrow direction. Use one of: {', '.join(arrow_symbols.keys())}")605606# Select the appropriate arrow symbol607arrow_symbol=arrow_symbols[arrow_direction]608609# checks whether the encoding has already been defined610ifnothasattr(self.chart,'encoding')ornothasattr(self.chart.encoding,'x'):611# If encoding is not defined use of default values612x_type='Q'613y_type='Q'614else:615# If encoding is defined, we extract the data types for the x and y axes616x_type=self.chart.encoding.x.shorthand.split(':')[-1]617y_type=self.chart.encoding.y.shorthand.split(':')[-1]618619# Explanation of data type extraction:620# 1. We first check whether the encoding has been defined. This is important because621# the user may call this method before defining the encoding of the graph.622# (The encoding in Altair defines how the data is mapped to the visual properties of the graph).623# 2. If encoding is not defined, we use ‘Q’ (Quantitative) as the default data type624# for both axes. This allows the method to work even without a defined encoding.625# 3. If the encoding is defined, we proceed with the data type extraction as before:626# - self.chart.encoding.x.shorthand: accesses the shorthand of the x-axis627# .shorthand: In Altair, ‘shorthand’ is a concise notation for specifying the encoding.628# Example of shorthand: ‘column:Q’ where ‘column’ is the name of the data column629# and ‘Q’ is the data type (in this case, Quantitative).630# - split(‘:’)[-1]: splits the shorthand at ‘:’ and takes the last element (the data type)631# Example: If shorthand is ‘price:Q’, split(‘:’) will return [‘price’, ‘Q’].632# 4. The extracted data type will be one of:633# - ‘Q’: Quantitative (continuous numeric)634# - ‘O’: Ordinal (ordered categories)635# N': Nominal (unordered categories)636# T': Temporal (dates or times)637638# Important: This operation is essential to ensure that the annotations639# added to the graph are consistent with the original axis data types.640# Without this match, annotations may be positioned641# incorrectly or cause rendering errors.642643# Create a DataFrame with a single point for the annotation644annotation_data=pd.DataFrame({'x':[x_point],'y':[y_point]})645646# Internal function to create the correct encoding based on the data type647defcreate_encoding(field,dtype):648"""649 Creates the appropriate encoding for Altair based on the data type.650 This function is crucial to ensure that the annotation is651 consistent with the original chart data type.652 """653654ifdtype=='O':# Ordinal655returnalt.X(field+':O',title='')iffield=='x'elsealt.Y(field+':O',title='')656elifdtype=='N':# Nominal657returnalt.X(field+':N',title='')iffield=='x'elsealt.Y(field+':N',title='')658elifdtype=='T':# Temporal659returnalt.X(field+':T',title='')iffield=='x'elsealt.Y(field+':T',title='')660else:# Quantity (default)661returnalt.X(field+':Q',title='')iffield=='x'elsealt.Y(field+':Q',title='')662663# Initialises the list of annotation layers664layers=[]665666# Adds full stop if required667ifshow_point:668point_layer=alt.Chart(annotation_data).mark_point(669color=point_color,670size=point_size671).encode(672x=create_encoding('x',x_type),673y=create_encoding('y',y_type)674)675layers.append(point_layer)676677# Adds the arrow678# We use mark_text to draw the arrow using a Unicode character679arrow_layer=alt.Chart(annotation_data).mark_text(680text=arrow_symbol,681fontSize=arrow_size,682dx=arrow_dx,# Horizontal offset to position the arrow was 22683dy=arrow_dy,# Vertical offset to position the arrow was -22684color=arrow_color685).encode(686x=create_encoding('x',x_type),687y=create_encoding('y',y_type)688)689layers.append(arrow_layer)690691# Adds text label692label_layer=alt.Chart(annotation_data).mark_text(693align='left',694baseline='top',695dx=label_dx,# Horizontal offset to position text was 37696dy=label_dy,# Vertical offset to position text was -37697fontSize=label_size,698color=label_color,699text=annotation_text700).encode(701x=create_encoding('x',x_type),702y=create_encoding('y',y_type)703)704layers.append(label_layer)705706# Combine all layers into a single annotation707annotation=alt.layer(*layers)708709# Adds annotation to history layers710self.story_layers.append({711'type':'annotation',712'chart':annotation713})714715# Note on flexibility and robustness:716# This approach makes the add_annotation method more flexible,717# as it can automatically adapt to different types of graphs without718# requiring manual input on the axis data type. In addition, using719# the Altair shorthand, the code automatically adapts even if the user720# has specified the encoding in different ways (e.g., using alt.X(‘column:Q’)721# or alt.X(‘column’, type=‘quantitative’)).722723724returnself# Return self to allow method chaining725726727defadd_line(self,value,orientation='horizontal',color='red',stroke_width=2,stroke_dash=[]):728"""729 Adds a reference line to the story.730731 Parameters:732 - value: The position of the line (x or y value)733 - orientation: 'horizontal' or 'vertical' (default: 'horizontal')734 - color: Color of the line (default: 'red')735 - stroke_width: Width of the line in pixels (default: 2)736 - stroke_dash: Pattern for dashed line, e.g. [5,5] (default: [] for solid line)737738 Returns:739 - self, to allow method chaining740 """741742iforientationnotin['horizontal','vertical']:743raiseValueError("orientation must be either 'horizontal' or 'vertical'")744745# Crea un DataFrame con un singolo valore746iforientation=='horizontal':747data=pd.DataFrame({'y':[value]})748encoding={'y':'y:Q'}749else:# vertical750data=pd.DataFrame({'x':[value]})751encoding={'x':'x:Q'}752753# definizione dei parametri della line754mark_params={755'color':color,756'strokeWidth':stroke_width,757}758759# Viene aggiunto stokeDash solo se è specificato un pattern760ifstroke_dash:761mark_params['strokeDash']=stroke_dash762763# Crea la linea764line=alt.Chart(data).mark_rule(765color=color,766strokeWidth=stroke_width,767strokeDash=stroke_dash768).encode(769**encoding770)771772# Aggiunge la linea ai layer della storia773self.story_layers.append({774'type':'line',775'chart':line776})777778returnself779780781782defem_to_px(self,em):783"""784 Converts a dimension from em to pixels.785786 This function is essential for maintaining visual consistency787 between different textual elements and devices.788789 Parameters:790 - em: Size in em791792 Returns:793 - Equivalent size in pixels (integer)794 """795returnint(em*self.base_font_size)796797def_get_position(self,position):798"""799 Calculates the x and y co-ordinates for positioning text elements in the graph.800801 This method is crucial for the correct positioning of various elements802 narrative elements such as context, call-to-action and sources.803804 Parameters:805 - position: String indicating the desired position (e.g. ‘left’, ‘right’, ‘top’, etc.).806807 Returns:808 - Tuple (x, y) representing the coordinates in pixels809 """810# Dictionary mapping positions to coordinates (x, y)811# Co-ordinates are calculated from the size of the graph812positions={813'left':(-100,self.chart.height/2),# 10 pixel from the left edge, centred vertically814'right':(self.chart.width+20,self.chart.height/2),# 10 pixel from the right edge, centred vertically815'top':(self.chart.width/2,80),# Centred horizontally, 80 pixels from above816'bottom':(self.chart.width/2,self.chart.height+40),# Horizontally centred, 40 pixels from bottom817'center':(self.chart.width/2,self.chart.height/2),# Graph Centre818'side-left':(-150,self.chart.height/2),# 10 pixels to the left of the border, centred vertically819'side-right':(self.chart.width+50,self.chart.height/2),# 10 pixels to the right of the border, centred vertically820}821822# If the required position is not in the dictionary, use a default position823# In this case, horizontally centred and 20 pixels from the bottom824returnpositions.get(position,(self.chart.width/2,self.chart.height-20))825826defcreate_title_layer(self,layer):827"""828 Creates the title layer (and subtitle if present).829830 This method is responsible for visually creating the main title831 and the optional subtitle of the graphic.832833 Parameters:834 - Layer: Dictionary containing the title information835836 Returns:837 - Altair Chart object representing the title layer838 """839title_chart=alt.Chart(self.chart.data).mark_text(840text=layer['title'],841fontSize=layer['title_font_size'],842fontWeight='bold',843align='center',844font=self.font,845color=layer['title_color']846).encode(847x=alt.value(self.chart.width/2+layer.get('dx',0)),# Centre horizontally848y=alt.value(-50+layer.get('dy',0))# Position 20 pixels from above849)850851iflayer['subtitle']:852subtitle_chart=alt.Chart(self.chart.data).mark_text(853text=layer['subtitle'],854fontSize=layer['subtitle_font_size'],855align='center',856font=self.font,857color=layer['subtitle_color']858).encode(859x=alt.value(self.chart.width/2+layer.get('s_dx',0)),# Centre horizontally860y=alt.value(-20+layer.get('s_dy',0))# Position 50 pixels from the top (below the title)861)862returntitle_chart+subtitle_chart863returntitle_chart864865defcreate_text_layer(self,layer):866"""867 Creates a generic text layer (context, next-steps, source).868869 This method is used to create text layers for various narrative purposes,870 such as adding context, next-steps or data source information.871872 Parameters:873 - Layer: Dictionary containing the text information to be added874875 Returns:876 - Altair Chart object representing the text layer877 """878x,y=self._get_position(layer['position'])879880# Apply offsets if they exist (for context layers)881iflayer['type']in['context','source']:882x+=layer.get('dx',0)883y+=layer.get('dy',0)884885returnalt.Chart(self.chart.data).mark_text(886text=layer['text'],887fontSize=layer.get('font_size',self.em_to_px(self.font_sizes[layer['type']])),888align='center',889baseline='middle',890font=self.font,891angle=270iflayer.get('vertical',False)else0,# Rotate text if ‘vertical’ is True892color=layer['color']893).encode(894x=alt.value(x),895y=alt.value(y)896)897898defconfigure_view(self,*args,**kwargs):899"""900 Configure aspects of the graph view using Altair's configure_view method.901902 This method allows you to configure various aspects of the chart view, such as903 the background colour, border style, internal spacing, etc.904905 Parameters:906 *args, **kwargs: Arguments to pass to the Altair configure_view method.907908 stores the view configuration for application during rendering.909 """910self.config['view']=kwargs911returnself912913914defrender(self):915"""916 It renders all layers of the story in a single graphic. 917 """918# Let's start with the basic graph919main_chart=self.chart920921# Create separate lists to place special graphics922top_charts=[]923bottom_charts=[]924left_charts=[]925right_charts=[]926overlay_charts=[]927928# Organise the layers according to their position929forlayerinself.story_layers:930iflayer['type']=='special_cta':931# We take the position from the layer932iflayer.get('position')=='top':933top_charts.append(layer['chart'])934eliflayer.get('position')=='bottom':935bottom_charts.append(layer['chart'])936eliflayer.get('position')=='left':937left_charts.append(layer['chart'])938eliflayer.get('position')=='right':939right_charts.append(layer['chart'])940eliflayer['type']=='title':941overlay_charts.append(self.create_title_layer(layer))942eliflayer['type']in['context','cta','source']:943overlay_charts.append(self.create_text_layer(layer))944eliflayer['type']in['shape','shape_label','annotation']:945overlay_charts.append(layer['chart'])946eliflayer['type']=='line':947overlay_charts.append(layer['chart'])948949# Overlaying the layers on the main graph950foroverlayinoverlay_charts:951main_chart+=overlay952953# Build the final layout954ifleft_charts:955main_chart=alt.hconcat(alt.vconcat(*left_charts),main_chart)956ifright_charts:957main_chart=alt.hconcat(main_chart,alt.vconcat(*right_charts))958iftop_charts:959main_chart=alt.vconcat(alt.hconcat(*top_charts),main_chart)960ifbottom_charts:961main_chart=alt.vconcat(main_chart,alt.hconcat(*bottom_charts))962963# Apply configurations964if'view'inself.config:965main_chart=main_chart.configure_view(**self.config['view'])966967returnmain_chart.resolve_axis(x='independent',y='independent')
Story class: Implements a structure for creating narrative views of data.
This class extends the functionality of Altair to include narrative elements
such as titles, contexts, call-to-action and annotations. It is designed to facilitate
the creation of more engaging and informative data visualisations.
14def__init__(self,data=None,width=600,height=400,font='Arial',base_font_size=16,**kwargs):15"""16 Initialise a Story object.1718 Parameters:19 - data: DataFrame or URL for graph data (default: None)20 - width: Graph width in pixels (default: 600)21 - height: Height of the graph in pixels (default: 400)22 - font: Font to be used for all text elements (default: 'Arial')23 - base_font_size: Basic font size in pixels (default: 16)24 - **kwargs: Additional parameters to be passed to the constructor of alt.Chart25 """26# Initialising the Altair Chart object with basic parameters27self.chart=alt.Chart(data,width=width,height=height,**kwargs)28self.font=font29self.base_font_size=base_font_size30self.story_layers=[]# List for storing history layers3132# Dictionaries for the sizes and colours of various text elements33# These values are multipliers for base_font_size34self.font_sizes={35'title':2,# The title will be twice as big as the basic font36'subtitle':1.5,# The subtitle will be 1.5 times bigger37'context':1.2,# The context text will be 1.2 times larger38'nextstep':1.3,# Next-Step text will be 1.3 times larger39'source':1# The source text will have the basic size40}41# Predefined colours for various text elements42self.colors={43'title':'black',44'subtitle':'gray',45'context':'black',46'nextstep':'black',47'source':'gray'48}49self.config={}
Initialise a Story object.
Parameters:
data: DataFrame or URL for graph data (default: None)
width: Graph width in pixels (default: 600)
height: Height of the graph in pixels (default: 400)
font: Font to be used for all text elements (default: 'Arial')
base_font_size: Basic font size in pixels (default: 16)
**kwargs: Additional parameters to be passed to the constructor of alt.Chart
91defadd_title(self,title,subtitle=None,title_color=None,subtitle_color=None,title_font_size=None,subtitle_font_size=None,dx=None,dy=None,s_dx=None,s_dy=None): 92""" 93 Adds a title layer (and optional subtitle) to the story. 94 95 Parameters: 96 - title: Main title text 97 - subtitle: Subtitle text (optional) 98 - title_color: Custom colour for the title (optional) 99 - subtitle_color: Custom colour for the subtitle (optional)100 - title_font_size: Custom font size for title in pixels (optional)101 - subtitle_font_size: Custom font size for subtitle in pixels (optional)102 - dx: Horizontal offset of the title in pixels (optional)103 - dy: Vertical offset of the title in pixels (optional)104 - s_dx: Horizontal offset of the subtitle in pixels (optional)105 - s_dy: Vertical offset of the subtitle in pixels (optional)106107 returns:108 - self, to allow method chaining109 """110self.story_layers.append({111'type':'title',112'title':title,113'subtitle':subtitle,114'title_color':title_colororself.colors['title'],115'subtitle_color':subtitle_colororself.colors['subtitle'],116'title_font_size':title_font_sizeorself.em_to_px(self.font_sizes['title']),117'subtitle_font_size':subtitle_font_sizeorself.em_to_px(self.font_sizes['subtitle']),118'dx':dxor0,119'dy':dyor0,120's_dx':s_dxor0,121's_dy':s_dyor0122})123returnself
Adds a title layer (and optional subtitle) to the story.
Parameters:
title: Main title text
subtitle: Subtitle text (optional)
title_color: Custom colour for the title (optional)
subtitle_color: Custom colour for the subtitle (optional)
title_font_size: Custom font size for title in pixels (optional)
subtitle_font_size: Custom font size for subtitle in pixels (optional)
dx: Horizontal offset of the title in pixels (optional)
dy: Vertical offset of the title in pixels (optional)
s_dx: Horizontal offset of the subtitle in pixels (optional)
s_dy: Vertical offset of the subtitle in pixels (optional)
125defadd_context(self,text,position='left',color=None,dx=0,dy=0,font_size=None):126"""127 Adds a context layer to the story.128129 Parameters:130 - text: The context text to be added131 - position: The position of the text (default: ‘left’)132 - colour: Custom text colour (optional)133 - dx: Horizontal offset in pixels from the base position (default: 0)134 - dy: Vertical offset in pixels from the base position (default: 0)135 - font_size: Custom font size in pixels (optional)136137 returns:138 - self, to allow method chaining139 """140self.story_layers.append({141'type':'context',142'text':text,143'position':position,144'color':colororself.colors['context'],145'dx':dx,146'dy':dy,147'font_size':font_sizeorself.em_to_px(self.font_sizes['context'])148149})150returnself
Adds a context layer to the story.
Parameters:
text: The context text to be added
position: The position of the text (default: ‘left’)
colour: Custom text colour (optional)
dx: Horizontal offset in pixels from the base position (default: 0)
dy: Vertical offset in pixels from the base position (default: 0)
font_size: Custom font size in pixels (optional)
returns:
self, to allow method chaining
defadd_next_steps(self,text=None,position='bottom',mode=None,title='What can we do next?',color=None,font_family='Arial',font_size=14,text_color=None,opacity=None,width=None,height=None,chart_width=None,chart_height=None,space=None,texts=None,url=None,button_width=120,button_height=40,button_color='#80C11E',button_opacity=0.2,button_corner_radius=5,button_text_color='black',button_font_family=None,button_font_size=None,line_steps_rect_width=10,line_steps_rect_height=10,line_steps_space=5,line_steps_chart_width=700,line_steps_chart_height=100,line_steps_color='#80C11E',line_steps_opacity=0.2,line_steps_text_color='black',line_steps_font_family=None,line_steps_font_size=None,stair_steps_rect_width=10,stair_steps_rect_height=3,stair_steps_chart_width=700,stair_steps_chart_height=300,stair_steps_color='#80C11E',stair_steps_opacity=0.2,stair_steps_text_color='black',stair_steps_font_family=None,stair_steps_font_size=None,title_color='black',title_font_family=None,title_font_size=None,**kwargs):
153defadd_next_steps(self,154#Basic parameters155text=None,156position='bottom',157mode=None,158title="What can we do next?",159160# General styling parameters (used as fallback)161color=None,# general color of elements162font_family='Arial',163font_size=14,164text_color=None,165opacity=None,166width=None,167height=None,168chart_width=None,169chart_height=None,170space=None,171172# List of texts for steps173texts=None,174175# Parameters per button176url=None,177button_width=120,178button_height=40,179button_color='#80C11E',180button_opacity=0.2,181button_corner_radius=5,182button_text_color='black',183button_font_family=None,# If None, use font_family184button_font_size=None,# If None, use font_size185186# Parameters for line_steps187line_steps_rect_width=10,188line_steps_rect_height=10,189line_steps_space=5,190line_steps_chart_width=700,191line_steps_chart_height=100,192line_steps_color='#80C11E',193line_steps_opacity=0.2,194line_steps_text_color='black',195line_steps_font_family=None,# If None, use font_family196line_steps_font_size=None,# If None, use font_size197198# Parameters for stair_steps199stair_steps_rect_width=10,200stair_steps_rect_height=3,201stair_steps_chart_width=700,202stair_steps_chart_height=300,203stair_steps_color='#80C11E',204stair_steps_opacity=0.2,205stair_steps_text_color='black',206stair_steps_font_family=None,# If None, use font_family207stair_steps_font_size=None,# If None, use font_size208209# Title Parameters210title_color='black',211title_font_family=None,# If None, use font_family212title_font_size=None,# If None, use font_size * 1.4213**kwargs):214215216"""217 Adds a next-step element to the visualisation with multiple customisation options.218219 Parameters220 ---------221 text : str Text for the basic version or for the button222 position : str, default=‘bottom’ Position of the element (‘bottom’, ‘top’, ‘left’, ‘right’)223 type : str, optional Display type (‘line_steps’, ‘button’, ‘stair_steps’)224 title : str, default="What can we do next?’ Title for special visualisations225 colour : str, optional Text colour for the basic version226 font_family : str, default=‘Arial’ Default font227 font_size : int, default=14 Default font size228 texts : list of str List of texts for line_steps and stair_steps229 url : str URL for the button230231 Parameters for Button232 ------------------233 button_width : int, default=120 Button width234 button_height : int, default=40 Height of the button235 button_color : str, default=‘#80C11E’ Background colour of the button236 button_opacity : float, default=0.2 Opacity of the button237 button_corner_radius : int, default=5 Corner radius of the button238 button_text_color : str, default=‘black’ Button text colour239 button_font_family : str, optional Font specific to the button240 button_font_size : int, optional Font size for the button241242 Parameters for Line Steps243 ----------------------244 line_steps_rect_width : int, default=10 Rectangle width245 line_steps_rect_height : int, default=10 Height of rectangles246 line_steps_space : int, default=5 Space between rectangles247 line_steps_chart_width : int, default=700 Total width of the chart248 line_steps_chart_height : int, default=100 Total chart height249 line_steps_color : str, default=‘#80C11E’ Colour of rectangles250 line_steps_opacity : float, default=0.2 Opacity of rectangles251 line_steps_text_colour : str, default=‘black’ Text colour252 line_steps_font_family : str, optional Font specific to line steps253 line_steps_font_size : int, optional Font size for line steps254255 Parameters for Stair Steps256 -----------------------257 stair_steps_rect_width : int, default=10 Width of steps258 stair_steps_rect_height : int, default=3 Height of steps259 stair_steps_chart_width : int, default=700 Total width of the chart260 stair_steps_chart_height : int, default=300 Total height of the chart261 stair_steps_color : str, default=‘#80C11E’ Colour of the steps262 stair_steps_opacity : float, default=0.2 Opacity of the steps263 stair_steps_text_colour : str, default=‘black’ Text colour264 stair_steps_font_family : str, Font specific to stair steps265 stair_steps_font_size : int, optional Font size for stair steps266267 Title parameters268 ----------------------269 title_color : str, default=‘black’ Title colour270 title_font_family : str, optional Specific font for the title271 title_font_size : int, optional Font size for the title272273 Returns274 -------275 self : Story object The current instance for method chaining276 """277278279# Apply general parameters to specific parameters if not set280# Colors and styling281button_color=button_colororcolor282button_opacity=button_opacityoropacity283button_text_color=button_text_colorortext_color284button_width=button_widthorwidth285button_height=button_heightorheight286287line_steps_color=line_steps_colororcolor288line_steps_opacity=line_steps_opacityoropacity289line_steps_text_color=line_steps_text_colorortext_color290line_steps_rect_width=line_steps_rect_widthorwidth291line_steps_rect_height=line_steps_rect_heightorheight292line_steps_space=line_steps_spaceorspace293line_steps_chart_width=line_steps_chart_widthorchart_width294line_steps_chart_height=line_steps_chart_heightorchart_height295296stair_steps_color=stair_steps_colororcolor297stair_steps_opacity=stair_steps_opacityoropacity298stair_steps_text_color=stair_steps_text_colorortext_color299stair_steps_rect_width=stair_steps_rect_widthorwidth300stair_steps_rect_height=stair_steps_rect_heightorheight301stair_steps_chart_width=stair_steps_chart_widthorchart_width302stair_steps_chart_height=stair_steps_chart_heightorchart_height303304title_color=title_colorortext_color305306307308# Basic version (text only)309ifmodeisNone:310iftextisNone:311raiseValueError("The parameter ‘text’ is required for the basic version")312self.story_layers.append({313'type':'cta',314'text':text,315'position':position,316'color':colororself.colors['cta']317})318returnself319320# Mode validation321valid_types=['line_steps','button','stair_steps']322ifmodenotinvalid_types:323raiseValueError(f"Invalid type. Use one of: {', '.join(valid_types)}")324325# Chart creation by mode326ifmode=='button':327iftextisNone:328raiseValueError("The parameter ‘text’ is required for the button type")329ifurlisNone:330raiseValueError("The ‘url’ parameter is required for the button type")331332# Button chart creation333df=pd.DataFrame([{334'text':text,335'url':url,336'x':0,337'y':0338}])339340base=alt.Chart(df).encode(341x=alt.X('x:Q',axis=None),342y=alt.Y('y:Q',axis=None)343).properties(344width=button_width,345height=button_height346)347348button_bg=base.mark_rect(349color=button_color,350opacity=button_opacity,351cornerRadius=button_corner_radius,352width=button_width,353height=button_height354).encode(355href='url:N'356)357358button_text=base.mark_text(359fontSize=button_font_sizeorfont_size,360font=button_font_familyorfont_family,361align='center',362baseline='middle',363color=button_text_color364).encode(365text='text'366)367368chart=alt.layer(button_bg,button_text)369370elifmode=='line_steps':371ifnottextsornotisinstance(texts,list):372raiseValueError("It is necessary to provide a list of texts for line_steps")373iflen(texts)>5:374raiseValueError("Maximum number of steps is 5")375iflen(texts)<1:376raiseValueError("Must provide at least one step")377378# Create DataFrame for rectangles and text379N=len(texts)380x=[i*(line_steps_rect_width+line_steps_space)foriinrange(N)]381y=[0for_inrange(N)]382x2=[(i+1)*line_steps_rect_width+i*line_steps_spaceforiinrange(N)]383y2=[line_steps_rect_heightfor_inrange(N)]384385df_rect=pd.DataFrame({386'x':x,'y':y,'x2':x2,'y2':y2,'text':texts387})388389# Create rectangles390rect=alt.Chart(df_rect).mark_rect(391color=line_steps_color,392opacity=line_steps_opacity393).encode(394x=alt.X('x:Q',axis=None),395y=alt.Y('y:Q',axis=None),396x2='x2:Q',397y2='y2:Q'398).properties(399width=line_steps_chart_width,400height=line_steps_chart_height401)402403# Add text labels404text=alt.Chart(df_rect).mark_text(405fontSize=line_steps_font_sizeorfont_size,406font=line_steps_font_familyorfont_family,407align='left',408dx=10,409lineHeight=18,410color=line_steps_text_color411).encode(412text='text:N',413x=alt.X('x:Q',axis=None),414y=alt.Y('y_half:Q',axis=None),415).transform_calculate(416y_half='datum.y2/2'417)418419ifN>1:420df_line=pd.DataFrame({421'x':[line_steps_rect_width*i+line_steps_space*(i-1)foriinrange(1,N)],422'y':[line_steps_rect_height/2for_inrange(N-1)],423'x2':[(line_steps_rect_width+line_steps_space)*iforiinrange(1,N)],424'y2':[line_steps_rect_height/2for_inrange(N-1)]425})426427line=alt.Chart(df_line).mark_line(428point=True,429strokeWidth=2430).encode(431x=alt.X('x:Q',axis=None),432y=alt.Y('y:Q',axis=None),433x2='x2:Q',434y2='y2:Q'435)436437chart=alt.layer(rect,line,text)438else:439chart=alt.layer(rect,text)440441elifmode=='stair_steps':442ifnottextsornotisinstance(texts,list):443raiseValueError("You must provide a list of texts for stair_steps")444iflen(texts)>5:445raiseValueError("Maximum number of steps is 5")446iflen(texts)<1:447raiseValueError("Must provide at least one step")448449# Create DataFrame for rectangles and text450N=len(texts)451x=[i*stair_steps_rect_widthforiinrange(N)]452y=[i*stair_steps_rect_heightforiinrange(N)]453x2=[(i+1)*stair_steps_rect_widthforiinrange(N)]454y2=[(i+1)*stair_steps_rect_heightforiinrange(N)]455456df_rect=pd.DataFrame({457'x':x,'y':y,'x2':x2,'y2':y2,'text':texts458})459460# Create rectangles461rect=alt.Chart(df_rect).mark_rect(462color=stair_steps_color,463opacity=stair_steps_opacity464).encode(465x=alt.X('x:Q',axis=None),466y=alt.Y('y:Q',axis=None,scale=alt.Scale(domain=[0,N*stair_steps_rect_height])),467x2='x2:Q',468y2='y2:Q'469).properties(470width=stair_steps_chart_width,471height=stair_steps_chart_height472)473474# Add text labels475text=alt.Chart(df_rect).mark_text(476fontSize=stair_steps_font_sizeorfont_size,477font=stair_steps_font_familyorfont_family,478align='left',479dx=10,480dy=0,481color=stair_steps_text_color482).encode(483text=alt.Text('text'),484x=alt.X('x:Q',axis=None),485y=alt.Y('y_mid:Q',axis=None),486).transform_calculate(487y_mid='(datum.y + datum.y2)/2'488)489490ifN>1:491line_data=[]492foriinrange(N-1):493line_data.append({494'x':x2[i],495'y':y2[i],496'x2':x[i+1],497'y2':y[i+1]498})499500df_line=pd.DataFrame(line_data)501502line=alt.Chart(df_line).mark_line(503point=True,504strokeWidth=2505).encode(506x=alt.X('x:Q',axis=None),507y=alt.Y('y:Q',axis=None),508x2='x2:Q',509y2='y2:Q'510)511512chart=alt.layer(rect,line,text)513else:514chart=alt.layer(rect,text)515516# Addition of title with customisable font517iftitle:518chart=chart.properties(519title=alt.TitleParams(520text=[title],521fontSize=title_font_sizeor(font_size*1.4),522font=title_font_familyorfont_family,523color=title_color,524offset=10525)526)527528# Addition to layer529self.story_layers.append({530'type':'special_cta',531'chart':chart,532'position':position533})534535returnself
Adds a next-step element to the visualisation with multiple customisation options.
Parameters
text : str Text for the basic version or for the button
position : str, default=‘bottom’ Position of the element (‘bottom’, ‘top’, ‘left’, ‘right’)
type : str, optional Display type (‘line_steps’, ‘button’, ‘stair_steps’)
title : str, default="What can we do next?’ Title for special visualisations
colour : str, optional Text colour for the basic version
font_family : str, default=‘Arial’ Default font
font_size : int, default=14 Default font size
texts : list of str List of texts for line_steps and stair_steps
url : str URL for the button
Parameters for Button
button_width : int, default=120 Button width
button_height : int, default=40 Height of the button
button_color : str, default=‘#80C11E’ Background colour of the button
button_opacity : float, default=0.2 Opacity of the button
button_corner_radius : int, default=5 Corner radius of the button
button_text_color : str, default=‘black’ Button text colour
button_font_family : str, optional Font specific to the button
button_font_size : int, optional Font size for the button
Parameters for Line Steps
line_steps_rect_width : int, default=10 Rectangle width
line_steps_rect_height : int, default=10 Height of rectangles
line_steps_space : int, default=5 Space between rectangles
line_steps_chart_width : int, default=700 Total width of the chart
line_steps_chart_height : int, default=100 Total chart height
line_steps_color : str, default=‘#80C11E’ Colour of rectangles
line_steps_opacity : float, default=0.2 Opacity of rectangles
line_steps_text_colour : str, default=‘black’ Text colour
line_steps_font_family : str, optional Font specific to line steps
line_steps_font_size : int, optional Font size for line steps
Parameters for Stair Steps
stair_steps_rect_width : int, default=10 Width of steps
stair_steps_rect_height : int, default=3 Height of steps
stair_steps_chart_width : int, default=700 Total width of the chart
stair_steps_chart_height : int, default=300 Total height of the chart
stair_steps_color : str, default=‘#80C11E’ Colour of the steps
stair_steps_opacity : float, default=0.2 Opacity of the steps
stair_steps_text_colour : str, default=‘black’ Text colour
stair_steps_font_family : str, Font specific to stair steps
stair_steps_font_size : int, optional Font size for stair steps
Title parameters
title_color : str, default=‘black’ Title colour
title_font_family : str, optional Specific font for the title
title_font_size : int, optional Font size for the title
Returns
self : Story object The current instance for method chaining
537defadd_source(self,text,position='bottom',vertical=False,color=None,dx=None,dy=None,font_size=None):538"""539 Add a source layer to the story.540541 Parameters:542 - text: The source text543 - position: The position of the text (default: 'bottom')544 - vertical: If True, the text will be rotated 90 degrees (default: False)545 - color: Custom text color (optional)546 - dx: Horizontal movement of the text (optional)547 - dy: Vertical movement of the text (optional)548 - font_size: Custom font size (optional)549550 Ritorna:551 - self, to allow the method chaining552 """553self.story_layers.append({554'type':'source',555'text':text,556'position':position,557'vertical':vertical,558'color':colororself.colors['source'],559'dx':dxor0,560'dy':dyor0,561'font_size':font_sizeorself.em_to_px(self.font_sizes['source'])562563})564returnself
Add a source layer to the story.
Parameters:
text: The source text
position: The position of the text (default: 'bottom')
vertical: If True, the text will be rotated 90 degrees (default: False)
color: Custom text color (optional)
dx: Horizontal movement of the text (optional)
dy: Vertical movement of the text (optional)
font_size: Custom font size (optional)
Ritorna:
self, to allow the method chaining
defadd_annotation(self,x_point,y_point,annotation_text='Point of interest',arrow_direction='right',arrow_color='blue',arrow_size=40,label_color='black',label_size=12,show_point=True,point_color='red',point_size=60,arrow_dx=0,arrow_dy=-45,label_dx=37,label_dy=-37):
566defadd_annotation(self,x_point,y_point,annotation_text="Point of interest",567arrow_direction='right',arrow_color='blue',arrow_size=40,568label_color='black',label_size=12,569show_point=True,point_color='red',point_size=60,570arrow_dx=0,arrow_dy=-45,571label_dx=37,label_dy=-37):572"""573 Create an arrow annotation on the graph.574575 This method is essential for highlighting specific points in the graph and adding576 contextual explanations. It is particularly useful for data narration, allowing577 to guide the observer's attention to relevant aspects of the visualisation.578579 Parameters:580 - x_point, y_point: Coordinates of the point to be annotated581 - annotation_text: Text of the annotation (default: ‘Point of interest’)582 - arrow_direction: Direction of the arrow (default: ‘right’)583 - arrow_color, arrow_size: Arrow colour and size584 - label_colour, label_size: Colour and size of the annotation text585 - show_point: If True, shows a point at the annotation location586 - point_color, point_size: Colour and size of the point587 - arrow_dx, arrow_dy: Distances in pixels to be added to the arrow position (default:0, -45)588 - label_dx, label_dy: Distances in pixels to be added to the label position (default:37, -37)589590 returns:591 - self, to allow method chaining592 """593594# Dictionary that maps arrow directions to corresponding Unicode symbols595arrow_symbols={596'left':'←','right':'→','up':'↑','down':'↓',597'upleft':'↖','upright':'↗','downleft':'↙','downright':'↘',598'leftup':'↰','leftdown':'↲','rightup':'↱','rightdown':'↳',599'upleftcurve':'↺','uprightcurve':'↻'600}601602# Check that the direction of the arrow is valid603ifarrow_directionnotinarrow_symbols:604raiseValueError(f"Invalid arrow direction. Use one of: {', '.join(arrow_symbols.keys())}")605606# Select the appropriate arrow symbol607arrow_symbol=arrow_symbols[arrow_direction]608609# checks whether the encoding has already been defined610ifnothasattr(self.chart,'encoding')ornothasattr(self.chart.encoding,'x'):611# If encoding is not defined use of default values612x_type='Q'613y_type='Q'614else:615# If encoding is defined, we extract the data types for the x and y axes616x_type=self.chart.encoding.x.shorthand.split(':')[-1]617y_type=self.chart.encoding.y.shorthand.split(':')[-1]618619# Explanation of data type extraction:620# 1. We first check whether the encoding has been defined. This is important because621# the user may call this method before defining the encoding of the graph.622# (The encoding in Altair defines how the data is mapped to the visual properties of the graph).623# 2. If encoding is not defined, we use ‘Q’ (Quantitative) as the default data type624# for both axes. This allows the method to work even without a defined encoding.625# 3. If the encoding is defined, we proceed with the data type extraction as before:626# - self.chart.encoding.x.shorthand: accesses the shorthand of the x-axis627# .shorthand: In Altair, ‘shorthand’ is a concise notation for specifying the encoding.628# Example of shorthand: ‘column:Q’ where ‘column’ is the name of the data column629# and ‘Q’ is the data type (in this case, Quantitative).630# - split(‘:’)[-1]: splits the shorthand at ‘:’ and takes the last element (the data type)631# Example: If shorthand is ‘price:Q’, split(‘:’) will return [‘price’, ‘Q’].632# 4. The extracted data type will be one of:633# - ‘Q’: Quantitative (continuous numeric)634# - ‘O’: Ordinal (ordered categories)635# N': Nominal (unordered categories)636# T': Temporal (dates or times)637638# Important: This operation is essential to ensure that the annotations639# added to the graph are consistent with the original axis data types.640# Without this match, annotations may be positioned641# incorrectly or cause rendering errors.642643# Create a DataFrame with a single point for the annotation644annotation_data=pd.DataFrame({'x':[x_point],'y':[y_point]})645646# Internal function to create the correct encoding based on the data type647defcreate_encoding(field,dtype):648"""649 Creates the appropriate encoding for Altair based on the data type.650 This function is crucial to ensure that the annotation is651 consistent with the original chart data type.652 """653654ifdtype=='O':# Ordinal655returnalt.X(field+':O',title='')iffield=='x'elsealt.Y(field+':O',title='')656elifdtype=='N':# Nominal657returnalt.X(field+':N',title='')iffield=='x'elsealt.Y(field+':N',title='')658elifdtype=='T':# Temporal659returnalt.X(field+':T',title='')iffield=='x'elsealt.Y(field+':T',title='')660else:# Quantity (default)661returnalt.X(field+':Q',title='')iffield=='x'elsealt.Y(field+':Q',title='')662663# Initialises the list of annotation layers664layers=[]665666# Adds full stop if required667ifshow_point:668point_layer=alt.Chart(annotation_data).mark_point(669color=point_color,670size=point_size671).encode(672x=create_encoding('x',x_type),673y=create_encoding('y',y_type)674)675layers.append(point_layer)676677# Adds the arrow678# We use mark_text to draw the arrow using a Unicode character679arrow_layer=alt.Chart(annotation_data).mark_text(680text=arrow_symbol,681fontSize=arrow_size,682dx=arrow_dx,# Horizontal offset to position the arrow was 22683dy=arrow_dy,# Vertical offset to position the arrow was -22684color=arrow_color685).encode(686x=create_encoding('x',x_type),687y=create_encoding('y',y_type)688)689layers.append(arrow_layer)690691# Adds text label692label_layer=alt.Chart(annotation_data).mark_text(693align='left',694baseline='top',695dx=label_dx,# Horizontal offset to position text was 37696dy=label_dy,# Vertical offset to position text was -37697fontSize=label_size,698color=label_color,699text=annotation_text700).encode(701x=create_encoding('x',x_type),702y=create_encoding('y',y_type)703)704layers.append(label_layer)705706# Combine all layers into a single annotation707annotation=alt.layer(*layers)708709# Adds annotation to history layers710self.story_layers.append({711'type':'annotation',712'chart':annotation713})714715# Note on flexibility and robustness:716# This approach makes the add_annotation method more flexible,717# as it can automatically adapt to different types of graphs without718# requiring manual input on the axis data type. In addition, using719# the Altair shorthand, the code automatically adapts even if the user720# has specified the encoding in different ways (e.g., using alt.X(‘column:Q’)721# or alt.X(‘column’, type=‘quantitative’)).722723724returnself# Return self to allow method chaining
Create an arrow annotation on the graph.
This method is essential for highlighting specific points in the graph and adding
contextual explanations. It is particularly useful for data narration, allowing
to guide the observer's attention to relevant aspects of the visualisation.
Parameters:
x_point, y_point: Coordinates of the point to be annotated
annotation_text: Text of the annotation (default: ‘Point of interest’)
arrow_direction: Direction of the arrow (default: ‘right’)
arrow_color, arrow_size: Arrow colour and size
label_colour, label_size: Colour and size of the annotation text
show_point: If True, shows a point at the annotation location
point_color, point_size: Colour and size of the point
arrow_dx, arrow_dy: Distances in pixels to be added to the arrow position (default:0, -45)
label_dx, label_dy: Distances in pixels to be added to the label position (default:37, -37)
727defadd_line(self,value,orientation='horizontal',color='red',stroke_width=2,stroke_dash=[]):728"""729 Adds a reference line to the story.730731 Parameters:732 - value: The position of the line (x or y value)733 - orientation: 'horizontal' or 'vertical' (default: 'horizontal')734 - color: Color of the line (default: 'red')735 - stroke_width: Width of the line in pixels (default: 2)736 - stroke_dash: Pattern for dashed line, e.g. [5,5] (default: [] for solid line)737738 Returns:739 - self, to allow method chaining740 """741742iforientationnotin['horizontal','vertical']:743raiseValueError("orientation must be either 'horizontal' or 'vertical'")744745# Crea un DataFrame con un singolo valore746iforientation=='horizontal':747data=pd.DataFrame({'y':[value]})748encoding={'y':'y:Q'}749else:# vertical750data=pd.DataFrame({'x':[value]})751encoding={'x':'x:Q'}752753# definizione dei parametri della line754mark_params={755'color':color,756'strokeWidth':stroke_width,757}758759# Viene aggiunto stokeDash solo se è specificato un pattern760ifstroke_dash:761mark_params['strokeDash']=stroke_dash762763# Crea la linea764line=alt.Chart(data).mark_rule(765color=color,766strokeWidth=stroke_width,767strokeDash=stroke_dash768).encode(769**encoding770)771772# Aggiunge la linea ai layer della storia773self.story_layers.append({774'type':'line',775'chart':line776})777778returnself
Adds a reference line to the story.
Parameters:
value: The position of the line (x or y value)
orientation: 'horizontal' or 'vertical' (default: 'horizontal')
color: Color of the line (default: 'red')
stroke_width: Width of the line in pixels (default: 2)
stroke_dash: Pattern for dashed line, e.g. [5,5] (default: [] for solid line)
Returns:
self, to allow method chaining
defem_to_px(self, em):
782defem_to_px(self,em):783"""784 Converts a dimension from em to pixels.785786 This function is essential for maintaining visual consistency787 between different textual elements and devices.788789 Parameters:790 - em: Size in em791792 Returns:793 - Equivalent size in pixels (integer)794 """795returnint(em*self.base_font_size)
Converts a dimension from em to pixels.
This function is essential for maintaining visual consistency
between different textual elements and devices.
Parameters:
em: Size in em
Returns:
Equivalent size in pixels (integer)
defcreate_title_layer(self, layer):
826defcreate_title_layer(self,layer):827"""828 Creates the title layer (and subtitle if present).829830 This method is responsible for visually creating the main title831 and the optional subtitle of the graphic.832833 Parameters:834 - Layer: Dictionary containing the title information835836 Returns:837 - Altair Chart object representing the title layer838 """839title_chart=alt.Chart(self.chart.data).mark_text(840text=layer['title'],841fontSize=layer['title_font_size'],842fontWeight='bold',843align='center',844font=self.font,845color=layer['title_color']846).encode(847x=alt.value(self.chart.width/2+layer.get('dx',0)),# Centre horizontally848y=alt.value(-50+layer.get('dy',0))# Position 20 pixels from above849)850851iflayer['subtitle']:852subtitle_chart=alt.Chart(self.chart.data).mark_text(853text=layer['subtitle'],854fontSize=layer['subtitle_font_size'],855align='center',856font=self.font,857color=layer['subtitle_color']858).encode(859x=alt.value(self.chart.width/2+layer.get('s_dx',0)),# Centre horizontally860y=alt.value(-20+layer.get('s_dy',0))# Position 50 pixels from the top (below the title)861)862returntitle_chart+subtitle_chart863returntitle_chart
Creates the title layer (and subtitle if present).
This method is responsible for visually creating the main title
and the optional subtitle of the graphic.
Parameters:
Layer: Dictionary containing the title information
Returns:
Altair Chart object representing the title layer
defcreate_text_layer(self, layer):
865defcreate_text_layer(self,layer):866"""867 Creates a generic text layer (context, next-steps, source).868869 This method is used to create text layers for various narrative purposes,870 such as adding context, next-steps or data source information.871872 Parameters:873 - Layer: Dictionary containing the text information to be added874875 Returns:876 - Altair Chart object representing the text layer877 """878x,y=self._get_position(layer['position'])879880# Apply offsets if they exist (for context layers)881iflayer['type']in['context','source']:882x+=layer.get('dx',0)883y+=layer.get('dy',0)884885returnalt.Chart(self.chart.data).mark_text(886text=layer['text'],887fontSize=layer.get('font_size',self.em_to_px(self.font_sizes[layer['type']])),888align='center',889baseline='middle',890font=self.font,891angle=270iflayer.get('vertical',False)else0,# Rotate text if ‘vertical’ is True892color=layer['color']893).encode(894x=alt.value(x),895y=alt.value(y)896)
Creates a generic text layer (context, next-steps, source).
This method is used to create text layers for various narrative purposes,
such as adding context, next-steps or data source information.
Parameters:
Layer: Dictionary containing the text information to be added
Returns:
Altair Chart object representing the text layer
defconfigure_view(self, *args, **kwargs):
898defconfigure_view(self,*args,**kwargs):899"""900 Configure aspects of the graph view using Altair's configure_view method.901902 This method allows you to configure various aspects of the chart view, such as903 the background colour, border style, internal spacing, etc.904905 Parameters:906 *args, **kwargs: Arguments to pass to the Altair configure_view method.907908 stores the view configuration for application during rendering.909 """910self.config['view']=kwargs911returnself
Configure aspects of the graph view using Altair's configure_view method.
This method allows you to configure various aspects of the chart view, such as
the background colour, border style, internal spacing, etc.
Parameters:
args, *kwargs: Arguments to pass to the Altair configure_view method.
stores the view configuration for application during rendering.
defrender(self):
914defrender(self):915"""916 It renders all layers of the story in a single graphic. 917 """918# Let's start with the basic graph919main_chart=self.chart920921# Create separate lists to place special graphics922top_charts=[]923bottom_charts=[]924left_charts=[]925right_charts=[]926overlay_charts=[]927928# Organise the layers according to their position929forlayerinself.story_layers:930iflayer['type']=='special_cta':931# We take the position from the layer932iflayer.get('position')=='top':933top_charts.append(layer['chart'])934eliflayer.get('position')=='bottom':935bottom_charts.append(layer['chart'])936eliflayer.get('position')=='left':937left_charts.append(layer['chart'])938eliflayer.get('position')=='right':939right_charts.append(layer['chart'])940eliflayer['type']=='title':941overlay_charts.append(self.create_title_layer(layer))942eliflayer['type']in['context','cta','source']:943overlay_charts.append(self.create_text_layer(layer))944eliflayer['type']in['shape','shape_label','annotation']:945overlay_charts.append(layer['chart'])946eliflayer['type']=='line':947overlay_charts.append(layer['chart'])948949# Overlaying the layers on the main graph950foroverlayinoverlay_charts:951main_chart+=overlay952953# Build the final layout954ifleft_charts:955main_chart=alt.hconcat(alt.vconcat(*left_charts),main_chart)956ifright_charts:957main_chart=alt.hconcat(main_chart,alt.vconcat(*right_charts))958iftop_charts:959main_chart=alt.vconcat(alt.hconcat(*top_charts),main_chart)960ifbottom_charts:961main_chart=alt.vconcat(main_chart,alt.hconcat(*bottom_charts))962963# Apply configurations964if'view'inself.config:965main_chart=main_chart.configure_view(**self.config['view'])966967returnmain_chart.resolve_axis(x='independent',y='independent')
It renders all layers of the story in a single graphic.
defstory(data=None, **kwargs):
973defstory(data=None,**kwargs):974"""975 Utility function for creating a Story instance.976977 This function simplifies the creation of a Story object, allowing978 to initialise it in a more concise and intuitive way.979980 Parameters:981 - data: DataFrame or URL for chart data (default: None)982 - **kwargs: Additional parameters to be passed to the Story constructor983984 Returns:985 - An instance of the Story class986 """987returnStory(data,**kwargs)
Utility function for creating a Story instance.
This function simplifies the creation of a Story object, allowing
to initialise it in a more concise and intuitive way.
Parameters:
data: DataFrame or URL for chart data (default: None)
**kwargs: Additional parameters to be passed to the Story constructor