text:
Research article 21 Jul 2021
Research article | 21 Jul 2021
Ice nucleation on surrogates of boreal forest SOA particles: effect of water content and oxidative age
Ice nucleation on surrogates of boreal forest SOA particles: effect of water content and oxidative age Ice nucleation on surrogates of boreal forest SOA particles: effect of water content and... Ana A. Piedehierro et al.
Ana A. Piedehierro1, André Welti1, Angela Buchholz2, Kimmo Korhonen2, Iida Pullinen2, Ilkka Summanen2, Annele Virtanen2, and Ari Laaksonen1,2 Ana A. Piedehierro et al. Ana A. Piedehierro1, André Welti1, Angela Buchholz2, Kimmo Korhonen2, Iida Pullinen2, Ilkka Summanen2, Annele Virtanen2, and Ari Laaksonen1,2
1Finnish Meteorological Institute, Helsinki, Finland
2Department of Applied Physics, University of Eastern Finland, Kuopio, Finland
Correspondence: Ana A. Piedehierro (ana.alvarez.piedehierro@fmi.fi)
Received: 07 Jan 2021 – Discussion started: 18 Feb 2021 – Revised: 28 May 2021 – Accepted: 28 May 2021 – Published: 21 Jul 2021
We investigate the effect of water content and oxidative age on ice nucleation using 100 nm monodisperse particles of boreal forest secondary organic aerosol (SOA) surrogates. Ice nucleation experiments are conducted in the temperature range between 210 and 240 K and from ice to water saturation using the Spectrometer for Ice Nuclei (SPIN). The effect of the particle water content on the ice nucleation process is tested by preconditioning α-pinene SOA at different humidities (40 %, 10 % and <1 % RHW). The influence of the particle oxidative age is tested by varying their O:C ratio (oxygen-to-carbon ratio, O:C ∼0.45, 0.8, 1.1). To assess the suitability of α-pinene as a model compound to study the ice nucleation properties of boreal forest SOA and to confirm the atmospheric relevance of our findings, we compare them to measurements of SOA using pine-needle oil or Scots pine tree emissions as precursors.
The ice nucleation measurements show that surrogates of boreal forest SOA particles promote only homogeneous ice formation. An effect of preconditioning humidity on homogeneous ice nucleation could be observed. Contrary to the expected behavior, homogeneous freezing is suppressed for SOA particles with high water content (preconditioned at 40 % RHW) and was only observed for SOA preconditioned at low RHW (≤10 %). No dependence of homogeneous freezing on the SOA oxidative age was observed. The results can be explained by a significant change of particulate water diffusivity as a function of humidity (from 10 % to 40 % RHW) at 293 K, where the aerosol is preconditioned. The measurements suggest that at low temperatures, water diffusion into dry SOA particles is slow enough to form a core-shell morphology. The liquid outer layer can equilibrate within the timescale of the experiment and freeze homogeneously. On SOA particles with higher water content, water diffuses faster into the particle, delaying equilibration at the particle surface and preventing the formation of a diluted shell, which can delay homogeneous freezing. We propose that the partial water vapor pressure to which the particles are exposed prior to an experiment can serve as an indicator of whether a core-shell structure is developing.
Piedehierro, A. A., Welti, A., Buchholz, A., Korhonen, K., Pullinen, I., Summanen, I., Virtanen, A., and Laaksonen, A.: Ice nucleation on surrogates of boreal forest SOA particles: effect of water content and oxidative age, Atmos. Chem. Phys., 21, 11069–11078, https://doi.org/10.5194/acp-21-11069-2021, 2021.
1 Introduction
Heterogeneous ice formation by ice-nucleating particles (INPs) allows for the formation of cirrus clouds at lower humidity than required for ice formation by homogeneous freezing of solution droplets, which is determined by the water activity criterion (Koop et al., 2000). Typical cirrus cloud INPs are water-insoluble particles such as mineral dusts, fly ash, metallic particles (DeMott et al., 2003; Cziczo et al., 2013) or soot (Bond et al., 2013). Prompted by the realization that secondary organic aerosol (SOA) particles can exist in a highly viscous, (semi-)solid state (Zobrist et al., 2008; Virtanen et al., 2010), the possibility that SOA particles could act as INPs has been investigated in recent years (Möhler et al., 2008; Prenni et al., 2009; Wang et al., 2012; Ladino et al., 2014; Schill et al., 2014; Ignatius et al., 2016; Charnawskas et al., 2017; Wagner et al., 2017; Bertozzi et al., 2021). SOA acting as INPs affect precipitation formation, cloud cover and the cloud albedo. A better understanding of ice nucleation on SOA is of special relevance for predicting cloud properties in areas with boreal forest, where biogenic SOA can form in high concentrations. SOA particles are composed of oxidation products of volatile organic compounds (VOCs), some of which are water-soluble. In contrast to low-viscosity (more liquid-like) particles, highly viscous aerosols are slow to take up or lose water or other vapors in response to variations in gas-phase composition (Mikhailov et al., 2009; Koop et al., 2011; Shiraiwa et al., 2013; Yli-Juuti et al., 2017). This resistance is particularly pronounced under dry conditions or low temperatures, where SOA particles can exist in a highly viscous or glassy state (Koop et al., 2011; Virtanen et al., 2010; Zobrist et al., 2008).
Upon updraft-driven humidification in the atmosphere, (semi-)solid amorphous organic aerosol could act as INPs in several ice nucleation mechanisms (Knopf et al., 2018). SOA particles that remain glassy during humidification could trigger ice formation via deposition nucleation (Murray et al., 2010). Upon humidification beyond the amorphous deliquescence relative humidity, water diffusion might be slow enough to form core-shell morphologies in particles with a highly viscous glassy matrix, allowing for ice formation via immersion freezing (Berkemeier et al., 2014; Lienhard et al., 2015). This situation could take place during fast updrafts (>3 m s−1) in which the equilibration time with the surrounding humidity is limited (Lienhard et al., 2015; Price et al., 2015). Typical equilibration times for 100 nm α-pinene SOA particles range from hours at 220 K to minutes at 230 K (Price et al., 2015). Continued humidification of the organic particles leads to complete liquefaction and homogeneous freezing as the only possible ice formation mechanism (Koop et al., 2011).
Previous studies have found that the ice nucleation (IN) ability of SOA particles varies between species: naphthalene-derived (Wang et al., 2012), methylglyoxal with methylamine (Schill et al., 2014) and isoprene-derived SOA particles (Wolf et al., 2020) were classified as effective INPs, exhibiting ice formation onsets at humidities clearly below homogeneous freezing conditions. α-pinene SOA was indicated to be ineffective at nucleating ice heterogeneously (e.g., Charnawskas et al., 2017, and references therein), while Ignatius et al. (2016) found α-pinene SOA particles to be effective INPs in the deposition mode. Although the rapid cooling of the aerosol during most experiments could result in the development of a core-shell morphology, none of the above studies observed ice formation by immersion freezing.
Using a cloud particle model capable of simulating water diffusion in individual aerosol particles, Fowler et al. (2020) have recently shown that at temperatures between 200 and 220 K a water layer condenses on α-pinene SOA particles, which can freeze homogeneously. In accordance with laboratory studies showing only homogeneous ice formation of α-pinene SOA particles, the same model indicates that at higher temperatures, water diffuses into particles, and freezing only takes place at humidities high enough to enable the freezing of solution droplets. Mie resonance measurements in levitating, highly viscous aerosol particles show steep gradients in water content between the core and outer layer (Bastelberger et al., 2018) supporting the interpretation that viscous SOA can adopt a core-shell structure, where the outer layer can freeze homogeneously.
We present experimental results of ice formation of atmospherically relevant surrogates of SOA particles, generated from biogenic emissions characteristic of boreal environments. To test the effect of water content and core-shell formation, α-pinene SOA particles with different diffusivity are generated by varying the oxidative aging and exposure of particles to different levels of humidity prior to cooling. The atmospheric relevance of the ice nucleation results from the α-pinene SOA is confirmed by comparing them to experiments with SOA particles formed from natural, boreal forest VOCs.
2 Methods
The SOA Ice Nucleation Experiment (SINE) campaign was carried out at the University of Eastern Finland Aerosol Physics Laboratory in June–July 2019. The experiments focused on studying the IN efficiency of SOA formed from boreal forest emissions and the influence of particle water content and oxidation level on IN activity.
Figure 1Experimental setup during SINE. The setup consists of five stages: SOA generation, size selection, SOA characterization, RH conditioning and ice nucleation measurement. Two methods of SOA generation were used: (a) homogeneous nucleation inside the PAM reactor and (b) seeded nucleation inside the ASC. For the PAM experiments, three configurations of the RH conditioning stage were used before measuring the IN activity of the SOA particles: (i) 40 %, (ii) 10 % or (iii) <1 % RHW.
2.1 General experimental setup
A schematic of the experimental setup is depicted in Fig. 1. SOA was generated either in an atmospheric simulation chamber (ASC; Faiola et al., 2019; batch chamber) or a potential aerosol mass reactor (PAM; Aerodyne Research Inc.; Kang et al., 2007; Lambe et al., 2011; oxidative flow reactor). The details of the two SOA formation procedures are described in Sect. 2.2.1 and 2.2.2.
Table 1Range of SOA formation and preconditioning parameters for each PAM reactor and ASC experiment. The VOC concentrations [VOC] reported for PAM experiments correspond to average concentrations while for ASC refer to starting concentrations. The variability of parameters during individual experiments was RHW±1 %, [VOC]±7.0 ppb and O:C ratio ±0.03.
∗ Concentration of VOCs detectable with the PTR-MS.
Download Print Version | Download XLSX
The SOA precursor concentrations (VOCs, Table 1) were monitored by a high-resolution time-of-flight proton transfer reaction mass spectrometer (PTR-MS; Ionicon model 8000). The size distribution and the oxidative age (O:C ratio, Table 1) of the SOA particles were monitored using a scanning mobility particle sizer (SMPS; TSI, Inc. model DMA 3082, CPC 3775) and a high-resolution time-of-flight aerosol mass spectrometer (HR-AMS; Aerodyne Research Inc., DeCarlo et al., 2006), respectively. For the ice nucleation measurements with a spectrometer for ice nuclei (SPIN; Droplet Measurement Technologies), the aerosol was size-selected using a differential mobility analyzer (DMA; TSI, model 3082) and preconditioned at 40 %, 10 % or <1 % RHW.
2.2 Aerosol generation
2.2.1 α-pinene SOA with different O:C generated with the PAM reactor
We used a PAM reactor to form α-pinene SOA particles of different oxidative ages, characterized by their O:C ratio. A syringe pump (Nexus 3000, Chemyx Inc.) was used to create a constant injection of α-pinene into a nitrogen carrier gas flow heated to 60 ∘C, which was introduced into the PAM reactor. In the PAM reactor, particles are formed by (a) dry ozonolysis, (b) wet ozonolysis or (c) wet photooxidation with OH radicals. The O3 concentration and, in the photooxidation experiments, the irradiation level were adjusted to create SOA particles with a specific O:C ratio (see Table 1 for settings). Low-O:C SOA (O:C ∼0.45) was produced in wet and dry ozonolysis experiments (no. 1–no. 7). The photooxidation experiments (no. 8–no. 12) generated medium- (O:C ∼0.9) and high-O:C (O:C ∼1.1) SOA. For dry ozonolysis, dry nitrogen and oxygen were used as carrier gases, resulting in RHW of <2 % in the sample flow exiting the PAM reactor. For wet ozonolysis and photooxidation experiments, the nitrogen flow was humidified to create RHW between 40 % and 50 % inside the reactor. The OH radicals in the photooxidation experiments were created from the photolysis of O3 at 254 nm and consecutive reaction of the formed O(1D) with water vapor. Typical O3 concentration was 6.5–7 ppm at the inlet of PAM. The average OH concentration inside the PAM reactor was 5–10 ×108 molec.cm-3 (estimated from OH exposure; Peng et al., 2015). The composition of the SOA particles formed in the PAM reactor was held constant over the course of the 4–5 h needed for the IN measurements. Between two experiments, the PAM reactor and all connected tubes were flushed with nitrogen or purified air (custom-built system, dry compressed air followed by particle, active charcoal and potassium permanganate filters) for several hours. Before a new experiment started, the PAM reactor ran for at least 30 min at similar O3 concentrations and irradiation as during the experiment but without VOCs to ensure low levels of background SOA (<1 µg m−3).
2.2.2 Boreal forest surrogate SOA from ASC
A second set of experiments was conducted utilizing the 12 m3 collapsible PTFE ASC to form SOA under atmospheric conditions (see Fig. 1). Aqueous ammonium sulfate particles were generated with an atomizer (Aerosol Generator Model 3076, TSI), subsequently dried below their efflorescence point in a silica gel dryer (50 cm long, outlet RHW<15 %) and injected into the ASC to reach seed concentrations of 5.0–7.5 ×104 cm−3 at the start of VOC ozonolysis. For experiments no. 13 and no. 15, α-pinene or pine needle oil were injected with a microliter syringe directly into a carrier gas flow. The injection times were adjusted to reach the desired 5–50 ppb in the ASC. The emissions from six 10-year-old pine saplings had been collected onto stainless steel multi-bed adsorbent cartridges containing Tenax TA and Carbograph adsorbent (Markes International, Inc.) during the springtime of 2019 (similar procedure as described by Faiola et al., 2019). For experiment no. 14, four of these tubes (equivalent to 30 h of emissions) were introduced into the ASC by flushing the cartridges with nitrogen for 20 min, while they were heated to 473 K. Mixing in the ASC was achieved by an additional flow of purified compressed air (>50 L min−1). O3 was added to the chamber from a custom-built ozone generator, immediately afterwards, to start the ozonolysis reactions.
The precursor concentration in the ASC was monitored with the PTR-MS. The particle size and composition were monitored continuously with the SMPS and the HR-AMS. The IN measurements started after the maximum in the SOA mass concentration was reached, ensuring a constant particle composition during the IN measurements. Between experiments, the ASC was flushed for several hours with purified compressed air. Additionally, chemical cleaning was conducted by filling the ASC with high concentrations of O3 (>500 ppb) for 30–60 min. These chemical cleaning intervals were followed by at least 10 h of flushing.
2.3 Ice nucleation measurements
The ice nucleation ability of 100 nm SOA particles was measured with the SPIN instrument (Garimella et al., 2016). SPIN is a parallel-plate continuous flow diffusion chamber (CFDC). The specific instrument (SPIN5) used in this study has been modified to perform low-temperature experiments (Welti et al., 2020). Inside the SPIN chamber, particles are exposed to RH and T conditions (temperature range 210–240 K and from ice to water saturation) relevant for ice formation in cirrus clouds. Ice formation during a residence time of 10 s is detected with an optical particle counter (OPC; details in Garimella et al., 2016). The ratio of ice-forming particles, measured with the OPC, and the number of particles introduced into SPIN, measured with a condensation particle counter (CPC; Airmodus A20), is reported as the activated fraction (AF). The activated fraction is measured along RH scans at several temperatures. The evaporation section at the end of the chamber guarantees that droplets are not counted as ice with the OPC. In this study, the size-selected SOA particles were exposed to different humidity conditions before entering the SPIN, to probe the effects of particle water content on IN activity. The samples were introduced (a) directly into the SPIN, (b) through a silica gel diffusion dryer or (c) sequentially through a silica gel diffusion dryer and a liquid nitrogen cold trap (see Fig. 1). The preconditioning led to sample RHW at the SPIN inlet (Vaisala HMP110 humidity sensor) of 40 %, 10 % or <1 %, respectively. Later, we refer to these precondition settings as wet, dry and super-dry.
Figure 2AF measurements for α-pinene O:C ∼0.45, 0.9 and 1.1 (rows) and 40 %, 10 % and <1 % RHW (columns). The color code indicates the activated fraction. The dashed light blue line marks water saturation. Conditions of homogeneous freezing (AF=1 %) of α-pinene SOA particles are shown by dotted orange lines. Homogeneous freezing conditions were calculated following Appendix B in Welti et al. (2020) and using the growth factor for α-pinene SOA given in Varutbangkul et al. (2006).
3 Results and discussion
3.1 Effect of O:C ratio and water content on ice formation conditions
The effects of oxidative age (approximated by the O:C ratio) and water content of α-pinene SOA particles on ice formation were investigated. Note that the water content of the particles was preconditioned after generation and before measuring their ice nucleation ability with the SPIN. The AF-spectra of α-pinene SOA particles generated with different O:C and preconditioned at different humidities are shown in Fig. 2.
Figure 3Relative humidity of ice formation on 1 % of 100 nm α-pinene SOA particles as a function of O:C ratio (circles) in comparison to ammonium sulfate (AS; shown by crosses; data from Welti et al., 2020). Open circles indicate experiments with α-pinene particles preconditioned at RHW<1 % and filled circles RHW=10 %. Trend lines are shown for experiments at 213, 223 and 233 K. Temperature is indicated by the color scale.
The O:C ratio appears to have a minor impact on the conditions of ice formation of the α-pinene SOA particles. Schill and Tolbert (2012) reported that the O:C ratio of organic acids affects heterogeneous ice nucleation and suggested that this could also be the case for α-pinene SOA. Aged particles (higher O:C ratio) would nucleate ice at lower humidity due to higher surface hydrophilicity, allowing for the adsorption of an ice-like layer from which the ice phase can develop. The decrease in ice formation onset humidity was strongest for 0.3<O:C<0.5 (Schill and Tolbert, 2012). In the range of O:C ratios covered here (0.45–1.1), no pronounced change in ice formation humidity was observed. Figure 3 summarizes RH conditions where AF=1 % was reached in dependence of O:C ratios. The humidity needed to reach 1 % AF is close to humidities at which ammonium sulfate solution droplets freeze homogeneously (e.g., reported in Welti et al., 2020). This suggests that homogeneous freezing dominated the ice formation of SOA particles with O:C ratios between 0.45 and 1.1, regardless of the oxidative age itself.
Figure 4Suggested freezing mechanism for SOA particles with different water content: (a) dry SOA and (b) wet SOA.
A strong dependence of ice formation on SOA water content was observed. Dry and super-dry SOA particles (RHW=10 % and <1 %, middle and right column in Fig. 2) form ice at humidities close to the homogeneous freezing line of solutes, the so-called Koop line (Koop et al., 2000), suggesting homogeneous freezing. Wet preconditioning of the SOA (at 40 % RHW, left column in Fig. 2) almost completely suppressed the formation of ice up to water saturation. SOA particles preconditioned at higher RHW contain more water, which acts as a plasticizer, and they are therefore less viscous. It was expected that the higher water content of the particles would facilitate the diffusion of condensed water molecules, enabling the homogeneous freezing of SOA solution droplets. To the best of our knowledge, a retardation of homogeneous freezing of SOA particles with higher water content has not been observed experimentally and only recently been suggested by Fowler et al. (2020), based on simulations. Figure 4 illustrates the mechanism of how water content could impact homogeneous freezing of SOA particles during SPIN measurements.
When dry SOA particles (Fig. 4a) take up water at low temperature inside SPIN, water diffusion towards the particle core is slow enough to enable the formation of a liquid layer in equilibrium with SPIN conditions. This outer liquid layer in equilibrium with the surrounding RH freezes at the homogeneous freezing conditions (Koop et al., 2000). This mechanism is similar to the one suggested by Fowler et al. (2020).
For SOA particles with higher water content (Fig. 4b), water diffuses faster into the particle, removing water from the surface. Assuming a comparable amount of water taken up by the wet- and dry-conditioned SOA during their 10 s residence time in SPIN, wet-conditioned particles can not reach equilibrium, resulting in a more concentrated state, too concentrated to freeze homogeneously. Longer residence times could restore the homogeneous freezing of the particles by the equilibration with the surrounding RH. However, as equilibration time scales can be on the order of minutes and longer (Price et al., 2015), we believe that the observed inhibition of homogeneous freezing could also happen in fast updrafts in the atmosphere.
Figure 5Comparison of SOA pre-treatment conditions to SOA phase transition (viscosity 4×105 to 6×106 Pa s, red envelope) from Petters et al. (2019). We note that the phase transition coincides with a partial water vapor pressure (shown in blue) of 3–5 hPa. Homogeneous ice nucleation was only observed for SOA particles exposed to low water vapor pressure during preconditioning.
Comparing the preconditioning humidities to the phase diagram of RH-induced glass transition as a function of temperature for α-pinene SOA in Fig. 5 (reproduced from Petters et al., 2019) suggests that SOA particles were in a liquid-like state before entering the SPIN instrument for the wet preconditioning (cross in Fig. 5, indicating suppression of ice formation). Dry preconditioned particles, for which homogeneous ice formation was observed, were likely glassy (blue hexagons in Fig. 5). The hygroscopic growth factors of α-pinene SOA at 10 % and 40 % RHW are 1.00008 and 1.00718 (Varutbangkul et al., 2006). Assuming an average molecular weight of 200 g mol−1 and a density of 1.5 g cm−3 for the SOA particles, the corresponding water contents are 0.17 % and 14 % mole fractions, respectively. This indicates that at 40 % RHW, SOA particles can take up a substantial amount of water molecules. Evaporation studies on α-pinene SOA (Li et al., 2019) also indicate that at room temperature and 40 % RHW, α-pinene SOA particles present negligible kinetic limitations, evaporating in a nearly liquid-like state, while reduced RHW conditions (<1 % RHW) lead to slower evaporation rates, due to the limited bulk diffusivity of the particles. According to Fig. 5, once the SOA particles enter the SPIN instrument, the particles solidify abruptly with the decreasing temperature but seem to retain a preconditioning humidity-dependent water diffusion rate, governing the equilibration timescale.
We propose that a partial water vapor pressure of 3–5 hPa can be used as marker condition to distinguish water diffusion rates of α-pinene particles. Slower water diffusion into particles kept below 3–5 hPa, leading to faster equilibration and homogeneous ice formation, while particles crossing this condition show faster diffusion and require longer equilibration times. To better understand the size-dependent equilibration timescales of the wet or (super-)dry preconditioned particles and consequently the time dependence of homogeneous ice formation on SOA particles, additional experiments are needed. It would be beneficial to investigate in detail the correlation between the homogeneous ice nucleation ability of SOA and water content by extending the T and RH conditions at which SOA are preconditioned to cover the threshold conditions shown in Fig. 5, including different particle sizes.
Figure 6Activated fraction of 100 nm SOA particles for (a) pine-needle oil, (b) pine tree and (c) α-pinene precursors. The dashed light blue line marks water saturation. Conditions where 1 % soluble particles freeze homogeneously according to the parameterization of Koop et al. (2000) are shown by dotted orange lines.
3.2 α-pinene as proxy for boreal forest emissions
After isoprene, monoterpenes are the second most emitted volatiles by plants (Sindelarova et al., 2014). Among monoterpenes, α-pinene is the most abundant VOCs emitted from the boreal forest (Wang et al., 2018). We studied the ice nucleation conditions of SOA particles nucleated from Scots pine tree emissions and compared them to two precursor proxies: pine-needle oil (Sigma Aldrich) and α-pinene. The SOA particles were generated in an ASC (Fig. 1) and their IN ability tested between 210 and 240 K and from ice to water saturation (Fig. 6). This is the first study measuring the IN ability of pure SOA from real pine emissions representative of boreal forest environments in laboratory-controlled conditions.
The particles nucleated ice only at humidities close to the Koop line between approx. 215 and 235 K, showing a steep onset when humidity increased during the RH scans. Compared to the Koop parametrization, ice formation on Scots pine tree (Fig. 6b) and α-pinene (Fig. 6c) SOA is retarded to higher supersaturations in the lowest range of temperatures, similar to dry-conditioned α-pinene SOA generated in the PAM reactor. In summary, the three tested precursors showed similar ice nucleation conditions, typical for homogeneous freezing, indicating that α-pinene is an appropriate proxy for IN by SOA from boreal forests emissions.
Figure 7Conditions of ice formation on 1 % of 100 nm α-pinene SOA particles. Circles and stars represent the results from the PAM and ASC experiments, respectively. Open and filled symbols mark RHW=1 % and 10 % preconditioning, respectively. α-pinene SOA ice nucleation data from previous studies are shown for comparison: [1] Möhler et al. (2008), [2] Ladino et al. (2014), [3] Ignatius et al. (2016), [4] Wagner et al. (2017) and [5] Charnawskas et al. (2017). The dashed light blue line marks water saturation. Conditions where 1 % soluble particles freeze homogeneously according to the parameterization of Koop et al. (2000) are shown by the dotted orange line.
3.3 Comparison to previous measurements of α-pinene SOA ice formation conditions
The 1 % AF values extracted from the RH scans with dry and super-dry α-pinene SOA are summarized in Fig. 7, together with data from the literature.
Conditions of 1 % AF ice formation on α-pinene SOA, measured in this study, agree well with previous measurements reported by, e.g., Ladino et al. (2014), Wagner et al. (2017) and Charnawskas et al. (2017). In these studies, SOA particles were generated at T and RH conditions comparable to the experiments for dry preconditioned SOA presented here. These studies concluded that α-pinene SOA particles are poor ice-nucleating particles for heterogeneous ice formation or that they exclusively nucleate ice homogeneously. However, the data deviate from the Koop line. Humidity conditions of ice formation decrease monotonically with increasing temperature, approximately parallel to the water saturation line, i.e., showing earlier onsets for the warmest temperatures and delayed onset at the cold temperatures. Ignatius et al. (2016) argued that ice forming below the Koop line should be classified as heterogeneous ice formation. However, homogeneous ice formation below the Koop line was also observed during reference measurements using ammonium sulfate solution droplets with the SPIN instrument (Welti et al., 2020). A steep increase in the AF at a certain humidity during RH scans is an additional indication that the driving ice formation mechanism was homogeneous freezing, despite the lower humidities for ice nucleation in comparison to the Koop parametrization.
Delayed onsets of homogeneous freezing above the humidity defined by the water activity criterion (Koop et al., 2000) at low temperatures could indicate that particles are not in equilibrium before freezing in SPIN. A similar offset for homogeneous freezing of sulfuric acid particles at low temperature has been observed in the AIDA chamber (Möhler et al., 2003), which was also attributed to diffusion-limited uptake of water vapor during cooling. The systematic delay of homogeneous freezing towards higher humidity suggests a decreasing, low water diffusion rate of SOA towards low temperature. Fowler et al. (2020) pointed out the same aspect and proposed even an inhibition of homogeneous ice nucleation below 200 K. Experimental evidence of homogeneous freezing inhibition at such low temperatures had previously been reported by Murray (2008) for citric acid solution droplets.
4 Conclusions
We have investigated the influence of particle water content and oxidative age on the ice nucleation ability of boreal forest SOA surrogates, using α-pinene SOA generated in a PAM reactor as a model compound. The suitability of α-pinene SOA as a proxy for the IN ability of more complex boreal forest surrogates was addressed by comparing IN measurements of SOA from two other precursors (pine-needle oil and Scots pine tree VOCs) generated in an ASC under more atmospherically relevant conditions. For the first time, the IN ability of pure lab-generated SOA from real pine emission has been measured.
Boreal forest SOA surrogates from all precursors were found to be inefficient ice-nucleating particles, in agreement with previous studies (e.g., Möhler et al., 2008; Ladino et al., 2014; Charnawskas et al., 2017). Our observations indicate that the IN ability of α-pinene SOA can be considered representative of more complex SOA produced in monoterpene-dominated precursor mixes from boreal environments.
Homogeneous ice nucleation was observed for α-pinene SOA preconditioned at low RHW (≤10 %), and contrary to the expected behavior, homogeneous freezing was suppressed for SOA with higher water content (40 % RHW). The onset humidity for homogeneous freezing did not depend on the oxidative age. The experiments indicate that SOA water content was the main variable controlling the onset humidity for homogeneous freezing at a certain temperature, suggesting a dependence on the water diffusion rate.
α-pinene SOA with low water content, presumably in a highly viscous state, did not act as INPs, but a decreased water diffusion rate into the particle allowed for the formation of a core-shell morphology, enabling the homogeneous freezing of the diluted outer layer. In contrast, SOA with higher water content, into which water diffused more efficiently during the residence time in SPIN, completely liquefied or developed a core-shell morphology in which the liquid phase was highly concentrated, inhibiting homogeneous freezing. The coincidence of SOA phase transition conditions with partial water vapor pressures between 3–5 hPa also points to the potential dominant role of water content in particle properties like viscosity or diffusivity that affected the SOA ice nucleation behavior as measured by SPIN.
Further investigations should include SOA generation under a wider range of RH and T conditions and measurements of initial viscosity for a clearer connection between the ice nucleation measurements and the freezing pathways leading to these observations.
Data sets are available from the authors upon request.
AV and AL proposed the initial research question. AAP and AW conducted the ice nucleation experiments with contributions from KK. AB, IP and IS were in charge of particle formation, characterization and the corresponding data analysis. AAP and AW prepared the manuscript with contributions from AB, AL and AV. All authors commented the manuscript. AV and AL acquired funding and supervised the project.
Publisher's note: Copernicus Publications remains neutral with regard to jurisdictional claims in published maps and institutional affiliations.
We thank Zijun Li (Rex) and Pasi Miettinen for their support during the campaign. The authors acknowledge the support from the Academy of Finland.
This research has been supported by the Academy of Finland (grant nos. 309141, 309142 and 307331).
Review statement
This paper was edited by Hinrich Grothe and reviewed by three anonymous referees.
Bastelberger, S., Krieger, U. K., Luo, B. P., and Peter, T.: Time evolution of steep diffusion fronts in highly viscous aerosol particles measured with Mie resonance spectroscopy, J. Chem. Phys., 149, 244506, https://doi.org/10.1063/1.5052216, 2018. a
Berkemeier, T., Shiraiwa, M., Pöschl, U., and Koop, T.: Competition between water uptake and ice nucleation by glassy organic aerosol particles, Atmos. Chem. Phys., 14, 12513–12531, https://doi.org/10.5194/acp-14-12513-2014, 2014. a
Bertozzi, B., Wagner, R., Höhler, K., Pfeifer, J., Saathoff, H., Song, J., Leisner, T., and Möhler, O.: Ice nucleation ability of Ammonium Sulfate aerosol particles internally mixed with Secondary Organics, Atmos. Chem. Phys. Discuss. [preprint], https://doi.org/10.5194/acp-2021-53, in review, 2021. a
Bond, T. C., Doherty, S. J., Fahey, D. W., Forster, P. M., Berntsen, T., DeAngelo, B. J., Flanner, M. G., Ghan, S., Kärcher, B., Koch, D., Kinne, S., Kondo, Y., Quinn, P. K., Sarofim, M. C., Schultz, M. G., Schulz, M., Venkataraman, C., Zhang, H., Zhang, S., Bellouin, N., Guttikunda, S. K., Hopke, P. K., Jacobson, M. Z., Kaiser, J. W., Klimont, Z., Lohmann, U., Schwarz, J. P., Shindell, D., Storelvmo, T., Warren, S. G., and Zender, C. S.: Bounding the role of black carbon in the climate system: A scientific assessment, J. Geophys. Res.-Atmos., 118, 5380–5552, https://doi.org/10.1002/jgrd.50171, 2013. a
Charnawskas, J. C., Alpert, P. A., Lambe, A. T., Berkemeier, T., O'Brien, R. E., Massoli, P., Onasch, T. B., Shiraiwa, M., Moffet, R. C., Gilles, M. K., Davidovits, P., Worsnop, D. R., and Knopf, D. A.: Condensed-phase biogenic–anthropogenic interactions with implications for cold cloud formation, Faraday Discuss., 200, 165–194, https://doi.org/10.1039/C7FD00010C, 2017. a, b, c, d, e
Cziczo, D. J., Froyd, K. D., Hoose, C., Jensen, E. J., Diao, M., Zondlo, M. A., Smith, J. B., Twohy, C. H., and Murphy, D. M.: Clarifying the Dominant Sources and Mechanisms of Cirrus Cloud Formation, Science, 340, 1320–1324, https://doi.org/10.1126/science.1234145, 2013. a
DeCarlo, P. F., Kimmel, J. R., Trimborn, A., Northway, M. J., Jayne, J. T., Aiken, A. C., Gonin, M., Fuhrer, K., Horvath, T., Docherty, K. S., Worsnop, D. R., and Jimenez, J. L.: Field-Deployable, High-Resolution, Time-of-Flight Aerosol Mass Spectrometer, Anal. Chem., 78, 8281–8289, https://doi.org/10.1021/ac061249n, 2006. a
DeMott, P. J., Cziczo, D. J., Prenni, A. J., Murphy, D. M., Kreidenweis, S. M., Thomson, D. S., Borys, R., and Rogers, D. C.: Measurements of the concentration and composition of nuclei for cirrus formation, P. Natl. Acad. Sci. USA, 100, 14655–14660, https://doi.org/10.1073/pnas.2532677100, 2003. a
Faiola, C. L., Pullinen, I., Buchholz, A., Khalaj, F., Ylisirniö, A., Kari, E., Miettinen, P., Holopainen, J. K., Kivimäenpää, M., Schobesberger, S., Yli-Juuti, T., and Virtanen, A.: Secondary Organic Aerosol Formation from Healthy and Aphid-Stressed Scots Pine Emissions, ACS Earth Space Chem., 3, 1756–1772, https://doi.org/10.1021/acsearthspacechem.9b00118, 2019. a, b
Fowler, K., Connolly, P., and Topping, D.: Modelling the effect of condensed-phase diffusion on the homogeneous nucleation of ice in ultra-viscous particles, Atmos. Chem. Phys., 20, 683–698, https://doi.org/10.5194/acp-20-683-2020, 2020. a, b, c, d
Garimella, S., Kristensen, T. B., Ignatius, K., Welti, A., Voigtländer, J., Kulkarni, G. R., Sagan, F., Kok, G. L., Dorsey, J., Nichman, L., Rothenberg, D. A., Rösch, M., Kirchgäßner, A. C. R., Ladkin, R., Wex, H., Wilson, T. W., Ladino, L. A., Abbatt, J. P. D., Stetzer, O., Lohmann, U., Stratmann, F., and Cziczo, D. J.: The SPectrometer for Ice Nuclei (SPIN): an instrument to investigate ice nucleation, Atmos. Meas. Tech., 9, 2781–2795, https://doi.org/10.5194/amt-9-2781-2016, 2016. a, b
Ignatius, K., Kristensen, T. B., Järvinen, E., Nichman, L., Fuchs, C., Gordon, H., Herenz, P., Hoyle, C. R., Duplissy, J., Garimella, S., Dias, A., Frege, C., Höppel, N., Tröstl, J., Wagner, R., Yan, C., Amorim, A., Baltensperger, U., Curtius, J., Donahue, N. M., Gallagher, M. W., Kirkby, J., Kulmala, M., Möhler, O., Saathoff, H., Schnaiter, M., Tomé, A., Virtanen, A., Worsnop, D., and Stratmann, F.: Heterogeneous ice nucleation of viscous secondary organic aerosol produced from ozonolysis of α-pinene, Atmos. Chem. Phys., 16, 6495–6509, https://doi.org/10.5194/acp-16-6495-2016, 2016. a, b, c, d
Kang, E., Root, M. J., Toohey, D. W., and Brune, W. H.: Introducing the concept of Potential Aerosol Mass (PAM), Atmos. Chem. Phys., 7, 5727–5744, https://doi.org/10.5194/acp-7-5727-2007, 2007. a
Knopf, D. A., Alpert, P. A., and Wang, B.: The Role of Organic Aerosol in Atmospheric Ice Nucleation: A Review, ACS Earth Space Chem., 2, 168–202, https://doi.org/10.1021/acsearthspacechem.7b00120, 2018. a
Koop, T., Luo, B., Tsias, A., and Peter, T.: Water activity as the determinant for homogeneous ice nucleation in aqueous solutions, Nature, 406, 611–614, https://doi.org/10.1038/35020537, 2000. a, b, c, d, e, f
Koop, T., Bookhold, J., Shiraiwa, M., and Pöschl, U.: Glass transition and phase state of organic compounds: dependency on molecular properties and implications for secondary organic aerosols in the atmosphere, Phys. Chem. Chem. Phys., 13, 19238, https://doi.org/10.1039/c1cp22617g, 2011. a, b, c
Ladino, L. A., Zhou, S., Yakobi-Hancock, J. D., Aljawhary, D., and Abbatt, J. P. D.: Factors controlling the ice nucleating abilities of α-pinene SOA particles, J. Geophys. Res.-Atmos., 119, 9041–9051, https://doi.org/10.1002/2014JD021578, 2014. a, b, c, d
Lambe, A. T., Ahern, A. T., Williams, L. R., Slowik, J. G., Wong, J. P. S., Abbatt, J. P. D., Brune, W. H., Ng, N. L., Wright, J. P., Croasdale, D. R., Worsnop, D. R., Davidovits, P., and Onasch, T. B.: Characterization of aerosol photooxidation flow reactors: heterogeneous oxidation, secondary organic aerosol formation and cloud condensation nuclei activity measurements, Atmos. Meas. Tech., 4, 445–461, https://doi.org/10.5194/amt-4-445-2011, 2011. a
Li, Z., Tikkanen, O. P., Buchholz, A., Hao, L., Kari, E., Yli-Juuti, T., and Virtanen, A.: Effect of Decreased Temperature on the Evaporation of α-Pinene Secondary Organic Aerosol Particles, ACS Earth Space Chem., 3, 2775–2785, https://doi.org/10.1021/acsearthspacechem.9b00240, 2019. a
Lienhard, D. M., Huisman, A. J., Krieger, U. K., Rudich, Y., Marcolli, C., Luo, B. P., Bones, D. L., Reid, J. P., Lambe, A. T., Canagaratna, M. R., Davidovits, P., Onasch, T. B., Worsnop, D. R., Steimer, S. S., Koop, T., and Peter, T.: Viscous organic aerosol particles in the upper troposphere: diffusivity-controlled water uptake and ice nucleation?, Atmos. Chem. Phys., 15, 13599–13613, https://doi.org/10.5194/acp-15-13599-2015, 2015. a, b
Mikhailov, E., Vlasenko, S., Martin, S. T., Koop, T., and Pöschl, U.: Amorphous and crystalline aerosol particles interacting with water vapor: conceptual framework and experimental evidence for restructuring, phase transitions and kinetic limitations, Atmos. Chem. Phys., 9, 9491–9522, https://doi.org/10.5194/acp-9-9491-2009, 2009. a
Möhler, O., Stetzer, O., Schaefers, S., Linke, C., Schnaiter, M., Tiede, R., Saathoff, H., Krämer, M., Mangold, A., Budz, P., Zink, P., Schreiner, J., Mauersberger, K., Haag, W., Kärcher, B., and Schurath, U.: Experimental investigation of homogeneous freezing of sulphuric acid particles in the aerosol chamber AIDA, Atmos. Chem. Phys., 3, 211–223, https://doi.org/10.5194/acp-3-211-2003, 2003. a
Möhler, O., Benz, S., Saathoff, H., Schnaiter, M., Wagner, R., Schneider, J., Walter, S., Ebert, V., and Wagner, S.: The effect of organic coating on the heterogeneous ice nucleation efficiency of mineral dust aerosols, Environ. Res. Lett., 3, 025007, https://doi.org/10.1088/1748-9326/3/2/025007, 2008. a, b, c
Murray, B. J.: Inhibition of ice crystallisation in highly viscous aqueous organic acid droplets, Atmos. Chem. Phys., 8, 5423–5433, https://doi.org/10.5194/acp-8-5423-2008, 2008. a
Murray, B. J., Wilson, T. W., Dobbie, S., Cui, Z., Al-Jumur, S. M. R. K., Möhler, O., Schnaiter, M., Wagner, R., Benz, S., Niemand, M., Saathoff, H., Ebert, V., Wagner, S., and Kärcher, B.: Heterogeneous nucleation of ice particles on glassy aerosols under cirrus conditions, Nat. Geosci., 3, 233–237, https://doi.org/10.1038/ngeo817, 2010. a
Peng, Z., Day, D. A., Stark, H., Li, R., Lee-Taylor, J., Palm, B. B., Brune, W. H., and Jimenez, J. L.: HOx radical chemistry in oxidation flow reactors with low-pressure mercury lamps systematically examined by modeling, Atmos. Meas. Tech., 8, 4863–4890, https://doi.org/10.5194/amt-8-4863-2015, 2015. a
Petters, S. S., Kreidenweis, S. M., Grieshop, A. P., Ziemann, P. J., and Petters, M. D.: Temperature- and Humidity-Dependent Phase States of Secondary Organic Aerosols, Geophys. Res. Lett., 46, 1005–1013, https://doi.org/10.1029/2018GL080563, 2019. a, b
Prenni, A. J., Petters, M. D., Faulhaber, A., Carrico, C. M., Ziemann, P. J., Kreidenweis, S. M., and DeMott, P. J.: Heterogeneous ice nucleation measurements of secondary organic aerosol generated from ozonolysis of alkenes, Geophys. Res. Lett., 36, L06808, https://doi.org/10.1029/2008GL036957, 2009. a
Price, H. C., Mattsson, J., Zhang, Y., Bertram, A. K., Davies, J. F., Grayson, J. W., Martin, S. T., O'Sullivan, D., Reid, J. P., Rickards, A. M. J., and Murray, B. J.: Water diffusion in atmospherically relevant α-pinene secondary organic material, Chem. Sci., 6, 4876–4883, https://doi.org/10.1039/C5SC00685F, 2015. a, b, c
Schill, G. P. and Tolbert, M. A.: Depositional ice nucleation on monocarboxylic acids: Effect of the O:C ratio, J. Phys. Chem. A, 116, 6817–6822, https://doi.org/10.1021/jp301772q, 2012. a, b
Schill, G. P., De Haan, D. O., and Tolbert, M. A.: Heterogeneous Ice Nucleation on Simulated Secondary Organic Aerosol, Environ. Sci. Technol., 48, 1675–1682, https://doi.org/10.1021/es4046428, 2014. a, b
Shiraiwa, M., Yee, L. D., Schilling, K. A., Loza, C. L., Craven, J. S., Zuend, A., Ziemann, P. J., and Seinfeld, J. H.: Size distribution dynamics reveal particle-phase chemistry in organic aerosol formation, P. Natl. Acad. Sci. USA, 110, 11746–11750, https://doi.org/10.1073/pnas.1307501110, 2013. a
Sindelarova, K., Granier, C., Bouarar, I., Guenther, A., Tilmes, S., Stavrakou, T., Müller, J.-F., Kuhn, U., Stefani, P., and Knorr, W.: Global data set of biogenic VOC emissions calculated by the MEGAN model over the last 30 years, Atmos. Chem. Phys., 14, 9317–9341, https://doi.org/10.5194/acp-14-9317-2014, 2014. a
Varutbangkul, V., Brechtel, F. J., Bahreini, R., Ng, N. L., Keywood, M. D., Kroll, J. H., Flagan, R. C., Seinfeld, J. H., Lee, A., and Goldstein, A. H.: Hygroscopicity of secondary organic aerosols formed by oxidation of cycloalkenes, monoterpenes, sesquiterpenes, and related compounds, Atmos. Chem. Phys., 6, 2367–2388, https://doi.org/10.5194/acp-6-2367-2006, 2006. a, b
Virtanen, A., Joutsensaari, J., Koop, T., Kannosto, J., Yli-Pirilä, P., Leskinen, J., Mäkelä, J. M., Holopainen, J. K., Pöschl, U., Kulmala, M., Worsnop, D. R., and Laaksonen, A.: An amorphous solid state of biogenic secondary organic aerosol particles, Nature, 467, 824–827, https://doi.org/10.1038/nature09455, 2010. a, b
Wagner, R., Höhler, K., Huang, W., Kiselev, A., Möhler, O., Mohr, C., Pajunoja, A., Saathoff, H., Schiebel, T., Shen, X., and Virtanen, A.: Heterogeneous ice nucleation of α-pinene SOA particles before and after ice cloud processing, J. Geophys. Res.-Atmos., 122, 4924–4943, https://doi.org/10.1002/2016JD026401, 2017. a, b, c
Wang, B., Lambe, A. T., Massoli, P., Onasch, T. B., Davidovits, P., Worsnop, D. R., and Knopf, D. A.: The deposition ice nucleation and immersion freezing potential of amorphous secondary organic aerosol: Pathways for ice and mixed-phase cloud formation, J. Geophys. Res.-Atmos., 117, D16209, https://doi.org/10.1029/2012JD018063, 2012. a, b
Wang, M., Schurgers, G., Hellén, H., Lagergren, F., and Holst, T.: Biogenic volatile organic compound emissions from a boreal forest floor, Boreal Environ. Res., 23, 249–265, available at: http://www.borenv.net/BER/archive/pdfs/ber23/ber23-249-265.pdf (last access: 1 July 2021), 2018. a
Welti, A., Korhonen, K., Miettinen, P., Piedehierro, A. A., Viisanen, Y., Virtanen, A., and Laaksonen, A.: SPIN modification for low-temperature experiments, Atmos. Meas. Tech., 13, 7059–7067, https://doi.org/10.5194/amt-13-7059-2020, 2020. a, b, c, d, e
Wolf, M. J., Zhang, Y., Zawadowicz, M. A., Goodell, M., Froyd, K., Freney, E., Sellegri, K., Rösch, M., Cui, T., Winter, M., Lacher, L., Axisa, D., DeMott, P. J., Levin, E. J., Gute, E., Abbatt, J., Koss, A., Kroll, J. H., Surratt, J. D., and Cziczo, D. J.: A biogenic secondary organic aerosol source of cirrus ice nucleating particles, Nat. Commun., 11, 4834, https://doi.org/10.1038/s41467-020-18424-6, 2020. a
Yli-Juuti, T., Pajunoja, A., Tikkanen, O., Buchholz, A., Faiola, C., Väisänen, O., Hao, L., Kari, E., Peräkylä, O., Garmash, O., Shiraiwa, M., Ehn, M., Lehtinen, K., and Virtanen, A.: Factors controlling the evaporation of secondary organic aerosol from α-pinene ozonolysis, Geophys. Res. Lett., 44, 2562–2570, https://doi.org/10.1002/2016GL072364, 2017. a
Zobrist, B., Marcolli, C., Pedernera, D. A., and Koop, T.: Do atmospheric aerosols form glasses?, Atmos. Chem. Phys., 8, 5221–5244, https://doi.org/10.5194/acp-8-5221-2008, 2008. a, b
Ice crystals in cirrus clouds contain particles that start ice formation. We study whether particles forming above boreal forests can help in the making of cirrus clouds and if the water content in the particles affects this property. In the laboratory, we made boreal-forest-like particles and cooled and humidified them to measure whether an ice crystal develops. We found that only when dry can these particles form an ice crystal but no better than solution droplets.
Ice crystals in cirrus clouds contain particles that start ice formation. We study whether...


text:
/**
 * Accessibility module - Keyboard navigation
 *
 * (c) 2010-2017 Highsoft AS
 * Author: Oystein Moseng
 *
 * License: www.highcharts.com/license
 */

'use strict';

import H from '../parts/Globals.js';
import '../parts/Utilities.js';
import '../parts/Chart.js';
import '../parts/Series.js';
import '../parts/Point.js';
import '../parts/Tooltip.js';
import '../parts/SvgRenderer.js';

var win = H.win,
    doc = win.document,
    addEvent = H.addEvent,
    fireEvent = H.fireEvent,
    merge = H.merge,
    pick = H.pick;

/*
 * Add focus border functionality to SVGElements. Draws a new rect on top of
 * element around its bounding box.
 */
H.extend(H.SVGElement.prototype, {

    /**
     * @private
     * @function Highcharts.SVGElement#addFocusBorder
     *
     * @param {number} margin
     *
     * @param {Higcharts.CSSObject} style
     */
    addFocusBorder: function (margin, style) {
        // Allow updating by just adding new border
        if (this.focusBorder) {
            this.removeFocusBorder();
        }
        // Add the border rect
        var bb = this.getBBox(),
            pad = pick(margin, 3);

        this.focusBorder = this.renderer.rect(
            bb.x - pad,
            bb.y - pad,
            bb.width + 2 * pad,
            bb.height + 2 * pad,
            style && style.borderRadius
        )
            .addClass('highcharts-focus-border')
            .attr({
                zIndex: 99
            })
            .add(this.parentGroup);

        if (!this.renderer.styledMode) {
            this.focusBorder.attr({
                stroke: style && style.stroke,
                'stroke-width': style && style.strokeWidth
            });
        }
    },

    /**
     * @private
     * @function Highcharts.SVGElement#removeFocusBorder
     */
    removeFocusBorder: function () {
        if (this.focusBorder) {
            this.focusBorder.destroy();
            delete this.focusBorder;
        }
    }
});

/*
 * Set for which series types it makes sense to move to the closest point with
 * up/down arrows, and which series types should just move to next series.
 */
H.Series.prototype.keyboardMoveVertical = true;
['column', 'pie'].forEach(function (type) {
    if (H.seriesTypes[type]) {
        H.seriesTypes[type].prototype.keyboardMoveVertical = false;
    }
});

/**
 * Strip HTML tags away from a string. Used for aria-label attributes, painting
 * on a canvas will fail if the text contains tags.
 *
 * @private
 * @function stripTags
 *
 * @param  {string} s
 *         The input string
 *
 * @return {string}
 *         The filtered string
 */
function stripTags(s) {
    return typeof s === 'string' ? s.replace(/<\/?[^>]+(>|$)/g, '') : s;
}

/**
 * Get the index of a point in a series. This is needed when using e.g. data
 * grouping.
 *
 * @private
 * @function getPointIndex
 *
 * @param {Highcharts.Point} point
 *        The point to find index of.
 *
 * @return {number}
 *         The index in the series.points array of the point.
 */
function getPointIndex(point) {
    var index = point.index,
        points = point.series.points,
        i = points.length;

    if (points[index] !== point) {
        while (i--) {
            if (points[i] === point) {
                return i;
            }
        }
    } else {
        return index;
    }
}

// Set default keyboard navigation options
H.setOptions({

    /**
     * @since        5.0.0
     * @optionparent accessibility
     */
    accessibility: {

        /**
         * Options for keyboard navigation.
         *
         * @since 5.0.0
         */
        keyboardNavigation: {

            /**
             * Enable keyboard navigation for the chart.
             *
             * @since 5.0.0
             */
            enabled: true,

            /**
             * Options for the focus border drawn around elements while
             * navigating through them.
             *
             * @sample highcharts/accessibility/custom-focus
             *         Custom focus ring
             *
             * @since 6.0.3
             */
            focusBorder: {

                /**
                 * Enable/disable focus border for chart.
                 *
                 * @since 6.0.3
                 */
                enabled: true,

                /**
                 * Hide the browser's default focus indicator.
                 *
                 * @since 6.0.4
                 */
                hideBrowserFocusOutline: true,

                /**
                 * Style options for the focus border drawn around elements
                 * while navigating through them. Note that some browsers in
                 * addition draw their own borders for focused elements. These
                 * automatic borders can not be styled by Highcharts.
                 *
                 * In styled mode, the border is given the
                 * `.highcharts-focus-border` class.
                 *
                 * @type    {Highcharts.CSSObject}
                 * @default {"color": "#335cad", "lineWidth": 2, "borderRadius": 3}
                 * @since   6.0.3
                 */
                style: {
                    /** @ignore-option */
                    color: '#335cad',
                    /** @ignore-option */
                    lineWidth: 2,
                    /** @ignore-option */
                    borderRadius: 3
                },

                /**
                 * Focus border margin around the elements.
                 *
                 * @since 6.0.3
                 */
                margin: 2

            },

            /**
             * Set the keyboard navigation mode for the chart. Can be "normal"
             * or "serialize". In normal mode, left/right arrow keys move
             * between points in a series, while up/down arrow keys move between
             * series. Up/down navigation acts intelligently to figure out which
             * series makes sense to move to from any given point.
             *
             * In "serialize" mode, points are instead navigated as a single
             * list. Left/right behaves as in "normal" mode. Up/down arrow keys
             * will behave like left/right. This is useful for unifying
             * navigation behavior with/without screen readers enabled.
             *
             * @type       {string}
             * @default    normal
             * @since      6.0.4
             * @validvalue ["normal", "serialize"]
             * @apioption  accessibility.keyboardNavigation.mode
             */

            /**
             * Skip null points when navigating through points with the
             * keyboard.
             *
             * @since 5.0.0
             */
            skipNullPoints: true

        }

    }

});

/**
 * Keyboard navigation for the legend. Requires the Accessibility module.
 *
 * @since     5.0.14
 * @apioption legend.keyboardNavigation
 */

/**
 * Enable/disable keyboard navigation for the legend. Requires the Accessibility
 * module.
 *
 * @see [accessibility.keyboardNavigation](
 *      #accessibility.keyboardNavigation.enabled)
 *
 * @type      {boolean}
 * @default   true
 * @since     5.0.13
 * @apioption legend.keyboardNavigation.enabled
 */

/**
 * Abstraction layer for keyboard navigation. Keep a map of keyCodes to handler
 * functions, and a next/prev move handler for tab order. The module's keyCode
 * handlers determine when to move to another module. Validate holds a function
 * to determine if there are prerequisites for this module to run that are not
 * met. Init holds a function to run once before any keyCodes are interpreted.
 * Terminate holds a function to run once before moving to next/prev module.
 *
 * @private
 * @class
 * @name KeyboardNavigationModule
 *
 * @param {Highcharts.Chart} chart
 *        The chart object keeps track of a list of KeyboardNavigationModules.
 *
 * @param {*} options
 */
function KeyboardNavigationModule(chart, options) {
    this.chart = chart;
    this.id = options.id;
    this.keyCodeMap = options.keyCodeMap;
    this.validate = options.validate;
    this.init = options.init;
    this.terminate = options.terminate;
}
KeyboardNavigationModule.prototype = {

    /**
     * Find handler function(s) for key code in the keyCodeMap and run it.
     *
     * @private
     * @function KeyboardNavigationModule#run
     *
     * @param {global.Event} e
     *
     * @return {boolean}
     */
    run: function (e) {
        var navModule = this,
            keyCode = e.which || e.keyCode,
            found = false,
            handled = false;

        this.keyCodeMap.forEach(function (codeSet) {
            if (codeSet[0].indexOf(keyCode) > -1) {
                found = true;
                handled = codeSet[1].call(navModule, keyCode, e) !== false;
            }
        });
        // Default tab handler, move to next/prev module
        if (!found && keyCode === 9) {
            handled = this.move(e.shiftKey ? -1 : 1);
        }
        return handled;
    },

    /**
     * Move to next/prev valid module, or undefined if none, and init it.
     * Returns true on success and false if there is no valid module to move to.
     *
     * @private
     * @function KeyboardNavigationModule#move
     *
     * @param {number} direction
     *
     * @return {boolean}
     */
    move: function (direction) {
        var chart = this.chart;

        if (this.terminate) {
            this.terminate(direction);
        }
        chart.keyboardNavigationModuleIndex += direction;
        var newModule = chart.keyboardNavigationModules[
            chart.keyboardNavigationModuleIndex
        ];

        // Remove existing focus border if any
        if (chart.focusElement) {
            chart.focusElement.removeFocusBorder();
        }

        // Verify new module
        if (newModule) {
            if (newModule.validate && !newModule.validate()) {
                return this.move(direction); // Invalid module, recurse
            }
            if (newModule.init) {
                newModule.init(direction); // Valid module, init it
                return true;
            }
        }
        // No module
        chart.keyboardNavigationModuleIndex = 0; // Reset counter

        // Set focus to chart or exit anchor depending on direction
        if (direction > 0) {
            this.chart.exiting = true;
            this.chart.tabExitAnchor.focus();
        } else {
            this.chart.renderTo.focus();
        }

        return false;
    }
};

/**
 * Utility function to attempt to fake a click event on an element.
 *
 * @private
 * @function fakeClickEvent
 *
 * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement}
 */
function fakeClickEvent(element) {
    var fakeEvent;

    if (element && element.onclick && doc.createEvent) {
        fakeEvent = doc.createEvent('Events');
        fakeEvent.initEvent('click', true, false);
        element.onclick(fakeEvent);
    }
}

/**
 * Determine if a series should be skipped
 *
 * @private
 * @function isSkipSeries
 *
 * @param {Highcharts.Series} series
 *
 * @return {boolean}
 */
function isSkipSeries(series) {
    var a11yOptions = series.chart.options.accessibility;

    return series.options.skipKeyboardNavigation ||
        series.options.enableMouseTracking === false || // #8440
        !series.visible ||
        // Skip all points in a series where pointDescriptionThreshold is
        // reached
        (a11yOptions.pointDescriptionThreshold &&
        a11yOptions.pointDescriptionThreshold <= series.points.length);
}

/**
 * Determine if a point should be skipped
 *
 * @private
 * @function isSkipPoint
 *
 * @param {Highcharts.Point} point
 *
 * @return {boolean}
 */
function isSkipPoint(point) {
    var a11yOptions = point.series.chart.options.accessibility;

    return point.isNull && a11yOptions.keyboardNavigation.skipNullPoints ||
        point.visible === false ||
        isSkipSeries(point.series);
}

/**
 * Get the point in a series that is closest (in distance) to a reference point.
 * Optionally supply weight factors for x and y directions.
 *
 * @private
 * @function getClosestPoint
 *
 * @param {Highcharts.Point} point
 *
 * @param {Highcharts.Series} series
 *
 * @param {number} [xWeight]
 *
 * @param {number} [yWeight]
 *
 * @return {Highcharts.Point|undefined}
 */
function getClosestPoint(point, series, xWeight, yWeight) {
    var minDistance = Infinity,
        dPoint,
        minIx,
        distance,
        i = series.points.length;

    if (point.plotX === undefined || point.plotY === undefined) {
        return;
    }
    while (i--) {
        dPoint = series.points[i];
        if (dPoint.plotX === undefined || dPoint.plotY === undefined) {
            continue;
        }
        distance = (point.plotX - dPoint.plotX) *
                (point.plotX - dPoint.plotX) * (xWeight || 1) +
                (point.plotY - dPoint.plotY) *
                (point.plotY - dPoint.plotY) * (yWeight || 1);
        if (distance < minDistance) {
            minDistance = distance;
            minIx = i;
        }
    }
    return minIx !== undefined && series.points[minIx];
}

/**
 * Pan along axis in a direction (1 or -1), optionally with a defined
 * granularity (number of steps it takes to walk across current view)
 *
 * @private
 * @function Highcharts.Axis#panStep
 *
 * @param {number} direction
 *
 * @param {number} [granularity]
 */
H.Axis.prototype.panStep = function (direction, granularity) {
    var gran = granularity || 3,
        extremes = this.getExtremes(),
        step = (extremes.max - extremes.min) / gran * direction,
        newMax = extremes.max + step,
        newMin = extremes.min + step,
        size = newMax - newMin;

    if (direction < 0 && newMin < extremes.dataMin) {
        newMin = extremes.dataMin;
        newMax = newMin + size;
    } else if (direction > 0 && newMax > extremes.dataMax) {
        newMax = extremes.dataMax;
        newMin = newMax - size;
    }
    this.setExtremes(newMin, newMax);
};

/**
 * Set chart's focus to an SVGElement. Calls focus() on it, and draws the focus
 * border.
 *
 * @private
 * @function Highcharts.Chart#setFocusToElement
 *
 * @param {Highcharts.SVGElement} svgElement
 *        Element to draw the border around.
 *
 * @param {Highcharts.SVGElement} [focusElement]
 *        If supplied, it draws the border around svgElement and sets the focus
 *        to focusElement.
 */
H.Chart.prototype.setFocusToElement = function (svgElement, focusElement) {
    var focusBorderOptions = this.options.accessibility
            .keyboardNavigation.focusBorder,
        browserFocusElement = focusElement || svgElement;

    // Set browser focus if possible
    if (
        browserFocusElement.element &&
        browserFocusElement.element.focus
    ) {
        browserFocusElement.element.focus();
        // Hide default focus ring
        if (focusBorderOptions.hideBrowserFocusOutline) {
            browserFocusElement.css({ outline: 'none' });
        }
    }
    if (focusBorderOptions.enabled) {
        // Remove old focus border
        if (this.focusElement) {
            this.focusElement.removeFocusBorder();
        }
        // Draw focus border (since some browsers don't do it automatically)
        svgElement.addFocusBorder(focusBorderOptions.margin, {
            stroke: focusBorderOptions.style.color,
            strokeWidth: focusBorderOptions.style.lineWidth,
            borderRadius: focusBorderOptions.style.borderRadius
        });
        this.focusElement = svgElement;
    }
};

/**
 * Highlights a point (show tooltip and display hover state).
 *
 * @private
 * @function Highcharts.Point#highlight
 *
 * @return {Highcharts.Point}
 *         This highlighted point.
 */
H.Point.prototype.highlight = function () {
    var chart = this.series.chart;

    if (!this.isNull) {
        this.onMouseOver(); // Show the hover marker and tooltip
    } else {
        if (chart.tooltip) {
            chart.tooltip.hide(0);
        }
        // Don't call blur on the element, as it messes up the chart div's focus
    }

    // We focus only after calling onMouseOver because the state change can
    // change z-index and mess up the element.
    if (this.graphic) {
        chart.setFocusToElement(this.graphic);
    }

    chart.highlightedPoint = this;
    return this;
};

/**
 * Function to highlight next/previous point in chart.
 *
 * @private
 * @function Highcharts.Chart#highlightAdjacentPoint
 *
 * @param {boolean} next
 *        Flag for the direction.
 *
 * @return {Highcharts.Point|false}
 *         Returns highlighted point on success, false on failure (no adjacent
 *         point to highlight in chosen direction).
 */
H.Chart.prototype.highlightAdjacentPoint = function (next) {
    var chart = this,
        series = chart.series,
        curPoint = chart.highlightedPoint,
        curPointIndex = curPoint && getPointIndex(curPoint) || 0,
        curPoints = curPoint && curPoint.series.points,
        lastSeries = chart.series && chart.series[chart.series.length - 1],
        lastPoint = lastSeries && lastSeries.points &&
                    lastSeries.points[lastSeries.points.length - 1],
        newSeries,
        newPoint;

    // If no points, return false
    if (!series[0] || !series[0].points) {
        return false;
    }

    if (!curPoint) {
        // No point is highlighted yet. Try first/last point depending on move
        // direction
        newPoint = next ? series[0].points[0] : lastPoint;
    } else {
        // We have a highlighted point.
        // Grab next/prev point & series
        newSeries = series[curPoint.series.index + (next ? 1 : -1)];
        newPoint = curPoints[curPointIndex + (next ? 1 : -1)];
        if (!newPoint && newSeries) {
            // Done with this series, try next one
            newPoint = newSeries.points[next ? 0 : newSeries.points.length - 1];
        }

        // If there is no adjacent point, we return false
        if (!newPoint) {
            return false;
        }
    }

    // Recursively skip points
    if (isSkipPoint(newPoint)) {
        // If we skip this whole series, move to the end of the series before we
        // recurse, just to optimize
        newSeries = newPoint.series;
        if (isSkipSeries(newSeries)) {
            chart.highlightedPoint = next ?
                newSeries.points[newSeries.points.length - 1] :
                newSeries.points[0];
        } else {
            // Otherwise, just move one point
            chart.highlightedPoint = newPoint;
        }
        // Retry
        return chart.highlightAdjacentPoint(next);
    }

    // There is an adjacent point, highlight it
    return newPoint.highlight();
};

/**
 * Highlight first valid point in a series. Returns the point if successfully
 * highlighted, otherwise false. If there is a highlighted point in the series,
 * use that as starting point.
 *
 * @private
 * @function Highcharts.Series#highlightFirstValidPoint
 *
 * @return {Highcharts.Point|false}
 */
H.Series.prototype.highlightFirstValidPoint = function () {
    var curPoint = this.chart.highlightedPoint,
        start = (curPoint && curPoint.series) === this ?
            getPointIndex(curPoint) :
            0,
        points = this.points;

    if (points) {
        for (var i = start, len = points.length; i < len; ++i) {
            if (!isSkipPoint(points[i])) {
                return points[i].highlight();
            }
        }
        for (var j = start; j >= 0; --j) {
            if (!isSkipPoint(points[j])) {
                return points[j].highlight();
            }
        }
    }
    return false;
};

/**
 * Highlight next/previous series in chart. Returns false if no adjacent series
 * in the direction, otherwise returns new highlighted point.
 *
 * @private
 * @function Highcharts.Chart#highlightAdjacentSeries
 *
 * @param {boolean} down
 *
 * @return {Highcharts.Point|false}
 */
H.Chart.prototype.highlightAdjacentSeries = function (down) {
    var chart = this,
        newSeries,
        newPoint,
        adjacentNewPoint,
        curPoint = chart.highlightedPoint,
        lastSeries = chart.series && chart.series[chart.series.length - 1],
        lastPoint = lastSeries && lastSeries.points &&
                    lastSeries.points[lastSeries.points.length - 1];

    // If no point is highlighted, highlight the first/last point
    if (!chart.highlightedPoint) {
        newSeries = down ? (chart.series && chart.series[0]) : lastSeries;
        newPoint = down ?
            (newSeries && newSeries.points && newSeries.points[0]) : lastPoint;
        return newPoint ? newPoint.highlight() : false;
    }

    newSeries = chart.series[curPoint.series.index + (down ? -1 : 1)];

    if (!newSeries) {
        return false;
    }

    // We have a new series in this direction, find the right point
    // Weigh xDistance as counting much higher than Y distance
    newPoint = getClosestPoint(curPoint, newSeries, 4);

    if (!newPoint) {
        return false;
    }

    // New series and point exists, but we might want to skip it
    if (isSkipSeries(newSeries)) {
        // Skip the series
        newPoint.highlight();
        adjacentNewPoint = chart.highlightAdjacentSeries(down); // Try recurse
        if (!adjacentNewPoint) {
            // Recurse failed
            curPoint.highlight();
            return false;
        }
        // Recurse succeeded
        return adjacentNewPoint;
    }

    // Highlight the new point or any first valid point back or forwards from it
    newPoint.highlight();
    return newPoint.series.highlightFirstValidPoint();
};

/**
 * Highlight the closest point vertically.
 *
 * @private
 * @function Highcharts.Chart#highlightAdjacentPointVertical
 *
 * @param {boolean} down
 *
 * @return {Highcharts.Point|false}
 */
H.Chart.prototype.highlightAdjacentPointVertical = function (down) {
    var curPoint = this.highlightedPoint,
        minDistance = Infinity,
        bestPoint;

    if (curPoint.plotX === undefined || curPoint.plotY === undefined) {
        return false;
    }
    this.series.forEach(function (series) {
        if (isSkipSeries(series)) {
            return;
        }
        series.points.forEach(function (point) {
            if (point.plotY === undefined || point.plotX === undefined ||
                point === curPoint) {
                return;
            }
            var yDistance = point.plotY - curPoint.plotY,
                width = Math.abs(point.plotX - curPoint.plotX),
                distance = Math.abs(yDistance) * Math.abs(yDistance) +
                    width * width * 4; // Weigh horizontal distance highly

            // Reverse distance number if axis is reversed
            if (series.yAxis.reversed) {
                yDistance *= -1;
            }

            if (
                yDistance < 0 && down || yDistance > 0 && !down || // Wrong dir
                distance < 5 || // Points in same spot => infinite loop
                isSkipPoint(point)
            ) {
                return;
            }

            if (distance < minDistance) {
                minDistance = distance;
                bestPoint = point;
            }
        });
    });

    return bestPoint ? bestPoint.highlight() : false;
};

/**
 * Show the export menu and focus the first item (if exists).
 *
 * @private
 * @function Highcharts.Chart#showExportMenu
 */
H.Chart.prototype.showExportMenu = function () {
    if (this.exportSVGElements && this.exportSVGElements[0]) {
        this.exportSVGElements[0].element.onclick();
        this.highlightExportItem(0);
    }
};

/**
 * Hide export menu.
 *
 * @private
 * @function Highcharts.Chart#hideExportMenu
 */
H.Chart.prototype.hideExportMenu = function () {
    var chart = this,
        exportList = chart.exportDivElements;

    if (exportList && chart.exportContextMenu) {
        // Reset hover states etc.
        exportList.forEach(function (el) {
            if (el.className === 'highcharts-menu-item' && el.onmouseout) {
                el.onmouseout();
            }
        });
        chart.highlightedExportItem = 0;
        // Hide the menu div
        chart.exportContextMenu.hideMenu();
        // Make sure the chart has focus and can capture keyboard events
        chart.container.focus();
    }
};

/**
 * Highlight export menu item by index.
 *
 * @private
 * @function Highcharts.Chart#highlightExportItem
 *
 * @param {number} ix
 *
 * @return {true|undefined}
 */
H.Chart.prototype.highlightExportItem = function (ix) {
    var listItem = this.exportDivElements && this.exportDivElements[ix],
        curHighlighted =
            this.exportDivElements &&
            this.exportDivElements[this.highlightedExportItem],
        hasSVGFocusSupport;

    if (
        listItem &&
        listItem.tagName === 'DIV' &&
        !(listItem.children && listItem.children.length)
    ) {
        // Test if we have focus support for SVG elements
        hasSVGFocusSupport = !!(
            this.renderTo.getElementsByTagName('g')[0] || {}
        ).focus;

        // Only focus if we can set focus back to the elements after
        // destroying the menu (#7422)
        if (listItem.focus && hasSVGFocusSupport) {
            listItem.focus();
        }
        if (curHighlighted && curHighlighted.onmouseout) {
            curHighlighted.onmouseout();
        }
        if (listItem.onmouseover) {
            listItem.onmouseover();
        }
        this.highlightedExportItem = ix;
        return true;
    }
};

/**
 * Try to highlight the last valid export menu item.
 *
 * @private
 * @function Highcharts.Chart#highlightLastExportItem
 */
H.Chart.prototype.highlightLastExportItem = function () {
    var chart = this,
        i;

    if (chart.exportDivElements) {
        i = chart.exportDivElements.length;
        while (i--) {
            if (chart.highlightExportItem(i)) {
                break;
            }
        }
    }
};

/**
 * Highlight range selector button by index.
 *
 * @private
 * @function Highcharts.Chart#highlightRangeSelectorButton
 *
 * @param {number} ix
 *
 * @return {boolean}
 */
H.Chart.prototype.highlightRangeSelectorButton = function (ix) {
    var buttons = this.rangeSelector.buttons;

    // Deselect old
    if (buttons[this.highlightedRangeSelectorItemIx]) {
        buttons[this.highlightedRangeSelectorItemIx].setState(
            this.oldRangeSelectorItemState || 0
        );
    }
    // Select new
    this.highlightedRangeSelectorItemIx = ix;
    if (buttons[ix]) {
        this.setFocusToElement(buttons[ix].box, buttons[ix]);
        this.oldRangeSelectorItemState = buttons[ix].state;
        buttons[ix].setState(2);
        return true;
    }
    return false;
};

/**
 * Highlight legend item by index.
 *
 * @private
 * @function Highcharts.Chart#highlightLegendItem
 *
 * @param {number} ix
 *
 * @return {boolean}
 */
H.Chart.prototype.highlightLegendItem = function (ix) {
    var items = this.legend.allItems,
        oldIx = this.highlightedLegendItemIx;

    if (items[ix]) {
        if (items[oldIx]) {
            fireEvent(
                items[oldIx].legendGroup.element,
                'mouseout'
            );
        }
        // Scroll if we have to
        if (items[ix].pageIx !== undefined &&
            items[ix].pageIx + 1 !== this.legend.currentPage) {
            this.legend.scroll(1 + items[ix].pageIx - this.legend.currentPage);
        }
        // Focus
        this.highlightedLegendItemIx = ix;
        this.setFocusToElement(items[ix].legendItem, items[ix].legendGroup);
        fireEvent(items[ix].legendGroup.element, 'mouseover');
        return true;
    }
    return false;
};

/**
 * Add keyboard navigation handling modules to chart.
 *
 * @private
 * @function Highcharts.Chart#addKeyboardNavigationModules
 */
H.Chart.prototype.addKeyboardNavigationModules = function () {
    var chart = this;

    /**
     * @private
     * @function navModuleFactory
     *
     * @param {string} id
     *
     * @param {Array<Array<number>,Function>} keyMap
     *
     * @param {Highcharts.Dictionary<Function>} options
     *
     * @return {KeyboardNavigationModule}
     */
    function navModuleFactory(id, keyMap, options) {
        return new KeyboardNavigationModule(chart, merge({
            keyCodeMap: keyMap
        }, { id: id }, options));
    }

    /**
     * List of the different keyboard handling modes we use depending on where
     * we are in the chart. Each mode has a set of handling functions mapped to
     * key codes. Each mode determines when to move to the next/prev mode.
     *
     * @private
     * @name Highcharts.Chart#keyboardNavigationModules
     * @type {Array<KeyboardNavigationModule>}
     */
    chart.keyboardNavigationModules = [

        // Entry point catching the first tab, allowing users to tab into points
        // more intuitively.
        navModuleFactory('entry', []),

        // Points
        navModuleFactory('points', [
            // Left/Right
            [[37, 39], function (keyCode) {
                var right = keyCode === 39;

                if (!chart.highlightAdjacentPoint(right)) {
                    // Failed to highlight next, wrap to last/first
                    return this.init(right ? 1 : -1);
                }
                return true;
            }],
            // Up/Down
            [[38, 40], function (keyCode) {
                var down = keyCode !== 38,
                    navOptions = chart.options.accessibility.keyboardNavigation;

                if (navOptions.mode && navOptions.mode === 'serialize') {
                    // Act like left/right
                    if (!chart.highlightAdjacentPoint(down)) {
                        return this.init(down ? 1 : -1);
                    }
                    return true;
                }
                // Normal mode, move between series
                var highlightMethod = chart.highlightedPoint &&
                        chart.highlightedPoint.series.keyboardMoveVertical ?
                    'highlightAdjacentPointVertical' :
                    'highlightAdjacentSeries';

                chart[highlightMethod](down);
                return true;
            }],
            // Enter/Spacebar
            [[13, 32], function () {
                if (chart.highlightedPoint) {
                    chart.highlightedPoint.firePointEvent('click');
                }
            }]
        ], {
            // Always start highlighting from scratch when entering this module
            init: function (dir) {
                var numSeries = chart.series.length,
                    i = dir > 0 ? 0 : numSeries,
                    res;

                if (dir > 0) {
                    delete chart.highlightedPoint;
                    // Find first valid point to highlight
                    while (i < numSeries) {
                        res = chart.series[i].highlightFirstValidPoint();
                        if (res) {
                            return res;
                        }
                        ++i;
                    }
                } else {
                    // Find last valid point to highlight
                    while (i--) {
                        chart.highlightedPoint = chart.series[i].points[
                            chart.series[i].points.length - 1
                        ];
                        // Highlight first valid point in the series will also
                        // look backwards. It always starts from currently
                        // highlighted point.
                        res = chart.series[i].highlightFirstValidPoint();
                        if (res) {
                            return res;
                        }
                    }
                }
            },
            // If leaving points, don't show tooltip anymore
            terminate: function () {
                if (chart.tooltip) {
                    chart.tooltip.hide(0);
                }
                delete chart.highlightedPoint;
            }
        }),

        // Exporting
        navModuleFactory('exporting', [
            // Left/Up
            [[37, 38], function () {
                var i = chart.highlightedExportItem || 0,
                    reachedEnd = true;

                // Try to highlight prev item in list. Highlighting e.g.
                // separators will fail.
                while (i--) {
                    if (chart.highlightExportItem(i)) {
                        reachedEnd = false;
                        break;
                    }
                }
                if (reachedEnd) {
                    chart.highlightLastExportItem();
                    return true;
                }
            }],
            // Right/Down
            [[39, 40], function () {
                var highlightedExportItem = chart.highlightedExportItem || 0,
                    reachedEnd = true;

                // Try to highlight next item in list. Highlighting e.g.
                // separators will fail.
                for (
                    var i = highlightedExportItem + 1;
                    i < chart.exportDivElements.length;
                    ++i
                ) {
                    if (chart.highlightExportItem(i)) {
                        reachedEnd = false;
                        break;
                    }
                }
                if (reachedEnd) {
                    chart.highlightExportItem(0);
                    return true;
                }
            }],
            // Enter/Spacebar
            [[13, 32], function () {
                fakeClickEvent(
                    chart.exportDivElements[chart.highlightedExportItem]
                );
            }]
        ], {
            // Only run exporting navigation if exporting support exists and is
            // enabled on chart
            validate: function () {
                return (
                    chart.exportChart &&
                    !(
                        chart.options.exporting &&
                        chart.options.exporting.enabled === false
                    )
                );
            },
            // Show export menu
            init: function (direction) {
                chart.highlightedPoint = null;
                chart.showExportMenu();
                // If coming back to export menu from other module, try to
                // highlight last item in menu
                if (direction < 0) {
                    chart.highlightLastExportItem();
                }
            },
            // Hide the menu
            terminate: function () {
                chart.hideExportMenu();
            }
        }),

        // Map zoom
        navModuleFactory('mapZoom', [
            // Up/down/left/right
            [[38, 40, 37, 39], function (keyCode) {
                chart[keyCode === 38 || keyCode === 40 ? 'yAxis' : 'xAxis'][0]
                    .panStep(keyCode < 39 ? -1 : 1);
            }],

            // Tabs
            [[9], function (keyCode, e) {
                var button;

                // Deselect old
                chart.mapNavButtons[chart.focusedMapNavButtonIx].setState(0);
                if (
                    e.shiftKey && !chart.focusedMapNavButtonIx ||
                    !e.shiftKey && chart.focusedMapNavButtonIx
                ) { // trying to go somewhere we can't?
                    chart.mapZoom(); // Reset zoom
                    // Nowhere to go, go to prev/next module
                    return this.move(e.shiftKey ? -1 : 1);
                }
                chart.focusedMapNavButtonIx += e.shiftKey ? -1 : 1;
                button = chart.mapNavButtons[chart.focusedMapNavButtonIx];
                chart.setFocusToElement(button.box, button);
                button.setState(2);
            }],

            // Enter/Spacebar
            [[13, 32], function () {
                fakeClickEvent(
                    chart.mapNavButtons[chart.focusedMapNavButtonIx].element
                );
            }]
        ], {
            // Only run this module if we have map zoom on the chart
            validate: function () {
                return (
                    chart.mapZoom &&
                    chart.mapNavButtons &&
                    chart.mapNavButtons.length === 2
                );
            },

            // Make zoom buttons do their magic
            init: function (direction) {
                var zoomIn = chart.mapNavButtons[0],
                    zoomOut = chart.mapNavButtons[1],
                    initialButton = direction > 0 ? zoomIn : zoomOut;

                chart.mapNavButtons.forEach(function (button, i) {
                    button.element.setAttribute('tabindex', -1);
                    button.element.setAttribute('role', 'button');
                    button.element.setAttribute(
                        'aria-label',
                        chart.langFormat(
                            'accessibility.mapZoom' + (i ? 'Out' : 'In'),
                            { chart: chart }
                        )
                    );
                });

                chart.setFocusToElement(initialButton.box, initialButton);
                initialButton.setState(2);
                chart.focusedMapNavButtonIx = direction > 0 ? 0 : 1;
            }
        }),

        // Highstock range selector (minus input boxes)
        navModuleFactory('rangeSelector', [
            // Left/Right/Up/Down
            [[37, 39, 38, 40], function (keyCode) {
                var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;

                // Try to highlight next/prev button
                if (
                    !chart.highlightRangeSelectorButton(
                        chart.highlightedRangeSelectorItemIx + direction
                    )
                ) {
                    return this.move(direction);
                }
            }],
            // Enter/Spacebar
            [[13, 32], function () {
                // Don't allow click if button used to be disabled
                if (chart.oldRangeSelectorItemState !== 3) {
                    fakeClickEvent(
                        chart.rangeSelector.buttons[
                            chart.highlightedRangeSelectorItemIx
                        ].element
                    );
                }
            }]
        ], {
            // Only run this module if we have range selector
            validate: function () {
                return (
                    chart.rangeSelector &&
                    chart.rangeSelector.buttons &&
                    chart.rangeSelector.buttons.length
                );
            },

            // Make elements focusable and accessible
            init: function (direction) {
                chart.rangeSelector.buttons.forEach(function (button) {
                    button.element.setAttribute('tabindex', '-1');
                    button.element.setAttribute('role', 'button');
                    button.element.setAttribute(
                        'aria-label',
                        chart.langFormat(
                            'accessibility.rangeSelectorButton',
                            {
                                chart: chart,
                                buttonText: button.text && button.text.textStr
                            }
                        )
                    );
                });
                // Focus first/last button
                chart.highlightRangeSelectorButton(
                    direction > 0 ? 0 : chart.rangeSelector.buttons.length - 1
                );
            }
        }),

        // Highstock range selector, input boxes
        navModuleFactory('rangeSelectorInput', [
            // Tab/Up/Down
            [[9, 38, 40], function (keyCode, e) {
                var direction =
                    (keyCode === 9 && e.shiftKey || keyCode === 38) ? -1 : 1,

                    newIx = chart.highlightedInputRangeIx =
                        chart.highlightedInputRangeIx + direction;

                // Try to highlight next/prev item in list.
                if (newIx > 1 || newIx < 0) { // Out of range
                    return this.move(direction);
                }
                chart.rangeSelector[newIx ? 'maxInput' : 'minInput'].focus();
            }]
        ], {
            // Only run if we have range selector with input boxes
            validate: function () {
                var inputVisible = (
                    chart.rangeSelector &&
                    chart.rangeSelector.inputGroup &&
                    chart.rangeSelector.inputGroup.element
                        .getAttribute('visibility') !== 'hidden'
                );

                return (
                    inputVisible &&
                    chart.options.rangeSelector.inputEnabled !== false &&
                    chart.rangeSelector.minInput &&
                    chart.rangeSelector.maxInput
                );
            },

            // Highlight first/last input box
            init: function (direction) {
                chart.highlightedInputRangeIx = direction > 0 ? 0 : 1;
                chart.rangeSelector[
                    chart.highlightedInputRangeIx ? 'maxInput' : 'minInput'
                ].focus();
            }
        }),

        // Legend navigation
        navModuleFactory('legend', [
            // Left/Right/Up/Down
            [[37, 39, 38, 40], function (keyCode) {
                var direction = (keyCode === 37 || keyCode === 38) ? -1 : 1;

                // Try to highlight next/prev legend item
                if (!chart.highlightLegendItem(
                    chart.highlightedLegendItemIx + direction
                ) && chart.legend.allItems.length > 1) {
                    // Wrap around if more than 1 item
                    this.init(direction);
                }
            }],
            // Enter/Spacebar
            [[13, 32], function () {
                var legendElement = chart.legend.allItems[
                    chart.highlightedLegendItemIx
                ].legendItem.element;

                fakeClickEvent(
                    !chart.legend.options.useHTML ? // #8561
                        legendElement.parentNode : legendElement
                );
            }]
        ], {
            // Only run this module if we have at least one legend - wait for
            // it - item. Don't run if the legend is populated by a colorAxis.
            // Don't run if legend navigation is disabled.
            validate: function () {
                return chart.legend && chart.legend.allItems &&
                    chart.legend.display &&
                    !(chart.colorAxis && chart.colorAxis.length) &&
                    (chart.options.legend &&
                    chart.options.legend.keyboardNavigation &&
                    chart.options.legend.keyboardNavigation.enabled) !== false;
            },

            // Make elements focusable and accessible
            init: function (direction) {
                chart.legend.allItems.forEach(function (item) {
                    item.legendGroup.element.setAttribute('tabindex', '-1');
                    item.legendGroup.element.setAttribute('role', 'button');
                    item.legendGroup.element.setAttribute(
                        'aria-label',
                        chart.langFormat(
                            'accessibility.legendItem',
                            {
                                chart: chart,
                                itemName: stripTags(item.name)
                            }
                        )
                    );
                });
                // Focus first/last item
                chart.highlightLegendItem(
                    direction > 0 ? 0 : chart.legend.allItems.length - 1
                );
            }
        })
    ];
};

/**
 * Add exit anchor to the chart. We use this to move focus out of chart whenever
 * we want, by setting focus to this div and not preventing the default tab
 * action. We also use this when users come back into the chart by tabbing back,
 * in order to navigate from the end of the chart.
 *
 * @private
 * @function Highcharts.Chart#addExitAnchor
 *
 * @return {Function}
 *         Returns the unbind function for the exit anchor's event handler.
 */
H.Chart.prototype.addExitAnchor = function () {
    var chart = this;

    chart.tabExitAnchor = doc.createElement('div');
    chart.tabExitAnchor.setAttribute('tabindex', '0');

    // Hide exit anchor
    merge(true, chart.tabExitAnchor.style, {
        position: 'absolute',
        left: '-9999px',
        top: 'auto',
        width: '1px',
        height: '1px',
        overflow: 'hidden'
    });

    chart.renderTo.appendChild(chart.tabExitAnchor);
    return addEvent(
        chart.tabExitAnchor,
        'focus',
        function (ev) {
            var e = ev || win.event,
                curModule;

            // If focusing and we are exiting, do nothing once.
            if (!chart.exiting) {

                // Not exiting, means we are coming in backwards
                chart.renderTo.focus();
                e.preventDefault();

                // Move to last valid keyboard nav module
                // Note the we don't run it, just set the index
                chart.keyboardNavigationModuleIndex =
                    chart.keyboardNavigationModules.length - 1;
                curModule = chart.keyboardNavigationModules[
                    chart.keyboardNavigationModuleIndex
                ];

                // Validate the module
                if (curModule.validate && !curModule.validate()) {
                    // Invalid.
                    // Move inits next valid module in direction
                    curModule.move(-1);
                } else {
                    // We have a valid module, init it
                    curModule.init(-1);
                }

            } else {
                // Don't skip the next focus, we only skip once.
                chart.exiting = false;
            }
        }
    );
};

/**
 * Clear the chart and reset the navigation state.
 *
 * @private
 * @function Highcharts.Chart#resetKeyboardNavigation
 */
H.Chart.prototype.resetKeyboardNavigation = function () {
    var chart = this,
        curMod = (
            chart.keyboardNavigationModules &&
            chart.keyboardNavigationModules[
                chart.keyboardNavigationModuleIndex || 0
            ]
        );

    if (curMod && curMod.terminate) {
        curMod.terminate();
    }
    if (chart.focusElement) {
        chart.focusElement.removeFocusBorder();
    }
    chart.keyboardNavigationModuleIndex = 0;
    chart.keyboardReset = true;
};

// On destroy, we need to clean up the focus border and the state.
H.addEvent(H.Series, 'destroy', function () {
    var chart = this.chart;

    if (chart.highlightedPoint && chart.highlightedPoint.series === this) {
        delete chart.highlightedPoint;
        if (chart.focusElement) {
            chart.focusElement.removeFocusBorder();
        }
    }
});

// Add keyboard navigation events on chart load.
H.Chart.prototype.callbacks.push(function (chart) {
    var a11yOptions = chart.options.accessibility;

    if (a11yOptions.enabled && a11yOptions.keyboardNavigation.enabled) {

        // Init nav modules. We start at the first module, and as the user
        // navigates through the chart the index will increase to use different
        // handler modules.
        chart.addKeyboardNavigationModules();
        chart.keyboardNavigationModuleIndex = 0;

        // Make chart container reachable by tab
        if (
            chart.container.hasAttribute &&
            !chart.container.hasAttribute('tabIndex')
        ) {
            chart.container.setAttribute('tabindex', '0');
        }

        // Add tab exit anchor
        if (!chart.tabExitAnchor) {
            chart.unbindExitAnchorFocus = chart.addExitAnchor();
        }

        // Handle keyboard events by routing them to active keyboard nav module
        chart.unbindKeydownHandler = addEvent(chart.renderTo, 'keydown',
            function (ev) {
                var e = ev || win.event,
                    curNavModule = chart.keyboardNavigationModules[
                        chart.keyboardNavigationModuleIndex
                    ];

                chart.keyboardReset = false;
                // If there is a nav module for the current index, run it.
                // Otherwise, we are outside of the chart in some direction.
                if (curNavModule) {
                    if (curNavModule.run(e)) {
                        // Successfully handled this key event, stop default
                        e.preventDefault();
                    }
                }
            });

        // Reset chart navigation state if we click outside the chart and it's
        // not already reset
        chart.unbindBlurHandler = addEvent(doc, 'mouseup', function () {
            if (
                !chart.keyboardReset &&
                !(chart.pointer && chart.pointer.chartPosition)
            ) {
                chart.resetKeyboardNavigation();
            }
        });

        // Add cleanup handlers
        addEvent(chart, 'destroy', function () {
            chart.resetKeyboardNavigation();
            if (chart.unbindExitAnchorFocus && chart.tabExitAnchor) {
                chart.unbindExitAnchorFocus();
            }
            if (chart.unbindKeydownHandler && chart.renderTo) {
                chart.unbindKeydownHandler();
            }
            if (chart.unbindBlurHandler) {
                chart.unbindBlurHandler();
            }
        });
    }
});

redpajama_set_name: RedPajamaGithub


One of the best things about the good girl/bad girl hybrid is that she lives in every woman.
Some say it makes the world go round, some think it's the root of all evil, but most of us wouldn't say no if it was offered to us – especially if it came with a handsome husband attached. The two never made it down the isle, but they were close enough for us to imagine!
Really, a lesbian scene on top of steroid treatments.