diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index c844db9778..cbc492488b 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -77,3 +77,4 @@ $ -> TwoFactorAuth.attachTo('.two-factor-auth-container') + diff --git a/app/assets/javascripts/component_ui/auto_window.js.coffee b/app/assets/javascripts/component_ui/auto_window.js.coffee index 7d08481cf7..2c075b432c 100644 --- a/app/assets/javascripts/component_ui/auto_window.js.coffee +++ b/app/assets/javascripts/component_ui/auto_window.js.coffee @@ -44,6 +44,7 @@ BORDER_WIDTH = 1 window_w = window.innerWidth markets_w = $('#market_list').width() order_book_w = $('#order_book').width() + $('#market_list .panel panel-default').width(markets_w) $('#candlestick').width(window_w - order_book_w - markets_w - gutter_4x - 20) @$node.resize() diff --git a/app/assets/javascripts/component_ui/candlestick.js.coffee b/app/assets/javascripts/component_ui/candlestick.js.coffee index f57ffb8050..6e94392f07 100644 --- a/app/assets/javascripts/component_ui/candlestick.js.coffee +++ b/app/assets/javascripts/component_ui/candlestick.js.coffee @@ -58,7 +58,10 @@ COLOR = { candlestick: _.extend({}, COLOR_OFF.candlestick), close: _.extend({}, COLOR_OFF.close) } -INDICATOR = {MA: false, EMA: false} +INDICATOR = {MA: false, EMA: false, OFF: false} +MACD_INDICATOR = {MACD: false, OFF: false} +SIG_INDICATOR = {SIG: false, OFF: false} +HIST_INDICATOR = {HIST: false, OFF: false} @CandlestickUI = flight.component -> @mask = -> @@ -93,9 +96,8 @@ INDICATOR = {MA: false, EMA: false} INDICATOR[data.x] = true if chart = @$node.find('#candlestick_chart').highcharts() - # reset all series depend on close for s in chart.series - if s.userOptions.linkedTo == 'close' + if s.userOptions.innerGroup == 'main' s.setVisible(true, false) for indicator, visible of INDICATOR @@ -104,6 +106,21 @@ INDICATOR = {MA: false, EMA: false} s.setVisible(visible, false) chart.redraw() + @switchMACDIndicator = (event, data) -> + MACD_INDICATOR[key] = false for key, val of MACD_INDICATOR + MACD_INDICATOR[data.x] = true + + if chart = @$node.find('#candlestick_chart').highcharts() + for s in chart.series + if s.userOptions.innerGroup == 'macd' + s.setVisible(MACD_INDICATOR['MACD'], false) + +# for indicator, visible of MACD_INDICATOR +# for s in chart.series +# if s.userOptions.algorithm? && (s.userOptions.algorithm == indicator) +# s.setVisible(visible, false) + chart.redraw() + @default_range = (unit) -> 1000 * 60 * unit * 100 @@ -140,27 +157,33 @@ INDICATOR = {MA: false, EMA: false} marginTop: 95 backgroundColor: 'rgba(0,0,0, 0.0)' + mapNavigation: + enableMouseWheelZoom: true + credits: enabled: false tooltip: - crosshairs: [{ - width: 0.5, - dashStyle: 'solid', - color: '#777' - }, false], +# crosshairs: [{ +# width: 0.5, +# dashStyle: 'solid', +# color: '#777' +# }, false], valueDecimals: gon.market.bid.fixed borderWidth: 0 backgroundColor: 'rgba(0,0,0,0)' borderRadius: 2 shadow: false shared: true + split: false positioner: (w, h, point) -> chart_w = $(@chart.renderTo).width() chart_h = $(@chart.renderTo).height() grid_h = Math.min(20, Math.ceil(chart_h/10)) - x = Math.max(10, point.plotX-w-20) - y = Math.max(0, Math.floor(point.plotY/grid_h)*grid_h-20) + # x = Math.max(10, point.plotX-w-20) + # y = Math.max(0, Math.floor(point.plotY/grid_h)*grid_h-20) + x = 8 + y = 8 x: x, y: y useHTML: true formatter: -> @@ -181,7 +204,13 @@ INDICATOR = {MA: false, EMA: false} tooltipTemplate title: title indicator: INDICATOR - format: (v, fixed=3) -> Highcharts.numberFormat v, fixed + macd_indicator: MACD_INDICATOR + format: (v, fixed=3) -> formatter.fixPriceGroup parseFloat(v) + format4: (v, fixed) -> + if fixed > 4 + formatter.round(v, 4) + else + formatter.round(v, fixed) points: _.reduce chart.series, fun, {} plotOptions: @@ -222,6 +251,9 @@ INDICATOR = {MA: false, EMA: false} maskFill: 'rgba(32, 32, 32, 0.6)' outlineColor: '#333' outlineWidth: 1 + baseSeries: 'candlestick' + series: + color: '#306196' xAxis: dateTimeLabelFormats: DATETIME_LABEL_FORMAT @@ -232,6 +264,20 @@ INDICATOR = {MA: false, EMA: false} tickColor: '#333' tickWidth: 2 range: range + maxPadding: 1 + crosshair: + snap: false + width: 0.5 + dashStyle: 'solid' + color: '#777' + interpolate: true + label: + enabled: true + formatter: (val) -> + Highcharts.dateFormat('%a %d %b %H:%M:%S', val) + backgroundColor: '#2b3434' + style: + color: '#aaa' events: afterSetExtremes: (e) -> if e.trigger == 'navigator' && e.triggerOp == 'navigator-drag' @@ -242,17 +288,31 @@ INDICATOR = {MA: false, EMA: false} { labels: enabled: true - align: 'right' - formatter: -> Highcharts.numberFormat @.value, gon.market.bid.fixed + align: 'left' + reserveSpace: true + formatter: -> formatter.fixPriceGroup @.value x: 2 y: -5 zIndex: -7 +# format: '{value:.9f}' gridLineColor: '#222' gridLineDashStyle: 'ShortDot' top: "0%" height: "70%" lineColor: '#fff' minRange: if gon.ticker.last then parseFloat(gon.ticker.last)/25 else null + crosshair: + snap: false + interpolate: true + width: 0.5 + dashStyle: 'solid' + color: '#777' + label: + enabled: true + formatter: (v) -> formatter.fixPriceGroup parseFloat(v) + backgroundColor: '#2b3434' + style: + color: '#aaa' } { labels: @@ -260,6 +320,17 @@ INDICATOR = {MA: false, EMA: false} top: "70%" gridLineColor: '#000' height: "15%" + crosshair: + snap: false + width: 0.5 + dashStyle: 'solid' + color: '#777' + label: + enabled: true + format: '{value:.4f}' + backgroundColor: '#2b3434' + style: + color: '#aaa' } { labels: @@ -267,25 +338,15 @@ INDICATOR = {MA: false, EMA: false} top: "85%" gridLineColor: '#000' height: "15%" + # crosshair: + # snap: false + # width: 0.5 + # dashStyle: 'solid' + # color: '#777' } ] series: [ - _.extend({ - id: 'candlestick' - name: gon.i18n.chart.candlestick - type: "candlestick" - data: data['candlestick'] - showInLegend: false - }, COLOR['candlestick']), - _.extend({ - id: 'close' - type: 'spline' - data: data['close'] - showInLegend: false - marker: - radius: 0 - }, COLOR['close']), { id: 'volume' name: gon.i18n.chart.volume @@ -299,12 +360,14 @@ INDICATOR = {MA: false, EMA: false} id: 'ma5' name: 'MA5', linkedTo: 'close', + innerGroup: 'main', showInLegend: true, type: 'trendline', algorithm: 'MA', periods: 5 color: '#7c9aaa' visible: INDICATOR['MA'] + zIndex: 1 marker: radius: 0 } @@ -312,12 +375,14 @@ INDICATOR = {MA: false, EMA: false} id: 'ma10' name: 'MA10' linkedTo: 'close', + innerGroup: 'main', showInLegend: true, type: 'trendline', algorithm: 'MA', periods: 10 color: '#be8f53' visible: INDICATOR['MA'] + zIndex: 2 marker: radius: 0 } @@ -325,12 +390,14 @@ INDICATOR = {MA: false, EMA: false} id: 'ema7' name: 'EMA7', linkedTo: 'close', + innerGroup: 'main', showInLegend: true, type: 'trendline', algorithm: 'EMA', periods: 7 color: '#7c9aaa' visible: INDICATOR['EMA'] + zIndex: 3 marker: radius: 0 } @@ -338,12 +405,14 @@ INDICATOR = {MA: false, EMA: false} id: 'ema30' name: 'EMA30', linkedTo: 'close', + innerGroup: 'main', showInLegend: true, type: 'trendline', algorithm: 'EMA', periods: 30 color: '#be8f53' visible: INDICATOR['EMA'] + zIndex: 4 marker: radius: 0 } @@ -351,11 +420,14 @@ INDICATOR = {MA: false, EMA: false} id: 'macd' name : 'MACD', linkedTo: 'close', + innerGroup: 'macd', yAxis: 2, showInLegend: true, type: 'trendline', algorithm: 'MACD' color: '#7c9aaa' + visible: MACD_INDICATOR['MACD'] + zIndex: 5 marker: radius: 0 } @@ -363,11 +435,14 @@ INDICATOR = {MA: false, EMA: false} id: 'sig' name : 'SIG', linkedTo: 'close', + innerGroup: 'macd', yAxis: 2, showInLegend: true, type: 'trendline', algorithm: 'signalLine' color: '#be8f53' + visible: MACD_INDICATOR['MACD'] + zIndex: 6 marker: radius: 0 } @@ -375,11 +450,30 @@ INDICATOR = {MA: false, EMA: false} id: 'hist' name: 'HIST', linkedTo: 'close', + innerGroup: 'macd', yAxis: 2, showInLegend: true, type: 'histogram' + visible: MACD_INDICATOR['MACD'] + zIndex: 7 color: '#990f0f' } + _.extend({ + id: 'candlestick' + name: gon.i18n.chart.candlestick + type: "candlestick" + data: data['candlestick'] + showInLegend: false + zIndex: 0 + }, COLOR['candlestick']), + _.extend({ + id: 'close' + type: 'spline' + data: data['close'] + showInLegend: false + marker: + radius: 0 + }, COLOR['close']), ] @formatPointArray = (point) -> @@ -438,5 +532,6 @@ INDICATOR = {MA: false, EMA: false} @on document, 'market::candlestick::response', @init @on document, 'market::candlestick::trades', @updateByTrades @on document, 'switch::main_indicator_switch', @switchMainIndicator + @on document, 'switch::indicator_switch', @switchMACDIndicator @on document, 'switch::type_switch', @switchType diff --git a/app/assets/javascripts/component_ui/market_switch.js.coffee b/app/assets/javascripts/component_ui/market_switch.js.coffee index 84bec42b9d..f013944ce3 100644 --- a/app/assets/javascripts/component_ui/market_switch.js.coffee +++ b/app/assets/javascripts/component_ui/market_switch.js.coffee @@ -14,6 +14,7 @@ window.MarketSwitchUI = flight.component -> @select('marketGroupName').text item.find('span').text() @select('marketsTable').attr("class", "table table-hover markets #{name}") + @select('marketsTable').attr("style", "font-size: 12px") @updateMarket = (select, ticker) -> trend = formatter.trend ticker.last_trend @@ -43,11 +44,20 @@ window.MarketSwitchUI = flight.component -> window.location.href = window.formatter.market_url($(@).data('market')) @.hide_accounts = $('tr.hide') + $('.view_all_accounts').on 'click', (e) => $el = $(e.currentTarget) - if @.hide_accounts.hasClass('hide') - $el.text($el.data('hide-text')) - @.hide_accounts.removeClass('hide') - else + if @.hide_accounts.hasClass('show1') $el.text($el.data('show-text')) - @.hide_accounts.addClass('hide') + for acc in @.hide_accounts + if acc.lastChild.firstChild.textContent != '0.0000' + if acc.attributes['class'].value.indexOf('show1') > 0 + acc.attributes['class'].value = acc.attributes['class'].value.substr(0, acc.attributes['class'].value.indexOf('show1') - 1) + acc.attributes['class'].value += " hide" + else if @.hide_accounts.hasClass('hide') + $el.text($el.data('hide-text')) + for acc in @.hide_accounts + if acc.lastChild.firstChild.textContent != '0.0000' + if acc.attributes['class'].value.indexOf('hide') > 0 + acc.attributes['class'].value = acc.attributes['class'].value.substr(0, acc.attributes['class'].value.indexOf('hide') - 1) + acc.attributes['class'].value += " show1" diff --git a/app/assets/javascripts/component_ui/order_total.js.coffee b/app/assets/javascripts/component_ui/order_total.js.coffee index c3ab7b66ef..06b2f06cba 100644 --- a/app/assets/javascripts/component_ui/order_total.js.coffee +++ b/app/assets/javascripts/component_ui/order_total.js.coffee @@ -14,5 +14,13 @@ @changeOrder @value unless @validateRange(total) @setInputValue @value + @fee = 0.0 + if event.target.id == 'ask_entry' + @fee = gon.market.ask.fee + else + @fee = gon.market.bid.fee + order.total = @value + order.fee = @value * @fee @trigger 'place_order::order::updated', order + console.log(event.target.id, order) diff --git a/app/assets/javascripts/component_ui/place_order.js.coffee b/app/assets/javascripts/component_ui/place_order.js.coffee index 6b4ffef5fe..4527d4e48e 100644 --- a/app/assets/javascripts/component_ui/place_order.js.coffee +++ b/app/assets/javascripts/component_ui/place_order.js.coffee @@ -3,9 +3,11 @@ formSel: 'form' successSel: '.status-success' infoSel: '.status-info' + feeSel: '.status-fee' dangerSel: '.status-danger' priceAlertSel: '.hint-price-disadvantage' positionsLabelSel: '.hint-positions' + feeLabelSel: '.hint-fee' priceSel: 'input[id$=price]' volumeSel: 'input[id$=volume]' @@ -23,6 +25,7 @@ @select('successSel').text('') @select('infoSel').text('') @select('dangerSel').text('') + @select('feeSel').text('') @resetForm = (event) -> @trigger 'place_order::reset::price' @@ -104,11 +107,17 @@ order[@usedInput] = 0 unless order[@usedInput] available = formatter.fix type, @getBalance().minus(order[@usedInput]) + if @select('priceSel').val() != 0.0 + @select('feeLabelSel').hide().text(formatter.fixPriceGroup(order.fee)).fadeIn() + else + @select('feeLabelSel').fadeOut().text('') + if BigNumber(available).equals(0) @select('positionsLabelSel').hide().text(gon.i18n.place_order["full_#{type}"]).fadeIn() else @select('positionsLabelSel').fadeOut().text('') node.text(available) + #console.log("priceorder/order_fee = " + order.fee) @priceAlertHide = (event) -> @select('priceAlertSel').fadeOut -> diff --git a/app/assets/javascripts/funds.js.coffee b/app/assets/javascripts/funds.js.coffee index 9ad8067ee5..74054f4d86 100644 --- a/app/assets/javascripts/funds.js.coffee +++ b/app/assets/javascripts/funds.js.coffee @@ -33,3 +33,6 @@ #= require_tree ./component_mixin #= require_tree ./component_data #= require_tree ./component_ui + + + diff --git a/app/assets/javascripts/locales/en.js.erb b/app/assets/javascripts/locales/en.js.erb index 610d4146a0..c012dbc6c4 100644 --- a/app/assets/javascripts/locales/en.js.erb +++ b/app/assets/javascripts/locales/en.js.erb @@ -4,5 +4,3 @@ - - diff --git a/app/assets/javascripts/market.js.coffee b/app/assets/javascripts/market.js.coffee index 8733a4e8e5..72d94b6e91 100644 --- a/app/assets/javascripts/market.js.coffee +++ b/app/assets/javascripts/market.js.coffee @@ -64,3 +64,4 @@ $ -> $('.panel-body-content').niceScroll autohidemode: true cursorborder: "none" + diff --git a/app/assets/javascripts/templates/market_trade.jst.eco b/app/assets/javascripts/templates/market_trade.jst.eco index d52e7b4117..0a882c5dd1 100644 --- a/app/assets/javascripts/templates/market_trade.jst.eco +++ b/app/assets/javascripts/templates/market_trade.jst.eco @@ -4,11 +4,11 @@
<%- formatter.trade_time @date %>
-
+
-
- <%- formatter.mask_fixed_price @price %> +
+ <%- formatter.ticker_price @price %>
<%- formatter.mask_fixed_volume @amount %> diff --git a/app/assets/javascripts/templates/order_active.jst.eco b/app/assets/javascripts/templates/order_active.jst.eco index c4c4d3631c..25485a28ed 100644 --- a/app/assets/javascripts/templates/order_active.jst.eco +++ b/app/assets/javascripts/templates/order_active.jst.eco @@ -4,16 +4,16 @@ <%- formatter.fulltime @at %> - + <%= formatter.short_trade @kind %><%= gon.i18n.trade_state[trade_state] %> - + <%- formatter.mask_fixed_price @price %> - + <%- formatter.mask_fixed_volume @volume %> - + <%- formatter.amount @volume, @price %> diff --git a/app/assets/javascripts/templates/order_book_ask.jst.eco b/app/assets/javascripts/templates/order_book_ask.jst.eco index 6577a0099c..7f17a3c2c3 100644 --- a/app/assets/javascripts/templates/order_book_ask.jst.eco +++ b/app/assets/javascripts/templates/order_book_ask.jst.eco @@ -1,8 +1,8 @@
- <%- formatter.mask_fixed_price @price %> + <%- formatter.ticker_price @price %>
-
+
<%- formatter.mask_fixed_volume @volume %>
diff --git a/app/assets/javascripts/templates/order_book_bid.jst.eco b/app/assets/javascripts/templates/order_book_bid.jst.eco index c524443577..e91291f00c 100644 --- a/app/assets/javascripts/templates/order_book_bid.jst.eco +++ b/app/assets/javascripts/templates/order_book_bid.jst.eco @@ -1,11 +1,11 @@ -
+
<%- formatter.amount @volume, @price %>
-
+
<%- formatter.mask_fixed_volume @volume %>
- <%- formatter.mask_fixed_price @price %> + <%- formatter.ticker_price @price %>
diff --git a/app/assets/javascripts/templates/order_done.jst.eco b/app/assets/javascripts/templates/order_done.jst.eco index 58917c27ae..b85134ffee 100644 --- a/app/assets/javascripts/templates/order_done.jst.eco +++ b/app/assets/javascripts/templates/order_done.jst.eco @@ -2,16 +2,16 @@ <%- formatter.fulltime @at %> - + <%- formatter.short_trade @kind %> - + <%= formatter.fix_bid @price %> - + <%= formatter.fix_ask @volume %> - + <%- formatter.amount @volume, @price %> diff --git a/app/assets/javascripts/templates/tooltip.jst.eco b/app/assets/javascripts/templates/tooltip.jst.eco index b9f249cc68..2187d74e18 100644 --- a/app/assets/javascripts/templates/tooltip.jst.eco +++ b/app/assets/javascripts/templates/tooltip.jst.eco @@ -21,35 +21,37 @@
<%= gon.i18n.chart.volume %> - <%= @format @points.volume.y, gon.market.ask.fixed %> + <%= @format4 @points.volume.y, 3 %>
    <% if @indicator['MA']: %>
  • - <%= @points.ma5.series.name %>: <%= @format @points.ma5.y, gon.market.bid.fixed %> + <%= @points.ma5.series.name %>: <%= @format4 @points.ma5.y, gon.market.bid.fixed %>
  • - <%= @points.ma10.series.name %>: <%= @format @points.ma10.y, gon.market.bid.fixed %> + <%= @points.ma10.series.name %>: <%= @format4 @points.ma10.y, gon.market.bid.fixed %>
  • <% end %> <% if @indicator['EMA']: %>
  • - <%= @points.ema7.series.name %>: <%= @format @points.ema7.y, gon.market.bid.fixed %> + <%= @points.ema7.series.name %>: <%= @format4 @points.ema7.y, gon.market.bid.fixed %>
  • - <%= @points.ema30.series.name %>: <%= @format @points.ema30.y, gon.market.bid.fixed %> + <%= @points.ema30.series.name %>: <%= @format4 @points.ema30.y, gon.market.bid.fixed %> +
  • + <% end %> + <% if @macd_indicator['MACD']: %> +
  • + <%= @points.macd.series.name %>: <%= @format4 @points.macd.y %> +
  • +
  • + <%= @points.sig.series.name %>: <%= @format4 @points.sig.y %> +
  • +
  • + <%= @points.hist.series.name %>: <%= @format4 @points.hist.y %>
  • <% end %> -
  • - <%= @points.macd.series.name %>: <%= @format @points.macd.y %> -
  • -
  • - <%= @points.sig.series.name %>: <%= @format @points.sig.y %> -
  • -
  • - <%= @points.hist.series.name %>: <%= @format @points.hist.y %> -
diff --git a/app/assets/stylesheets/market.css.scss b/app/assets/stylesheets/market.css.scss index 74ccebe865..00cad8915d 100644 --- a/app/assets/stylesheets/market.css.scss +++ b/app/assets/stylesheets/market.css.scss @@ -479,8 +479,8 @@ chat_tabs_wrapper { .last { padding-top: 5px; - font-size: 34px; - line-height: 34px; + font-size: 24px; + line-height: 42px; } } @@ -495,6 +495,11 @@ chat_tabs_wrapper { table.asks, table.bids { font-size: 12px; + + td.price { + .fill { color: #333; } + } + .new { background-color: #333; div { display: none; } @@ -570,6 +575,10 @@ chat_tabs_wrapper { } .panel-body-content { min-height: $market-trades-height - $panel_table_header_high; + + td.price { + .fill { color: #333; } + } } table { @@ -730,29 +739,38 @@ chat_tabs_wrapper { input { text-align: right; - padding-top: 8px !important; + padding-top: 6px !important; padding-bottom: 4px !important; padding-right: 6px !important; - padding-left: 24px !important; + padding-left: 6px !important; + font-size: 13px } margin-bottom: 8px; } .hint-price-disadvantage, .hint-positions { - top: 3px; - left: 60px; + top: 2px; + left: 72px; + margin-left: 0px; + position: absolute; + z-index: 10; + } + + .hint-fee { + top: 14px; + left: 72px; margin-left: 0px; position: absolute; z-index: 10; } .label { font-size: inherit; } - .label.label-info { color: $brand-info } + .label.label-info { color: $brand-info; font-size: 9px; padding: 0 0 0 0 } .label.label-success { color: $brand-success } - .label.label-danger { color: $brand-danger } + .label.label-danger { color: $brand-danger; font-size: 9px; padding: 0 0 0 0 } - span.status-info, .status-success, .status-danger { + span.status-info, .status-success, .status-danger, .status-fee { top: -3px; left: -6px; margin-left: 0px; diff --git a/app/assets/stylesheets/vars/_market.css.scss b/app/assets/stylesheets/vars/_market.css.scss index 7a307a3e45..4f2c382521 100644 --- a/app/assets/stylesheets/vars/_market.css.scss +++ b/app/assets/stylesheets/vars/_market.css.scss @@ -13,8 +13,8 @@ $gutter-4x: $gutter * 4; $gutter-5x: $gutter * 5; $gutter-6x: $gutter * 6; -$entry-width: 242px; -$entry-height: 576px; +$entry-width: 332px; +$entry-height: 496px; $right-entry-height: 196px; $ticker-width: $entry-width * 2 + $gutter; @@ -24,7 +24,7 @@ $depths-width: $entry-width * 2 + $gutter; $depths-height: 102px; $my-orders-width: $depths-width; -$my-orders-height: 92px; +$my-orders-height: 184px; $order-book-width: $depths-width; $order-book-height: $min-height - $navbar-height - $right-entry-height - $ticker-height - $depths-height - $my-orders-height - $gutter-6x; diff --git a/app/models/currency.rb b/app/models/currency.rb index 1f10289173..5d4e251a60 100644 --- a/app/models/currency.rb +++ b/app/models/currency.rb @@ -98,6 +98,7 @@ def refresh_status end if @local_status + Rails.logger.info @local_status Rails.cache.write(blocks_count_cache_key, @local_status[:blocks]) if coin? Rails.cache.write(headers_count_cache_key, @local_status[:headers]) if coin? Rails.cache.write(blocktime_cache_key, Time.at(@local_status[:mediantime]).to_datetime.strftime("%Y-%m-%d %H:%M:%S")) if coin? diff --git a/app/models/global.rb b/app/models/global.rb index 128bcfc9c6..435d0c0fe7 100644 --- a/app/models/global.rb +++ b/app/models/global.rb @@ -10,7 +10,7 @@ def channel def trigger(event, data) Pusher.trigger_async(channel, event, data) - Rails.logger.info "Pusher_3: #{channel}, #{data}" + # Rails.logger.info "Pusher_3: #{channel}, #{data}" end def daemon_statuses diff --git a/app/models/worker/order_processor.rb b/app/models/worker/order_processor.rb index ccc32831ec..2a865d488c 100644 --- a/app/models/worker/order_processor.rb +++ b/app/models/worker/order_processor.rb @@ -4,6 +4,7 @@ class OrderProcessor def initialize @cancel_queue = [] create_cancel_thread + #create_test_thread end def process(payload, metadata, delivery_info) @@ -63,6 +64,42 @@ def process_cancel_jobs Rails.logger.debug $!.backtrace.join("\n") end + def create_test_order + member_rand = rand(10) + member1 = 2 + member2 = 3 + if member_rand >= 5 + member1 = 3 + member2 = 2 + end + order_bid = OrderBid.new( + source: 'APIv2', + state: ::Order::WAIT, + member_id: member1, + ask: 'gio', + bid: 'btc', + currency: 4, + ord_type: 'limit', + price: 1+rand(100)/10.0, + volume: 0.01, + origin_volume: 0.01 + ) + Ordering.new(order_bid).submit + order_ask = OrderAsk.new( + source: 'APIv2', + state: ::Order::WAIT, + member_id: member2, + ask: 'gio', + bid: 'btc', + currency: 4, + ord_type: 'limit', + price: 1+rand(100)/10.0, + volume: 0.01, + origin_volume: 0.01 + ) + Ordering.new(order_ask).submit + end + def create_cancel_thread Thread.new do loop do @@ -72,5 +109,14 @@ def create_cancel_thread end end + def create_test_thread + Thread.new do + loop do + sleep 5 + create_test_order + end + end + end + end end diff --git a/app/services/coin_rpc.rb b/app/services/coin_rpc.rb index 73fad12910..beded8731e 100644 --- a/app/services/coin_rpc.rb +++ b/app/services/coin_rpc.rb @@ -108,12 +108,13 @@ def safe_getbalance def getblockchaininfo @lastBlock = eth_getBlockByNumber("latest", true) - Rails.logger.info @lastBlock + #Rails.logger.info @lastBlock + #Rails.logger.info "number = " + Integer(@lastBlock[:number]).to_s + ", timestamp = " + Integer(@lastBlock[:timestamp]).to_s { - blocks: @lastBlock[:number].to_i(10), + blocks: Integer(@lastBlock[:number]), headers: 0, - mediantime: @lastBlock[:timestamp].to_i(10) + mediantime: Integer(@lastBlock[:timestamp]) } end diff --git a/app/views/private/markets/_ask_entry.html.slim b/app/views/private/markets/_ask_entry.html.slim index 4ba1ecbefa..50b1c44bb9 100644 --- a/app/views/private/markets/_ask_entry.html.slim +++ b/app/views/private/markets/_ask_entry.html.slim @@ -14,6 +14,7 @@ span.input-group-addon = t(market.ask['currency'], scope: 'market.currency') .input-group.total label.input-group-addon = t('.total') + span.label.label-info.hint-fee = f.text_field :total, class: 'form-control' span.input-group-addon = t(market.bid['currency'], scope: 'market.currency') = render partial: 'balance', locals: {currency: t(market.ask['currency'], scope: 'market.currency')} diff --git a/app/views/private/markets/_balance.html.slim b/app/views/private/markets/_balance.html.slim index d6afe1191e..7f01de0c54 100644 --- a/app/views/private/markets/_balance.html.slim +++ b/app/views/private/markets/_balance.html.slim @@ -3,5 +3,6 @@ span.label.label-info.status-info span.label.label-success.status-success span.label.label-danger.status-danger + span.label.label-info.status-fee span.current-balance.number span.unit = currency diff --git a/app/views/private/markets/_bid_entry.html.slim b/app/views/private/markets/_bid_entry.html.slim index ed2c355835..915564592f 100644 --- a/app/views/private/markets/_bid_entry.html.slim +++ b/app/views/private/markets/_bid_entry.html.slim @@ -14,6 +14,7 @@ .input-group.total label.input-group-addon = t('.total') span.label.label-info.hint-positions + span.label.label-info.hint-fee = f.text_field :total, class: 'form-control' span.input-group-addon = t(market.bid['currency'], scope: 'market.currency') = render partial: 'balance', locals: {currency: t(market.bid['currency'], scope: 'market.currency')} diff --git a/app/views/private/markets/_indicator_switch.html.slim b/app/views/private/markets/_indicator_switch.html.slim index d7e0c6fe0f..910e5b56e2 100644 --- a/app/views/private/markets/_indicator_switch.html.slim +++ b/app/views/private/markets/_indicator_switch.html.slim @@ -7,6 +7,9 @@ li: ul#main_indicator_switch.indicator_switch.list-inline data-x='MA' li.text-center: a.hand-point data-x='MA' href='#' MA li.text-center: a.hand-point data-x='EMA' href='#' EMA + li.text-center: a.hand-point data-x='OFF' href='#' x li.text-center: span | li: ul#indicator_switch.indicator_switch.list-inline data-x='MACD' li.text-center: a.hand-point data-x='MACD' href='#' MACD + li.text-center: a.hand-point data-x='OFF' href='#' x + diff --git a/app/views/private/markets/_market_list.html.slim b/app/views/private/markets/_market_list.html.slim index 0fb82b9fb9..9d2c486611 100644 --- a/app/views/private/markets/_market_list.html.slim +++ b/app/views/private/markets/_market_list.html.slim @@ -18,7 +18,7 @@ table.table.table-hover.markets.all tbody - @markets.each do |market| - tr.market id="market-list-#{market.id}" class="quote-#{market.quote_unit}" data-market=market.id + tr.market id="market-list-#{market.id}" class="quote-#{market.quote_unit}" style="font-size: 12px" data-market=market.id td.col-xs-4.name = link_to market.name, market_path(market) td.col-xs-15.price diff --git a/app/views/private/markets/_market_trades.html.slim b/app/views/private/markets/_market_trades.html.slim index d31dc8e443..81459c2e7c 100644 --- a/app/views/private/markets/_market_trades.html.slim +++ b/app/views/private/markets/_market_trades.html.slim @@ -10,7 +10,7 @@ thead: tr th.at.col-xs-5.text-left #{t('.time')} th.my.col-xs-1.text-left - th.price.col-xs-6.text-right #{t('.price')} + th[class='price col-xs-6 text-right' style='padding: 0 10px 0 0'] #{t('.price')} th.volume.col-xs-5.text-right #{t('.volume')} .panel-body.panel-body-content table.table.table-hover.all-trades diff --git a/app/views/private/markets/_order_book.html.slim b/app/views/private/markets/_order_book.html.slim index 99d93cac5f..0baedd22e9 100644 --- a/app/views/private/markets/_order_book.html.slim +++ b/app/views/private/markets/_order_book.html.slim @@ -3,13 +3,13 @@ #order_book_header.row .col-xs-12.col-left: table.table thead: tr - th.amount.text-left.col-xs-8 = t('.amount') - th.volume.text-left.col-xs-10 = t('.volume') - th.price.text-right.col-xs-12 = t('.bid') + th[class='amount text-right col-xs-6' style='padding: 0 15px 0 0'] = t('.amount') + th[class='volume text-right col-xs-6' style='padding: 0 3px 0 0'] = t('.volume') + th[class='price text-right col-xs-6' style='padding: 0 0 0 0'] = t('.bid') .col-xs-12: table.table.table-head thead: tr - th[class='price text-left col-xs-12' style='padding: 0 0 0 0'] = t('.ask') - th[class='volume text-left col-xs-10' style='padding: 0 0 0 0'] = t('.volume') + th[class='price text-right col-xs-7' style='padding: 0 0 0 0'] = t('.ask') + th[class='volume text-right col-xs-8' style='padding: 0 0 0 0'] = t('.volume') th.amount.text-right.col-xs-10 = t('.amount') .panel-body.panel-body-content #order_book_body.row diff --git a/config/initializers/pusher.rb b/config/initializers/pusher.rb index 067bbd6a15..01b1ef9115 100644 --- a/config/initializers/pusher.rb +++ b/config/initializers/pusher.rb @@ -1,5 +1,5 @@ Pusher.app_id = ENV['PUSHER_APP'] Pusher.key = ENV['PUSHER_KEY'] Pusher.secret = ENV['PUSHER_SECRET'] -Pusher.host = ENV['PUSHER_HOST'] || 'api.pusherapp.com' +Pusher.host = ENV['PUSHER_INNER_HOST'] || 'api.pusherapp.com' Pusher.port = ENV['PUSHER_PORT'].present? ? ENV['PUSHER_PORT'].to_i : 80 diff --git a/config/nginx.conf b/config/nginx.conf index e2dbd06952..6ebff5390e 100644 --- a/config/nginx.conf +++ b/config/nginx.conf @@ -35,6 +35,8 @@ # return 301 https://graviex.net$request_uri; #} +limit_conn_zone $binary_remote_addr zone=perip:10m; + server { # listen 443 ssl spdy default; # server_name graviex.net www.graviex.net; @@ -61,24 +63,27 @@ server { resolver_timeout 5s; location = /favicon.png { + limit_conn perip 3; expires max; add_header Cache-Control public; } location = /ZeroClipboard.swf { + limit_conn perip 3; expires max; add_header Cache-Control public; } location ~ ^/(assets)/ { + limit_conn perip 3; gzip_static on; expires max; add_header Cache-Control public; - } # disable gzip on all omniauth paths to prevent BREACH location ~ ^/auth/ { + limit_conn perip 3; gzip off; passenger_enabled on; } diff --git a/vendor/assets/javascripts/highstock.js b/vendor/assets/javascripts/highstock.js index 8b69546e11..3fd43cc03d 100644 --- a/vendor/assets/javascripts/highstock.js +++ b/vendor/assets/javascripts/highstock.js @@ -1,388 +1,43493 @@ +/** + * @license Highcharts JS v5aaca1890988fc7073f112a2ff1556645b8dda5e (2018-01-26) + * + * (c) 2009-2016 Torstein Honsi + * + * License: www.highcharts.com/license + */'use strict'; +(function (root, factory) { +if (typeof module === 'object' && module.exports) { +module.exports = root.document ? +factory(root) : +factory; +} else { +root.Highcharts = factory(root); +} +}(typeof window !== 'undefined' ? window : this, function (win) { +var Highcharts = (function () { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +/* global win, window */ + +// glob is a temporary fix to allow our es-modules to work. +var glob = typeof win === 'undefined' ? window : win, + doc = glob.document, + SVG_NS = 'http://www.w3.org/2000/svg', + userAgent = (glob.navigator && glob.navigator.userAgent) || '', + svg = doc && doc.createElementNS && !!doc.createElementNS(SVG_NS, 'svg').createSVGRect, + isMS = /(edge|msie|trident)/i.test(userAgent) && !glob.opera, + isFirefox = /Firefox/.test(userAgent), + hasBidiBug = isFirefox && parseInt(userAgent.split('Firefox/')[1], 10) < 4; // issue #38; + +var Highcharts = glob.Highcharts ? glob.Highcharts.error(16, true) : { + product: 'Highcharts', + version: '5aaca1890988fc7073f112a2ff1556645b8dda5e', + deg2rad: Math.PI * 2 / 360, + doc: doc, + hasBidiBug: hasBidiBug, + hasTouch: doc && doc.documentElement.ontouchstart !== undefined, + isMS: isMS, + isWebKit: /AppleWebKit/.test(userAgent), + isFirefox: isFirefox, + isTouchDevice: /(Mobile|Android|Windows Phone)/.test(userAgent), + SVG_NS: SVG_NS, + chartCount: 0, + seriesTypes: {}, + symbolSizes: {}, + svg: svg, + win: glob, + marginNames: ['plotTop', 'marginRight', 'marginBottom', 'plotLeft'], + noop: function () { + return undefined; + }, + /** + * An array containing the current chart objects in the page. A chart's + * position in the array is preserved throughout the page's lifetime. When + * a chart is destroyed, the array item becomes `undefined`. + * @type {Array.} + * @memberOf Highcharts + */ + charts: [] +}; +return Highcharts; +}()); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +/* eslint max-len: ["warn", 80, 4] */ + +/** + * The Highcharts object is the placeholder for all other members, and various + * utility functions. The most important member of the namespace would be the + * chart constructor. + * + * @example + * var chart = Highcharts.chart('container', { ... }); + * + * @namespace Highcharts + */ + +H.timers = []; + +var charts = H.charts, + doc = H.doc, + win = H.win; + +/** + * Provide error messages for debugging, with links to online explanation. This + * function can be overridden to provide custom error handling. + * + * @function #error + * @memberOf Highcharts + * @param {Number|String} code - The error code. See [errors.xml]{@link + * https://github.com/highcharts/highcharts/blob/master/errors/errors.xml} + * for available codes. If it is a string, the error message is printed + * directly in the console. + * @param {Boolean} [stop=false] - Whether to throw an error or just log a + * warning in the console. + * + * @sample highcharts/chart/highcharts-error/ Custom error handler + */ +H.error = function (code, stop) { + var msg = H.isNumber(code) ? + 'Highcharts error #' + code + ': www.highcharts.com/errors/' + code : + code; + if (stop) { + throw new Error(msg); + } + // else ... + if (win.console) { + console.log(msg); // eslint-disable-line no-console + } +}; + +/** + * An animator object used internally. One instance applies to one property + * (attribute or style prop) on one element. Animation is always initiated + * through {@link SVGElement#animate}. + * + * @constructor Fx + * @memberOf Highcharts + * @param {HTMLDOMElement|SVGElement} elem - The element to animate. + * @param {AnimationOptions} options - Animation options. + * @param {string} prop - The single attribute or CSS property to animate. + * @private + * + * @example + * var rect = renderer.rect(0, 0, 10, 10).add(); + * rect.animate({ width: 100 }); + */ +H.Fx = function (elem, options, prop) { + this.options = options; + this.elem = elem; + this.prop = prop; +}; +H.Fx.prototype = { + + /** + * Set the current step of a path definition on SVGElement. + * + * @function #dSetter + * @memberOf Highcharts.Fx + */ + dSetter: function () { + var start = this.paths[0], + end = this.paths[1], + ret = [], + now = this.now, + i = start.length, + startVal; + + // Land on the final path without adjustment points appended in the ends + if (now === 1) { + ret = this.toD; + + } else if (i === end.length && now < 1) { + while (i--) { + startVal = parseFloat(start[i]); + ret[i] = + isNaN(startVal) ? // a letter instruction like M or L + end[i] : + now * (parseFloat(end[i] - startVal)) + startVal; + + } + // If animation is finished or length not matching, land on right value + } else { + ret = end; + } + this.elem.attr('d', ret, null, true); + }, + + /** + * Update the element with the current animation step. + * + * @function #update + * @memberOf Highcharts.Fx + */ + update: function () { + var elem = this.elem, + prop = this.prop, // if destroyed, it is null + now = this.now, + step = this.options.step; + + // Animation setter defined from outside + if (this[prop + 'Setter']) { + this[prop + 'Setter'](); + + // Other animations on SVGElement + } else if (elem.attr) { + if (elem.element) { + elem.attr(prop, now, null, true); + } + + // HTML styles, raw HTML content like container size + } else { + elem.style[prop] = now + this.unit; + } + + if (step) { + step.call(elem, now, this); + } + + }, + + /** + * Run an animation. + * + * @function #run + * @memberOf Highcharts.Fx + * @param {Number} from - The current value, value to start from. + * @param {Number} to - The end value, value to land on. + * @param {String} [unit] - The property unit, for example `px`. + * + */ + run: function (from, to, unit) { + var self = this, + options = self.options, + timer = function (gotoEnd) { + return timer.stopped ? false : self.step(gotoEnd); + }, + requestAnimationFrame = + win.requestAnimationFrame || + function (step) { + setTimeout(step, 13); + }, + step = function () { + for (var i = 0; i < H.timers.length; i++) { + if (!H.timers[i]()) { + H.timers.splice(i--, 1); + } + } + + if (H.timers.length) { + requestAnimationFrame(step); + } + }; + + if (from === to) { + delete options.curAnim[this.prop]; + if (options.complete && H.keys(options.curAnim).length === 0) { + options.complete.call(this.elem); + } + } else { // #7166 + this.startTime = +new Date(); + this.start = from; + this.end = to; + this.unit = unit; + this.now = this.start; + this.pos = 0; + + timer.elem = this.elem; + timer.prop = this.prop; + + if (timer() && H.timers.push(timer) === 1) { + requestAnimationFrame(step); + } + } + }, + + /** + * Run a single step in the animation. + * + * @function #step + * @memberOf Highcharts.Fx + * @param {Boolean} [gotoEnd] - Whether to go to the endpoint of the + * animation after abort. + * @returns {Boolean} Returns `true` if animation continues. + */ + step: function (gotoEnd) { + var t = +new Date(), + ret, + done, + options = this.options, + elem = this.elem, + complete = options.complete, + duration = options.duration, + curAnim = options.curAnim; + + if (elem.attr && !elem.element) { // #2616, element is destroyed + ret = false; + + } else if (gotoEnd || t >= duration + this.startTime) { + this.now = this.end; + this.pos = 1; + this.update(); + + curAnim[this.prop] = true; + + done = true; + + H.objectEach(curAnim, function (val) { + if (val !== true) { + done = false; + } + }); + + if (done && complete) { + complete.call(elem); + } + ret = false; + + } else { + this.pos = options.easing((t - this.startTime) / duration); + this.now = this.start + ((this.end - this.start) * this.pos); + this.update(); + ret = true; + } + return ret; + }, + + /** + * Prepare start and end values so that the path can be animated one to one. + * + * @function #initPath + * @memberOf Highcharts.Fx + * @param {SVGElement} elem - The SVGElement item. + * @param {String} fromD - Starting path definition. + * @param {Array} toD - Ending path definition. + * @returns {Array} An array containing start and end paths in array form + * so that they can be animated in parallel. + */ + initPath: function (elem, fromD, toD) { + fromD = fromD || ''; + var shift, + startX = elem.startX, + endX = elem.endX, + bezier = fromD.indexOf('C') > -1, + numParams = bezier ? 7 : 3, + fullLength, + slice, + i, + start = fromD.split(' '), + end = toD.slice(), // copy + isArea = elem.isArea, + positionFactor = isArea ? 2 : 1, + reverse; + + /** + * In splines make moveTo and lineTo points have six parameters like + * bezier curves, to allow animation one-to-one. + */ + function sixify(arr) { + var isOperator, + nextIsOperator; + i = arr.length; + while (i--) { + + // Fill in dummy coordinates only if the next operator comes + // three places behind (#5788) + isOperator = arr[i] === 'M' || arr[i] === 'L'; + nextIsOperator = /[a-zA-Z]/.test(arr[i + 3]); + if (isOperator && nextIsOperator) { + arr.splice( + i + 1, 0, + arr[i + 1], arr[i + 2], + arr[i + 1], arr[i + 2] + ); + } + } + } + + /** + * Insert an array at the given position of another array + */ + function insertSlice(arr, subArr, index) { + [].splice.apply( + arr, + [index, 0].concat(subArr) + ); + } + + /** + * If shifting points, prepend a dummy point to the end path. + */ + function prepend(arr, other) { + while (arr.length < fullLength) { + + // Move to, line to or curve to? + arr[0] = other[fullLength - arr.length]; + + // Prepend a copy of the first point + insertSlice(arr, arr.slice(0, numParams), 0); + + // For areas, the bottom path goes back again to the left, so we + // need to append a copy of the last point. + if (isArea) { + insertSlice( + arr, + arr.slice(arr.length - numParams), arr.length + ); + i--; + } + } + arr[0] = 'M'; + } + + /** + * Copy and append last point until the length matches the end length + */ + function append(arr, other) { + var i = (fullLength - arr.length) / numParams; + while (i > 0 && i--) { + + // Pull out the slice that is going to be appended or inserted. + // In a line graph, the positionFactor is 1, and the last point + // is sliced out. In an area graph, the positionFactor is 2, + // causing the middle two points to be sliced out, since an area + // path starts at left, follows the upper path then turns and + // follows the bottom back. + slice = arr.slice().splice( + (arr.length / positionFactor) - numParams, + numParams * positionFactor + ); + + // Move to, line to or curve to? + slice[0] = other[fullLength - numParams - (i * numParams)]; + + // Disable first control point + if (bezier) { + slice[numParams - 6] = slice[numParams - 2]; + slice[numParams - 5] = slice[numParams - 1]; + } + + // Now insert the slice, either in the middle (for areas) or at + // the end (for lines) + insertSlice(arr, slice, arr.length / positionFactor); + + if (isArea) { + i--; + } + } + } + + if (bezier) { + sixify(start); + sixify(end); + } + + // For sideways animation, find out how much we need to shift to get the + // start path Xs to match the end path Xs. + if (startX && endX) { + for (i = 0; i < startX.length; i++) { + // Moving left, new points coming in on right + if (startX[i] === endX[0]) { + shift = i; + break; + // Moving right + } else if (startX[0] === + endX[endX.length - startX.length + i]) { + shift = i; + reverse = true; + break; + } + } + if (shift === undefined) { + start = []; + } + } + + if (start.length && H.isNumber(shift)) { + + // The common target length for the start and end array, where both + // arrays are padded in opposite ends + fullLength = end.length + shift * positionFactor * numParams; + + if (!reverse) { + prepend(end, start); + append(start, end); + } else { + prepend(start, end); + append(end, start); + } + } + + return [start, end]; + } +}; // End of Fx prototype + +/** + * Handle animation of the color attributes directly. + */ +H.Fx.prototype.fillSetter = +H.Fx.prototype.strokeSetter = function () { + this.elem.attr( + this.prop, + H.color(this.start).tweenTo(H.color(this.end), this.pos), + null, + true + ); +}; + + +/** + * Utility function to deep merge two or more objects and return a third object. + * If the first argument is true, the contents of the second object is copied + * into the first object. The merge function can also be used with a single + * object argument to create a deep copy of an object. + * + * @function #merge + * @memberOf Highcharts + * @param {Boolean} [extend] - Whether to extend the left-side object (a) or + return a whole new object. + * @param {Object} a - The first object to extend. When only this is given, the + function returns a deep copy. + * @param {...Object} [n] - An object to merge into the previous one. + * @returns {Object} - The merged object. If the first argument is true, the + * return is the same as the second argument. + */ +H.merge = function () { + var i, + args = arguments, + len, + ret = {}, + doCopy = function (copy, original) { + // An object is replacing a primitive + if (typeof copy !== 'object') { + copy = {}; + } + + H.objectEach(original, function (value, key) { + + // Copy the contents of objects, but not arrays or DOM nodes + if ( + H.isObject(value, true) && + !H.isClass(value) && + !H.isDOMElement(value) + ) { + copy[key] = doCopy(copy[key] || {}, value); + + // Primitives and arrays are copied over directly + } else { + copy[key] = original[key]; + } + }); + return copy; + }; + + // If first argument is true, copy into the existing object. Used in + // setOptions. + if (args[0] === true) { + ret = args[1]; + args = Array.prototype.slice.call(args, 2); + } + + // For each argument, extend the return + len = args.length; + for (i = 0; i < len; i++) { + ret = doCopy(ret, args[i]); + } + + return ret; +}; + +/** + * Shortcut for parseInt + * @ignore + * @param {Object} s + * @param {Number} mag Magnitude + */ +H.pInt = function (s, mag) { + return parseInt(s, mag || 10); +}; + +/** + * Utility function to check for string type. + * + * @function #isString + * @memberOf Highcharts + * @param {Object} s - The item to check. + * @returns {Boolean} - True if the argument is a string. + */ +H.isString = function (s) { + return typeof s === 'string'; +}; + +/** + * Utility function to check if an item is an array. + * + * @function #isArray + * @memberOf Highcharts + * @param {Object} obj - The item to check. + * @returns {Boolean} - True if the argument is an array. + */ +H.isArray = function (obj) { + var str = Object.prototype.toString.call(obj); + return str === '[object Array]' || str === '[object Array Iterator]'; +}; + +/** + * Utility function to check if an item is of type object. + * + * @function #isObject + * @memberOf Highcharts + * @param {Object} obj - The item to check. + * @param {Boolean} [strict=false] - Also checks that the object is not an + * array. + * @returns {Boolean} - True if the argument is an object. + */ +H.isObject = function (obj, strict) { + return !!obj && typeof obj === 'object' && (!strict || !H.isArray(obj)); +}; + +/** + * Utility function to check if an Object is a HTML Element. + * + * @function #isDOMElement + * @memberOf Highcharts + * @param {Object} obj - The item to check. + * @returns {Boolean} - True if the argument is a HTML Element. + */ +H.isDOMElement = function (obj) { + return H.isObject(obj) && typeof obj.nodeType === 'number'; +}; + +/** + * Utility function to check if an Object is an class. + * + * @function #isClass + * @memberOf Highcharts + * @param {Object} obj - The item to check. + * @returns {Boolean} - True if the argument is an class. + */ +H.isClass = function (obj) { + var c = obj && obj.constructor; + return !!( + H.isObject(obj, true) && + !H.isDOMElement(obj) && + (c && c.name && c.name !== 'Object') + ); +}; + +/** + * Utility function to check if an item is a number and it is finite (not NaN, + * Infinity or -Infinity). + * + * @function #isNumber + * @memberOf Highcharts + * @param {Object} n + * The item to check. + * @return {Boolean} + * True if the item is a finite number + */ +H.isNumber = function (n) { + return typeof n === 'number' && !isNaN(n) && n < Infinity && n > -Infinity; +}; + +/** + * Remove the last occurence of an item from an array. + * + * @function #erase + * @memberOf Highcharts + * @param {Array} arr - The array. + * @param {*} item - The item to remove. + */ +H.erase = function (arr, item) { + var i = arr.length; + while (i--) { + if (arr[i] === item) { + arr.splice(i, 1); + break; + } + } +}; + +/** + * Check if an object is null or undefined. + * + * @function #defined + * @memberOf Highcharts + * @param {Object} obj - The object to check. + * @returns {Boolean} - False if the object is null or undefined, otherwise + * true. + */ +H.defined = function (obj) { + return obj !== undefined && obj !== null; +}; + +/** + * Set or get an attribute or an object of attributes. To use as a setter, pass + * a key and a value, or let the second argument be a collection of keys and + * values. To use as a getter, pass only a string as the second argument. + * + * @function #attr + * @memberOf Highcharts + * @param {Object} elem - The DOM element to receive the attribute(s). + * @param {String|Object} [prop] - The property or an object of key-value pairs. + * @param {String} [value] - The value if a single property is set. + * @returns {*} When used as a getter, return the value. + */ +H.attr = function (elem, prop, value) { + var ret; + + // if the prop is a string + if (H.isString(prop)) { + // set the value + if (H.defined(value)) { + elem.setAttribute(prop, value); + + // get the value + } else if (elem && elem.getAttribute) { + ret = elem.getAttribute(prop); + } + + // else if prop is defined, it is a hash of key/value pairs + } else if (H.defined(prop) && H.isObject(prop)) { + H.objectEach(prop, function (val, key) { + elem.setAttribute(key, val); + }); + } + return ret; +}; + +/** + * Check if an element is an array, and if not, make it into an array. + * + * @function #splat + * @memberOf Highcharts + * @param obj {*} - The object to splat. + * @returns {Array} The produced or original array. + */ +H.splat = function (obj) { + return H.isArray(obj) ? obj : [obj]; +}; + +/** + * Set a timeout if the delay is given, otherwise perform the function + * synchronously. + * + * @function #syncTimeout + * @memberOf Highcharts + * @param {Function} fn - The function callback. + * @param {Number} delay - Delay in milliseconds. + * @param {Object} [context] - The context. + * @returns {Number} An identifier for the timeout that can later be cleared + * with clearTimeout. + */ +H.syncTimeout = function (fn, delay, context) { + if (delay) { + return setTimeout(fn, delay, context); + } + fn.call(0, context); +}; + + +/** + * Utility function to extend an object with the members of another. + * + * @function #extend + * @memberOf Highcharts + * @param {Object} a - The object to be extended. + * @param {Object} b - The object to add to the first one. + * @returns {Object} Object a, the original object. + */ +H.extend = function (a, b) { + var n; + if (!a) { + a = {}; + } + for (n in b) { + a[n] = b[n]; + } + return a; +}; + + +/** + * Return the first value that is not null or undefined. + * + * @function #pick + * @memberOf Highcharts + * @param {...*} items - Variable number of arguments to inspect. + * @returns {*} The value of the first argument that is not null or undefined. + */ +H.pick = function () { + var args = arguments, + i, + arg, + length = args.length; + for (i = 0; i < length; i++) { + arg = args[i]; + if (arg !== undefined && arg !== null) { + return arg; + } + } +}; + +/** + * @typedef {Object} CSSObject - A style object with camel case property names. + * The properties can be whatever styles are supported on the given SVG or HTML + * element. + * @example + * { + * fontFamily: 'monospace', + * fontSize: '1.2em' + * } + */ +/** + * Set CSS on a given element. + * + * @function #css + * @memberOf Highcharts + * @param {HTMLDOMElement} el - A HTML DOM element. + * @param {CSSObject} styles - Style object with camel case property names. + * + */ +H.css = function (el, styles) { + if (H.isMS && !H.svg) { // #2686 + if (styles && styles.opacity !== undefined) { + styles.filter = 'alpha(opacity=' + (styles.opacity * 100) + ')'; + } + } + H.extend(el.style, styles); +}; + +/** + * A HTML DOM element. + * @typedef {Object} HTMLDOMElement + */ + +/** + * Utility function to create an HTML element with attributes and styles. + * + * @function #createElement + * @memberOf Highcharts + * @param {String} tag - The HTML tag. + * @param {Object} [attribs] - Attributes as an object of key-value pairs. + * @param {CSSObject} [styles] - Styles as an object of key-value pairs. + * @param {Object} [parent] - The parent HTML object. + * @param {Boolean} [nopad=false] - If true, remove all padding, border and + * margin. + * @returns {HTMLDOMElement} The created DOM element. + */ +H.createElement = function (tag, attribs, styles, parent, nopad) { + var el = doc.createElement(tag), + css = H.css; + if (attribs) { + H.extend(el, attribs); + } + if (nopad) { + css(el, { padding: 0, border: 'none', margin: 0 }); + } + if (styles) { + css(el, styles); + } + if (parent) { + parent.appendChild(el); + } + return el; +}; + +/** + * Extend a prototyped class by new members. + * + * @function #extendClass + * @memberOf Highcharts + * @param {Object} parent - The parent prototype to inherit. + * @param {Object} members - A collection of prototype members to add or + * override compared to the parent prototype. + * @returns {Object} A new prototype. + */ +H.extendClass = function (parent, members) { + var object = function () {}; + object.prototype = new parent(); // eslint-disable-line new-cap + H.extend(object.prototype, members); + return object; +}; + +/** + * Left-pad a string to a given length by adding a character repetetively. + * + * @function #pad + * @memberOf Highcharts + * @param {Number} number - The input string or number. + * @param {Number} length - The desired string length. + * @param {String} [padder=0] - The character to pad with. + * @returns {String} The padded string. + */ +H.pad = function (number, length, padder) { + return new Array((length || 2) + 1 - + String(number).length).join(padder || 0) + number; +}; + +/** + * @typedef {Number|String} RelativeSize - If a number is given, it defines the + * pixel length. If a percentage string is given, like for example `'50%'`, + * the setting defines a length relative to a base size, for example the size + * of a container. + */ +/** + * Return a length based on either the integer value, or a percentage of a base. + * + * @function #relativeLength + * @memberOf Highcharts + * @param {RelativeSize} value + * A percentage string or a number. + * @param {number} base + * The full length that represents 100%. + * @param {number} [offset=0] + * A pixel offset to apply for percentage values. Used internally in + * axis positioning. + * @return {number} + * The computed length. + */ +H.relativeLength = function (value, base, offset) { + return (/%$/).test(value) ? + (base * parseFloat(value) / 100) + (offset || 0) : + parseFloat(value); +}; + +/** + * Wrap a method with extended functionality, preserving the original function. + * + * @function #wrap + * @memberOf Highcharts + * @param {Object} obj - The context object that the method belongs to. In real + * cases, this is often a prototype. + * @param {String} method - The name of the method to extend. + * @param {Function} func - A wrapper function callback. This function is called + * with the same arguments as the original function, except that the + * original function is unshifted and passed as the first argument. + * + */ +H.wrap = function (obj, method, func) { + var proceed = obj[method]; + obj[method] = function () { + var args = Array.prototype.slice.call(arguments), + outerArgs = arguments, + ctx = this, + ret; + ctx.proceed = function () { + proceed.apply(ctx, arguments.length ? arguments : outerArgs); + }; + args.unshift(proceed); + ret = func.apply(this, args); + ctx.proceed = null; + return ret; + }; +}; + + + +/** + * Format a single variable. Similar to sprintf, without the % prefix. + * + * @example + * formatSingle('.2f', 5); // => '5.00'. + * + * @function #formatSingle + * @memberOf Highcharts + * @param {String} format The format string. + * @param {*} val The value. + * @param {Time} [time] + * A `Time` instance that determines the date formatting, for example for + * applying time zone corrections to the formatted date. + + * @returns {String} The formatted representation of the value. + */ +H.formatSingle = function (format, val, time) { + var floatRegex = /f$/, + decRegex = /\.([0-9])/, + lang = H.defaultOptions.lang, + decimals; + + if (floatRegex.test(format)) { // float + decimals = format.match(decRegex); + decimals = decimals ? decimals[1] : -1; + if (val !== null) { + val = H.numberFormat( + val, + decimals, + lang.decimalPoint, + format.indexOf(',') > -1 ? lang.thousandsSep : '' + ); + } + } else { + val = (time || H.time).dateFormat(format, val); + } + return val; +}; + +/** + * Format a string according to a subset of the rules of Python's String.format + * method. + * + * @function #format + * @memberOf Highcharts + * @param {String} str + * The string to format. + * @param {Object} ctx + * The context, a collection of key-value pairs where each key is + * replaced by its value. + * @param {Time} [time] + * A `Time` instance that determines the date formatting, for example for + * applying time zone corrections to the formatted date. + * @returns {String} The formatted string. + * + * @example + * var s = Highcharts.format( + * 'The {color} fox was {len:.2f} feet long', + * { color: 'red', len: Math.PI } + * ); + * // => The red fox was 3.14 feet long + */ +H.format = function (str, ctx, time) { + var splitter = '{', + isInside = false, + segment, + valueAndFormat, + path, + i, + len, + ret = [], + val, + index; + + while (str) { + index = str.indexOf(splitter); + if (index === -1) { + break; + } + + segment = str.slice(0, index); + if (isInside) { // we're on the closing bracket looking back + + valueAndFormat = segment.split(':'); + path = valueAndFormat.shift().split('.'); // get first and leave + len = path.length; + val = ctx; + + // Assign deeper paths + for (i = 0; i < len; i++) { + if (val) { + val = val[path[i]]; + } + } + + // Format the replacement + if (valueAndFormat.length) { + val = H.formatSingle(valueAndFormat.join(':'), val, time); + } + + // Push the result and advance the cursor + ret.push(val); + + } else { + ret.push(segment); + + } + str = str.slice(index + 1); // the rest + isInside = !isInside; // toggle + splitter = isInside ? '}' : '{'; // now look for next matching bracket + } + ret.push(str); + return ret.join(''); +}; + +/** + * Get the magnitude of a number. + * + * @function #getMagnitude + * @memberOf Highcharts + * @param {Number} number The number. + * @returns {Number} The magnitude, where 1-9 are magnitude 1, 10-99 magnitude 2 + * etc. + */ +H.getMagnitude = function (num) { + return Math.pow(10, Math.floor(Math.log(num) / Math.LN10)); +}; + +/** + * Take an interval and normalize it to multiples of round numbers. + * + * @todo Move this function to the Axis prototype. It is here only for + * historical reasons. + * @function #normalizeTickInterval + * @memberOf Highcharts + * @param {Number} interval - The raw, un-rounded interval. + * @param {Array} [multiples] - Allowed multiples. + * @param {Number} [magnitude] - The magnitude of the number. + * @param {Boolean} [allowDecimals] - Whether to allow decimals. + * @param {Boolean} [hasTickAmount] - If it has tickAmount, avoid landing + * on tick intervals lower than original. + * @returns {Number} The normalized interval. + */ +H.normalizeTickInterval = function (interval, multiples, magnitude, + allowDecimals, hasTickAmount) { + var normalized, + i, + retInterval = interval; + + // round to a tenfold of 1, 2, 2.5 or 5 + magnitude = H.pick(magnitude, 1); + normalized = interval / magnitude; + + // multiples for a linear scale + if (!multiples) { + multiples = hasTickAmount ? + // Finer grained ticks when the tick amount is hard set, including + // when alignTicks is true on multiple axes (#4580). + [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10] : + + // Else, let ticks fall on rounder numbers + [1, 2, 2.5, 5, 10]; + + + // the allowDecimals option + if (allowDecimals === false) { + if (magnitude === 1) { + multiples = H.grep(multiples, function (num) { + return num % 1 === 0; + }); + } else if (magnitude <= 0.1) { + multiples = [1 / magnitude]; + } + } + } + + // normalize the interval to the nearest multiple + for (i = 0; i < multiples.length; i++) { + retInterval = multiples[i]; + // only allow tick amounts smaller than natural + if ((hasTickAmount && retInterval * magnitude >= interval) || + (!hasTickAmount && (normalized <= (multiples[i] + + (multiples[i + 1] || multiples[i])) / 2))) { + break; + } + } + + // Multiply back to the correct magnitude. Correct floats to appropriate + // precision (#6085). + retInterval = H.correctFloat( + retInterval * magnitude, + -Math.round(Math.log(0.001) / Math.LN10) + ); + + return retInterval; +}; + + +/** + * Sort an object array and keep the order of equal items. The ECMAScript + * standard does not specify the behaviour when items are equal. + * + * @function #stableSort + * @memberOf Highcharts + * @param {Array} arr - The array to sort. + * @param {Function} sortFunction - The function to sort it with, like with + * regular Array.prototype.sort. + * + */ +H.stableSort = function (arr, sortFunction) { + var length = arr.length, + sortValue, + i; + + // Add index to each item + for (i = 0; i < length; i++) { + arr[i].safeI = i; // stable sort index + } + + arr.sort(function (a, b) { + sortValue = sortFunction(a, b); + return sortValue === 0 ? a.safeI - b.safeI : sortValue; + }); + + // Remove index from items + for (i = 0; i < length; i++) { + delete arr[i].safeI; // stable sort index + } +}; + +/** + * Non-recursive method to find the lowest member of an array. `Math.min` raises + * a maximum call stack size exceeded error in Chrome when trying to apply more + * than 150.000 points. This method is slightly slower, but safe. + * + * @function #arrayMin + * @memberOf Highcharts + * @param {Array} data An array of numbers. + * @returns {Number} The lowest number. + */ +H.arrayMin = function (data) { + var i = data.length, + min = data[0]; + + while (i--) { + if (data[i] < min) { + min = data[i]; + } + } + return min; +}; + +/** + * Non-recursive method to find the lowest member of an array. `Math.max` raises + * a maximum call stack size exceeded error in Chrome when trying to apply more + * than 150.000 points. This method is slightly slower, but safe. + * + * @function #arrayMax + * @memberOf Highcharts + * @param {Array} data - An array of numbers. + * @returns {Number} The highest number. + */ +H.arrayMax = function (data) { + var i = data.length, + max = data[0]; + + while (i--) { + if (data[i] > max) { + max = data[i]; + } + } + return max; +}; + +/** + * Utility method that destroys any SVGElement instances that are properties on + * the given object. It loops all properties and invokes destroy if there is a + * destroy method. The property is then delete. + * + * @function #destroyObjectProperties + * @memberOf Highcharts + * @param {Object} obj - The object to destroy properties on. + * @param {Object} [except] - Exception, do not destroy this property, only + * delete it. + * + */ +H.destroyObjectProperties = function (obj, except) { + H.objectEach(obj, function (val, n) { + // If the object is non-null and destroy is defined + if (val && val !== except && val.destroy) { + // Invoke the destroy + val.destroy(); + } + + // Delete the property from the object. + delete obj[n]; + }); +}; + + +/** + * Discard a HTML element by moving it to the bin and delete. + * + * @function #discardElement + * @memberOf Highcharts + * @param {HTMLDOMElement} element - The HTML node to discard. + * + */ +H.discardElement = function (element) { + var garbageBin = H.garbageBin; + // create a garbage bin element, not part of the DOM + if (!garbageBin) { + garbageBin = H.createElement('div'); + } + + // move the node and empty bin + if (element) { + garbageBin.appendChild(element); + } + garbageBin.innerHTML = ''; +}; + +/** + * Fix JS round off float errors. + * + * @function #correctFloat + * @memberOf Highcharts + * @param {Number} num - A float number to fix. + * @param {Number} [prec=14] - The precision. + * @returns {Number} The corrected float number. + */ +H.correctFloat = function (num, prec) { + return parseFloat( + num.toPrecision(prec || 14) + ); +}; + +/** + * Set the global animation to either a given value, or fall back to the given + * chart's animation option. + * + * @function #setAnimation + * @memberOf Highcharts + * @param {Boolean|Animation} animation - The animation object. + * @param {Object} chart - The chart instance. + * + * @todo This function always relates to a chart, and sets a property on the + * renderer, so it should be moved to the SVGRenderer. + */ +H.setAnimation = function (animation, chart) { + chart.renderer.globalAnimation = H.pick( + animation, + chart.options.chart.animation, + true + ); +}; + +/** + * Get the animation in object form, where a disabled animation is always + * returned as `{ duration: 0 }`. + * + * @function #animObject + * @memberOf Highcharts + * @param {Boolean|AnimationOptions} animation - An animation setting. Can be an + * object with duration, complete and easing properties, or a boolean to + * enable or disable. + * @returns {AnimationOptions} An object with at least a duration property. + */ +H.animObject = function (animation) { + return H.isObject(animation) ? + H.merge(animation) : + { duration: animation ? 500 : 0 }; +}; + +/** + * The time unit lookup + */ +H.timeUnits = { + millisecond: 1, + second: 1000, + minute: 60000, + hour: 3600000, + day: 24 * 3600000, + week: 7 * 24 * 3600000, + month: 28 * 24 * 3600000, + year: 364 * 24 * 3600000 +}; + +/** + * Format a number and return a string based on input settings. + * + * @function #numberFormat + * @memberOf Highcharts + * @param {Number} number - The input number to format. + * @param {Number} decimals - The amount of decimals. A value of -1 preserves + * the amount in the input number. + * @param {String} [decimalPoint] - The decimal point, defaults to the one given + * in the lang options, or a dot. + * @param {String} [thousandsSep] - The thousands separator, defaults to the one + * given in the lang options, or a space character. + * @returns {String} The formatted number. + * + * @sample highcharts/members/highcharts-numberformat/ Custom number format + */ +H.numberFormat = function (number, decimals, decimalPoint, thousandsSep) { + number = +number || 0; + decimals = +decimals; + + var lang = H.defaultOptions.lang, + origDec = (number.toString().split('.')[1] || '').split('e')[0].length, + strinteger, + thousands, + ret, + roundedNumber, + exponent = number.toString().split('e'), + fractionDigits; + + if (decimals === -1) { + // Preserve decimals. Not huge numbers (#3793). + decimals = Math.min(origDec, 20); + } else if (!H.isNumber(decimals)) { + decimals = 2; + } else if (decimals && exponent[1] && exponent[1] < 0) { + // Expose decimals from exponential notation (#7042) + fractionDigits = decimals + +exponent[1]; + if (fractionDigits >= 0) { + // remove too small part of the number while keeping the notation + exponent[0] = (+exponent[0]).toExponential(fractionDigits) + .split('e')[0]; + decimals = fractionDigits; + } else { + // fractionDigits < 0 + exponent[0] = exponent[0].split('.')[0] || 0; + + if (decimals < 20) { + // use number instead of exponential notation (#7405) + number = (exponent[0] * Math.pow(10, exponent[1])) + .toFixed(decimals); + } else { + // or zero + number = 0; + } + exponent[1] = 0; + } + } + + // Add another decimal to avoid rounding errors of float numbers. (#4573) + // Then use toFixed to handle rounding. + roundedNumber = ( + Math.abs(exponent[1] ? exponent[0] : number) + + Math.pow(10, -Math.max(decimals, origDec) - 1) + ).toFixed(decimals); + + // A string containing the positive integer component of the number + strinteger = String(H.pInt(roundedNumber)); + + // Leftover after grouping into thousands. Can be 0, 1 or 3. + thousands = strinteger.length > 3 ? strinteger.length % 3 : 0; + + // Language + decimalPoint = H.pick(decimalPoint, lang.decimalPoint); + thousandsSep = H.pick(thousandsSep, lang.thousandsSep); + + // Start building the return + ret = number < 0 ? '-' : ''; + + // Add the leftover after grouping into thousands. For example, in the + // number 42 000 000, this line adds 42. + ret += thousands ? strinteger.substr(0, thousands) + thousandsSep : ''; + + // Add the remaining thousands groups, joined by the thousands separator + ret += strinteger + .substr(thousands) + .replace(/(\d{3})(?=\d)/g, '$1' + thousandsSep); + + // Add the decimal point and the decimal component + if (decimals) { + // Get the decimal component + ret += decimalPoint + roundedNumber.slice(-decimals); + } + + if (exponent[1] && +ret !== 0) { + ret += 'e' + exponent[1]; + } + + return ret; +}; + +/** + * Easing definition + * @ignore + * @param {Number} pos Current position, ranging from 0 to 1. + */ +Math.easeInOutSine = function (pos) { + return -0.5 * (Math.cos(Math.PI * pos) - 1); +}; + +/** + * Get the computed CSS value for given element and property, only for numerical + * properties. For width and height, the dimension of the inner box (excluding + * padding) is returned. Used for fitting the chart within the container. + * + * @function #getStyle + * @memberOf Highcharts + * @param {HTMLDOMElement} el - A HTML element. + * @param {String} prop - The property name. + * @param {Boolean} [toInt=true] - Parse to integer. + * @returns {Number} - The numeric value. + */ +H.getStyle = function (el, prop, toInt) { + + var style; + + // For width and height, return the actual inner pixel size (#4913) + if (prop === 'width') { + return Math.min(el.offsetWidth, el.scrollWidth) - + H.getStyle(el, 'padding-left') - + H.getStyle(el, 'padding-right'); + } else if (prop === 'height') { + return Math.min(el.offsetHeight, el.scrollHeight) - + H.getStyle(el, 'padding-top') - + H.getStyle(el, 'padding-bottom'); + } + + if (!win.getComputedStyle) { + // SVG not supported, forgot to load oldie.js? + H.error(27, true); + } + + // Otherwise, get the computed style + style = win.getComputedStyle(el, undefined); + if (style) { + style = style.getPropertyValue(prop); + if (H.pick(toInt, prop !== 'opacity')) { + style = H.pInt(style); + } + } + return style; +}; + +/** + * Search for an item in an array. + * + * @function #inArray + * @memberOf Highcharts + * @param {*} item - The item to search for. + * @param {arr} arr - The array or node collection to search in. + * @returns {Number} - The index within the array, or -1 if not found. + */ +H.inArray = function (item, arr) { + return (H.indexOfPolyfill || Array.prototype.indexOf).call(arr, item); +}; + +/** + * Filter an array by a callback. + * + * @function #grep + * @memberOf Highcharts + * @param {Array} arr - The array to filter. + * @param {Function} callback - The callback function. The function receives the + * item as the first argument. Return `true` if the item is to be + * preserved. + * @returns {Array} - A new, filtered array. + */ +H.grep = function (arr, callback) { + return (H.filterPolyfill || Array.prototype.filter).call(arr, callback); +}; + +/** + * Return the value of the first element in the array that satisfies the + * provided testing function. + * + * @function #find + * @memberOf Highcharts + * @param {Array} arr - The array to test. + * @param {Function} callback - The callback function. The function receives the + * item as the first argument. Return `true` if this item satisfies the + * condition. + * @returns {Mixed} - The value of the element. + */ +H.find = Array.prototype.find ? + function (arr, callback) { + return arr.find(callback); + } : + // Legacy implementation. PhantomJS, IE <= 11 etc. #7223. + function (arr, fn) { + var i, + length = arr.length; + + for (i = 0; i < length; i++) { + if (fn(arr[i], i)) { + return arr[i]; + } + } + }; + +/** + * Map an array by a callback. + * + * @function #map + * @memberOf Highcharts + * @param {Array} arr - The array to map. + * @param {Function} fn - The callback function. Return the new value for the + * new array. + * @returns {Array} - A new array item with modified items. + */ +H.map = function (arr, fn) { + var results = [], + i = 0, + len = arr.length; + + for (; i < len; i++) { + results[i] = fn.call(arr[i], arr[i], i, arr); + } + + return results; +}; + +/** + * Returns an array of a given object's own properties. + * + * @function #keys + * @memberOf highcharts + * @param {Object} obj - The object of which the properties are to be returned. + * @returns {Array} - An array of strings that represents all the properties. + */ +H.keys = function (obj) { + return (H.keysPolyfill || Object.keys).call(undefined, obj); +}; + +/** + * Reduce an array to a single value. + * + * @function #reduce + * @memberOf Highcharts + * @param {Array} arr - The array to reduce. + * @param {Function} fn - The callback function. Return the reduced value. + * Receives 4 arguments: Accumulated/reduced value, current value, current + * array index, and the array. + * @param {Mixed} initialValue - The initial value of the accumulator. + * @returns {Mixed} - The reduced value. + */ +H.reduce = function (arr, func, initialValue) { + return (H.reducePolyfill || Array.prototype.reduce).call( + arr, + func, + initialValue + ); +}; + +/** + * Get the element's offset position, corrected for `overflow: auto`. + * + * @function #offset + * @memberOf Highcharts + * @param {HTMLDOMElement} el - The HTML element. + * @returns {Object} An object containing `left` and `top` properties for the + * position in the page. + */ +H.offset = function (el) { + var docElem = doc.documentElement, + box = el.parentElement ? // IE11 throws Unspecified error in test suite + el.getBoundingClientRect() : + { top: 0, left: 0 }; + + return { + top: box.top + (win.pageYOffset || docElem.scrollTop) - + (docElem.clientTop || 0), + left: box.left + (win.pageXOffset || docElem.scrollLeft) - + (docElem.clientLeft || 0) + }; +}; + +/** + * Stop running animation. + * + * @todo A possible extension to this would be to stop a single property, when + * we want to continue animating others. Then assign the prop to the timer + * in the Fx.run method, and check for the prop here. This would be an + * improvement in all cases where we stop the animation from .attr. Instead of + * stopping everything, we can just stop the actual attributes we're setting. + * + * @function #stop + * @memberOf Highcharts + * @param {SVGElement} el - The SVGElement to stop animation on. + * @param {string} [prop] - The property to stop animating. If given, the stop + * method will stop a single property from animating, while others continue. + * + */ +H.stop = function (el, prop) { + + var i = H.timers.length; + + // Remove timers related to this element (#4519) + while (i--) { + if (H.timers[i].elem === el && (!prop || prop === H.timers[i].prop)) { + H.timers[i].stopped = true; // #4667 + } + } +}; + +/** + * Iterate over an array. + * + * @function #each + * @memberOf Highcharts + * @param {Array} arr - The array to iterate over. + * @param {Function} fn - The iterator callback. It passes three arguments: + * * item - The array item. + * * index - The item's index in the array. + * * arr - The array that each is being applied to. + * @param {Object} [ctx] The context. + */ +H.each = function (arr, fn, ctx) { // modern browsers + return (H.forEachPolyfill || Array.prototype.forEach).call(arr, fn, ctx); +}; + +/** + * Iterate over object key pairs in an object. + * + * @function #objectEach + * @memberOf Highcharts + * @param {Object} obj - The object to iterate over. + * @param {Function} fn - The iterator callback. It passes three arguments: + * * value - The property value. + * * key - The property key. + * * obj - The object that objectEach is being applied to. + * @param {Object} ctx The context + */ +H.objectEach = function (obj, fn, ctx) { + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + fn.call(ctx, obj[key], key, obj); + } + } +}; + +/** + * Add an event listener. + * + * @function #addEvent + * @memberOf Highcharts + * @param {Object} el - The element or object to add a listener to. It can be a + * {@link HTMLDOMElement}, an {@link SVGElement} or any other object. + * @param {String} type - The event type. + * @param {Function} fn - The function callback to execute when the event is + * fired. + * @returns {Function} A callback function to remove the added event. + */ +H.addEvent = function (el, type, fn) { + + var events, + itemEvents, + addEventListener = el.addEventListener || H.addEventListenerPolyfill; + + // If events are previously set directly on the prototype, pick them up + // and copy them over to the instance. Otherwise instance handlers would + // be set on the prototype and apply to multiple charts in the page. + if ( + el.hcEvents && + // IE8, window and document don't have hasOwnProperty + !Object.prototype.hasOwnProperty.call(el, 'hcEvents') + ) { + itemEvents = {}; + H.objectEach(el.hcEvents, function (handlers, eventType) { + itemEvents[eventType] = handlers.slice(0); + }); + el.hcEvents = itemEvents; + } + + events = el.hcEvents = el.hcEvents || {}; + + // Handle DOM events + if (addEventListener) { + addEventListener.call(el, type, fn, false); + } + + if (!events[type]) { + events[type] = []; + } + + events[type].push(fn); + + // Return a function that can be called to remove this event. + return function () { + H.removeEvent(el, type, fn); + }; +}; + +/** + * Remove an event that was added with {@link Highcharts#addEvent}. + * + * @function #removeEvent + * @memberOf Highcharts + * @param {Object} el - The element to remove events on. + * @param {String} [type] - The type of events to remove. If undefined, all + * events are removed from the element. + * @param {Function} [fn] - The specific callback to remove. If undefined, all + * events that match the element and optionally the type are removed. + * + */ +H.removeEvent = function (el, type, fn) { + + var events, + hcEvents = el.hcEvents, + index; + + function removeOneEvent(type, fn) { + var removeEventListener = + el.removeEventListener || H.removeEventListenerPolyfill; + + if (removeEventListener) { + removeEventListener.call(el, type, fn, false); + } + } + + function removeAllEvents() { + var types, + len; + + if (!el.nodeName) { + return; // break on non-DOM events + } + + if (type) { + types = {}; + types[type] = true; + } else { + types = hcEvents; + } + + H.objectEach(types, function (val, n) { + if (hcEvents[n]) { + len = hcEvents[n].length; + while (len--) { + removeOneEvent(n, hcEvents[n][len]); + } + } + }); + } + + if (hcEvents) { + if (type) { + events = hcEvents[type] || []; + if (fn) { + index = H.inArray(fn, events); + if (index > -1) { + events.splice(index, 1); + hcEvents[type] = events; + } + removeOneEvent(type, fn); + + } else { + removeAllEvents(); + hcEvents[type] = []; + } + } else { + removeAllEvents(); + el.hcEvents = {}; + } + } +}; + +/** + * Fire an event that was registered with {@link Highcharts#addEvent}. + * + * @function #fireEvent + * @memberOf Highcharts + * @param {Object} el - The object to fire the event on. It can be a + * {@link HTMLDOMElement}, an {@link SVGElement} or any other object. + * @param {String} type - The type of event. + * @param {Object} [eventArguments] - Custom event arguments that are passed on + * as an argument to the event handler. + * @param {Function} [defaultFunction] - The default function to execute if the + * other listeners haven't returned false. + * + */ +H.fireEvent = function (el, type, eventArguments, defaultFunction) { + var e, + hcEvents = el.hcEvents, + events, + len, + i, + fn; + + eventArguments = eventArguments || {}; + + if (doc.createEvent && (el.dispatchEvent || el.fireEvent)) { + e = doc.createEvent('Events'); + e.initEvent(type, true, true); + + H.extend(e, eventArguments); + + if (el.dispatchEvent) { + el.dispatchEvent(e); + } else { + el.fireEvent(type, e); + } + + } else if (hcEvents) { + + events = hcEvents[type] || []; + len = events.length; + + if (!eventArguments.target) { // We're running a custom event + + H.extend(eventArguments, { + // Attach a simple preventDefault function to skip default + // handler if called. The built-in defaultPrevented property is + // not overwritable (#5112) + preventDefault: function () { + eventArguments.defaultPrevented = true; + }, + // Setting target to native events fails with clicking the + // zoom-out button in Chrome. + target: el, + // If the type is not set, we're running a custom event (#2297). + // If it is set, we're running a browser event, and setting it + // will cause en error in IE8 (#2465). + type: type + }); + } + + + for (i = 0; i < len; i++) { + fn = events[i]; + + // If the event handler return false, prevent the default handler + // from executing + if (fn && fn.call(el, eventArguments) === false) { + eventArguments.preventDefault(); + } + } + } + + // Run the default if not prevented + if (defaultFunction && !eventArguments.defaultPrevented) { + defaultFunction(eventArguments); + } +}; + +/** + * An animation configuration. Animation configurations can also be defined as + * booleans, where `false` turns off animation and `true` defaults to a duration + * of 500ms. + * @typedef {Object} AnimationOptions + * @property {Number} duration - The animation duration in milliseconds. + * @property {String} [easing] - The name of an easing function as defined on + * the `Math` object. + * @property {Function} [complete] - A callback function to exectute when the + * animation finishes. + * @property {Function} [step] - A callback function to execute on each step of + * each attribute or CSS property that's being animated. The first argument + * contains information about the animation and progress. + */ + + +/** + * The global animate method, which uses Fx to create individual animators. + * + * @function #animate + * @memberOf Highcharts + * @param {HTMLDOMElement|SVGElement} el - The element to animate. + * @param {Object} params - An object containing key-value pairs of the + * properties to animate. Supports numeric as pixel-based CSS properties + * for HTML objects and attributes for SVGElements. + * @param {AnimationOptions} [opt] - Animation options. + */ +H.animate = function (el, params, opt) { + var start, + unit = '', + end, + fx, + args; + + if (!H.isObject(opt)) { // Number or undefined/null + args = arguments; + opt = { + duration: args[2], + easing: args[3], + complete: args[4] + }; + } + if (!H.isNumber(opt.duration)) { + opt.duration = 400; + } + opt.easing = typeof opt.easing === 'function' ? + opt.easing : + (Math[opt.easing] || Math.easeInOutSine); + opt.curAnim = H.merge(params); + + H.objectEach(params, function (val, prop) { + // Stop current running animation of this property + H.stop(el, prop); + + fx = new H.Fx(el, opt, prop); + end = null; + + if (prop === 'd') { + fx.paths = fx.initPath( + el, + el.d, + params.d + ); + fx.toD = params.d; + start = 0; + end = 1; + } else if (el.attr) { + start = el.attr(prop); + } else { + start = parseFloat(H.getStyle(el, prop)) || 0; + if (prop !== 'opacity') { + unit = 'px'; + } + } + + if (!end) { + end = val; + } + if (end && end.match && end.match('px')) { + end = end.replace(/px/g, ''); // #4351 + } + fx.run(start, end, unit); + }); +}; + +/** + * Factory to create new series prototypes. + * + * @function #seriesType + * @memberOf Highcharts + * + * @param {String} type - The series type name. + * @param {String} parent - The parent series type name. Use `line` to inherit + * from the basic {@link Series} object. + * @param {Object} options - The additional default options that is merged with + * the parent's options. + * @param {Object} props - The properties (functions and primitives) to set on + * the new prototype. + * @param {Object} [pointProps] - Members for a series-specific extension of the + * {@link Point} prototype if needed. + * @returns {*} - The newly created prototype as extended from {@link Series} + * or its derivatives. + */ +// docs: add to API + extending Highcharts +H.seriesType = function (type, parent, options, props, pointProps) { + var defaultOptions = H.getOptions(), + seriesTypes = H.seriesTypes; + + // Merge the options + defaultOptions.plotOptions[type] = H.merge( + defaultOptions.plotOptions[parent], + options + ); + + // Create the class + seriesTypes[type] = H.extendClass(seriesTypes[parent] || + function () {}, props); + seriesTypes[type].prototype.type = type; + + // Create the point class if needed + if (pointProps) { + seriesTypes[type].prototype.pointClass = + H.extendClass(H.Point, pointProps); + } + + return seriesTypes[type]; +}; + +/** + * Get a unique key for using in internal element id's and pointers. The key + * is composed of a random hash specific to this Highcharts instance, and a + * counter. + * @function #uniqueKey + * @memberOf Highcharts + * @return {string} The key. + * @example + * var id = H.uniqueKey(); // => 'highcharts-x45f6hp-0' + */ +H.uniqueKey = (function () { + + var uniqueKeyHash = Math.random().toString(36).substring(2, 9), + idCounter = 0; + + return function () { + return 'highcharts-' + uniqueKeyHash + '-' + idCounter++; + }; +}()); + +/** + * Register Highcharts as a plugin in jQuery + */ +if (win.jQuery) { + win.jQuery.fn.highcharts = function () { + var args = [].slice.call(arguments); + + if (this[0]) { // this[0] is the renderTo div + + // Create the chart + if (args[0]) { + new H[ // eslint-disable-line no-new + // Constructor defaults to Chart + H.isString(args[0]) ? args.shift() : 'Chart' + ](this[0], args[0], args[1]); + return this; + } + + // When called without parameters or with the return argument, + // return an existing chart + return charts[H.attr(this[0], 'data-highcharts-chart')]; + } + }; +} + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var each = H.each, + isNumber = H.isNumber, + map = H.map, + merge = H.merge, + pInt = H.pInt; + +/** + * @typedef {string} ColorString + * A valid color to be parsed and handled by Highcharts. Highcharts internally + * supports hex colors like `#ffffff`, rgb colors like `rgb(255,255,255)` and + * rgba colors like `rgba(255,255,255,1)`. Other colors may be supported by the + * browsers and displayed correctly, but Highcharts is not able to process them + * and apply concepts like opacity and brightening. + */ +/** + * Handle color operations. The object methods are chainable. + * @param {String} input The input color in either rbga or hex format + */ +H.Color = function (input) { + // Backwards compatibility, allow instanciation without new + if (!(this instanceof H.Color)) { + return new H.Color(input); + } + // Initialize + this.init(input); +}; +H.Color.prototype = { + + // Collection of parsers. This can be extended from the outside by pushing parsers + // to Highcharts.Color.prototype.parsers. + parsers: [{ + // RGBA color + regex: /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/, + parse: function (result) { + return [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; + } + }, { + // RGB color + regex: /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/, + parse: function (result) { + return [pInt(result[1]), pInt(result[2]), pInt(result[3]), 1]; + } + }], + + // Collection of named colors. Can be extended from the outside by adding + // colors to Highcharts.Color.prototype.names. + names: { + none: 'rgba(255,255,255,0)', + white: '#ffffff', + black: '#000000' + }, + + /** + * Parse the input color to rgba array + * @param {String} input + */ + init: function (input) { + var result, + rgba, + i, + parser, + len; + + this.input = input = this.names[ + input && input.toLowerCase ? + input.toLowerCase() : + '' + ] || input; + + // Gradients + if (input && input.stops) { + this.stops = map(input.stops, function (stop) { + return new H.Color(stop[1]); + }); + + // Solid colors + } else { + + // Bitmasking as input[0] is not working for legacy IE. + if (input && input.charAt && input.charAt() === '#') { + + len = input.length; + input = parseInt(input.substr(1), 16); + + // Handle long-form, e.g. #AABBCC + if (len === 7) { + + rgba = [ + (input & 0xFF0000) >> 16, + (input & 0xFF00) >> 8, + (input & 0xFF), + 1 + ]; + + // Handle short-form, e.g. #ABC + // In short form, the value is assumed to be the same + // for both nibbles for each component. e.g. #ABC = #AABBCC + } else if (len === 4) { + + rgba = [ + ((input & 0xF00) >> 4) | (input & 0xF00) >> 8, + ((input & 0xF0) >> 4) | (input & 0xF0), + ((input & 0xF) << 4) | (input & 0xF), + 1 + ]; + } + } + + // Otherwise, check regex parsers + if (!rgba) { + i = this.parsers.length; + while (i-- && !rgba) { + parser = this.parsers[i]; + result = parser.regex.exec(input); + if (result) { + rgba = parser.parse(result); + } + } + } + } + this.rgba = rgba || []; + }, + + /** + * Return the color a specified format + * @param {String} format + */ + get: function (format) { + var input = this.input, + rgba = this.rgba, + ret; + + if (this.stops) { + ret = merge(input); + ret.stops = [].concat(ret.stops); + each(this.stops, function (stop, i) { + ret.stops[i] = [ret.stops[i][0], stop.get(format)]; + }); + + // it's NaN if gradient colors on a column chart + } else if (rgba && isNumber(rgba[0])) { + if (format === 'rgb' || (!format && rgba[3] === 1)) { + ret = 'rgb(' + rgba[0] + ',' + rgba[1] + ',' + rgba[2] + ')'; + } else if (format === 'a') { + ret = rgba[3]; + } else { + ret = 'rgba(' + rgba.join(',') + ')'; + } + } else { + ret = input; + } + return ret; + }, + + /** + * Brighten the color + * @param {Number} alpha + */ + brighten: function (alpha) { + var i, + rgba = this.rgba; + + if (this.stops) { + each(this.stops, function (stop) { + stop.brighten(alpha); + }); + + } else if (isNumber(alpha) && alpha !== 0) { + for (i = 0; i < 3; i++) { + rgba[i] += pInt(alpha * 255); + + if (rgba[i] < 0) { + rgba[i] = 0; + } + if (rgba[i] > 255) { + rgba[i] = 255; + } + } + } + return this; + }, + + /** + * Set the color's opacity to a given alpha value + * @param {Number} alpha + */ + setOpacity: function (alpha) { + this.rgba[3] = alpha; + return this; + }, + + /* + * Return an intermediate color between two colors. + * + * @param {Highcharts.Color} to + * The color object to tween to. + * @param {Number} pos + * The intermediate position, where 0 is the from color (current + * color item), and 1 is the `to` color. + * + * @return {String} + * The intermediate color in rgba notation. + */ + tweenTo: function (to, pos) { + // Check for has alpha, because rgba colors perform worse due to lack of + // support in WebKit. + var fromRgba = this.rgba, + toRgba = to.rgba, + hasAlpha, + ret; + + // Unsupported color, return to-color (#3920, #7034) + if (!toRgba.length || !fromRgba || !fromRgba.length) { + ret = to.input || 'none'; + + // Interpolate + } else { + hasAlpha = (toRgba[3] !== 1 || fromRgba[3] !== 1); + ret = (hasAlpha ? 'rgba(' : 'rgb(') + + Math.round(toRgba[0] + (fromRgba[0] - toRgba[0]) * (1 - pos)) + + ',' + + Math.round(toRgba[1] + (fromRgba[1] - toRgba[1]) * (1 - pos)) + + ',' + + Math.round(toRgba[2] + (fromRgba[2] - toRgba[2]) * (1 - pos)) + + ( + hasAlpha ? + ( + ',' + + (toRgba[3] + (fromRgba[3] - toRgba[3]) * (1 - pos)) + ) : + '' + ) + + ')'; + } + return ret; + } +}; +H.color = function (input) { + return new H.Color(input); +}; + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var SVGElement, + SVGRenderer, + + addEvent = H.addEvent, + animate = H.animate, + attr = H.attr, + charts = H.charts, + color = H.color, + css = H.css, + createElement = H.createElement, + defined = H.defined, + deg2rad = H.deg2rad, + destroyObjectProperties = H.destroyObjectProperties, + doc = H.doc, + each = H.each, + extend = H.extend, + erase = H.erase, + grep = H.grep, + hasTouch = H.hasTouch, + inArray = H.inArray, + isArray = H.isArray, + isFirefox = H.isFirefox, + isMS = H.isMS, + isObject = H.isObject, + isString = H.isString, + isWebKit = H.isWebKit, + merge = H.merge, + noop = H.noop, + objectEach = H.objectEach, + pick = H.pick, + pInt = H.pInt, + removeEvent = H.removeEvent, + splat = H.splat, + stop = H.stop, + svg = H.svg, + SVG_NS = H.SVG_NS, + symbolSizes = H.symbolSizes, + win = H.win; + +/** + * @typedef {Object} SVGDOMElement - An SVG DOM element. + */ +/** + * The SVGElement prototype is a JavaScript wrapper for SVG elements used in the + * rendering layer of Highcharts. Combined with the {@link + * Highcharts.SVGRenderer} object, these prototypes allow freeform annotation + * in the charts or even in HTML pages without instanciating a chart. The + * SVGElement can also wrap HTML labels, when `text` or `label` elements are + * created with the `useHTML` parameter. + * + * The SVGElement instances are created through factory functions on the + * {@link Highcharts.SVGRenderer} object, like + * [rect]{@link Highcharts.SVGRenderer#rect}, [path]{@link + * Highcharts.SVGRenderer#path}, [text]{@link Highcharts.SVGRenderer#text}, + * [label]{@link Highcharts.SVGRenderer#label}, [g]{@link + * Highcharts.SVGRenderer#g} and more. + * + * @class Highcharts.SVGElement + */ +SVGElement = H.SVGElement = function () { + return this; +}; +extend(SVGElement.prototype, /** @lends Highcharts.SVGElement.prototype */ { + + // Default base for animation + opacity: 1, + SVG_NS: SVG_NS, + + /** + * For labels, these CSS properties are applied to the `text` node directly. + * + * @private + * @type {Array.} + */ + textProps: ['direction', 'fontSize', 'fontWeight', 'fontFamily', + 'fontStyle', 'color', 'lineHeight', 'width', 'textAlign', + 'textDecoration', 'textOverflow', 'textOutline'], + + /** + * Initialize the SVG element. This function only exists to make the + * initiation process overridable. It should not be called directly. + * + * @param {SVGRenderer} renderer + * The SVGRenderer instance to initialize to. + * @param {String} nodeName + * The SVG node name. + * + */ + init: function (renderer, nodeName) { + + /** + * The primary DOM node. Each `SVGElement` instance wraps a main DOM + * node, but may also represent more nodes. + * + * @name element + * @memberOf SVGElement + * @type {SVGDOMNode|HTMLDOMNode} + */ + this.element = nodeName === 'span' ? + createElement(nodeName) : + doc.createElementNS(this.SVG_NS, nodeName); + + /** + * The renderer that the SVGElement belongs to. + * + * @name renderer + * @memberOf SVGElement + * @type {SVGRenderer} + */ + this.renderer = renderer; + }, + + /** + * Animate to given attributes or CSS properties. + * + * @param {SVGAttributes} params SVG attributes or CSS to animate. + * @param {AnimationOptions} [options] Animation options. + * @param {Function} [complete] Function to perform at the end of animation. + * + * @sample highcharts/members/element-on/ + * Setting some attributes by animation + * + * @returns {SVGElement} Returns the SVGElement for chaining. + */ + animate: function (params, options, complete) { + var animOptions = H.animObject( + pick(options, this.renderer.globalAnimation, true) + ); + if (animOptions.duration !== 0) { + // allows using a callback with the global animation without + // overwriting it + if (complete) { + animOptions.complete = complete; + } + animate(this, params, animOptions); + } else { + this.attr(params, null, complete); + if (animOptions.step) { + animOptions.step.call(this); + } + } + return this; + }, + + /** + * @typedef {Object} GradientOptions + * @property {Object} linearGradient Holds an object that defines the start + * position and the end position relative to the shape. + * @property {Number} linearGradient.x1 Start horizontal position of the + * gradient. Ranges 0-1. + * @property {Number} linearGradient.x2 End horizontal position of the + * gradient. Ranges 0-1. + * @property {Number} linearGradient.y1 Start vertical position of the + * gradient. Ranges 0-1. + * @property {Number} linearGradient.y2 End vertical position of the + * gradient. Ranges 0-1. + * @property {Object} radialGradient Holds an object that defines the center + * position and the radius. + * @property {Number} radialGradient.cx Center horizontal position relative + * to the shape. Ranges 0-1. + * @property {Number} radialGradient.cy Center vertical position relative + * to the shape. Ranges 0-1. + * @property {Number} radialGradient.r Radius relative to the shape. Ranges + * 0-1. + * @property {Array.} stops The first item in each tuple is the + * position in the gradient, where 0 is the start of the gradient and 1 + * is the end of the gradient. Multiple stops can be applied. The second + * item is the color for each stop. This color can also be given in the + * rgba format. + * + * @example + * // Linear gradient used as a color option + * color: { + * linearGradient: { x1: 0, x2: 0, y1: 0, y2: 1 }, + * stops: [ + * [0, '#003399'], // start + * [0.5, '#ffffff'], // middle + * [1, '#3366AA'] // end + * ] + * } + * } + */ + /** + * Build and apply an SVG gradient out of a common JavaScript configuration + * object. This function is called from the attribute setters. + * + * @private + * @param {GradientOptions} color The gradient options structure. + * @param {string} prop The property to apply, can either be `fill` or + * `stroke`. + * @param {SVGDOMElement} elem SVG DOM element to apply the gradient on. + */ + colorGradient: function (color, prop, elem) { + var renderer = this.renderer, + colorObject, + gradName, + gradAttr, + radAttr, + gradients, + gradientObject, + stops, + stopColor, + stopOpacity, + radialReference, + id, + key = [], + value; + + // Apply linear or radial gradients + if (color.radialGradient) { + gradName = 'radialGradient'; + } else if (color.linearGradient) { + gradName = 'linearGradient'; + } + + if (gradName) { + gradAttr = color[gradName]; + gradients = renderer.gradients; + stops = color.stops; + radialReference = elem.radialReference; + + // Keep < 2.2 kompatibility + if (isArray(gradAttr)) { + color[gradName] = gradAttr = { + x1: gradAttr[0], + y1: gradAttr[1], + x2: gradAttr[2], + y2: gradAttr[3], + gradientUnits: 'userSpaceOnUse' + }; + } + + // Correct the radial gradient for the radial reference system + if ( + gradName === 'radialGradient' && + radialReference && + !defined(gradAttr.gradientUnits) + ) { + radAttr = gradAttr; // Save the radial attributes for updating + gradAttr = merge( + gradAttr, + renderer.getRadialAttr(radialReference, radAttr), + { gradientUnits: 'userSpaceOnUse' } + ); + } + + // Build the unique key to detect whether we need to create a new + // element (#1282) + objectEach(gradAttr, function (val, n) { + if (n !== 'id') { + key.push(n, val); + } + }); + objectEach(stops, function (val) { + key.push(val); + }); + key = key.join(','); + + // Check if a gradient object with the same config object is created + // within this renderer + if (gradients[key]) { + id = gradients[key].attr('id'); + + } else { + + // Set the id and create the element + gradAttr.id = id = H.uniqueKey(); + gradients[key] = gradientObject = + renderer.createElement(gradName) + .attr(gradAttr) + .add(renderer.defs); + + gradientObject.radAttr = radAttr; + + // The gradient needs to keep a list of stops to be able to + // destroy them + gradientObject.stops = []; + each(stops, function (stop) { + var stopObject; + if (stop[1].indexOf('rgba') === 0) { + colorObject = H.color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + stopObject = renderer.createElement('stop').attr({ + offset: stop[0], + 'stop-color': stopColor, + 'stop-opacity': stopOpacity + }).add(gradientObject); + + // Add the stop element to the gradient + gradientObject.stops.push(stopObject); + }); + } + + // Set the reference to the gradient object + value = 'url(' + renderer.url + '#' + id + ')'; + elem.setAttribute(prop, value); + elem.gradient = key; + + // Allow the color to be concatenated into tooltips formatters etc. + // (#2995) + color.toString = function () { + return value; + }; + } + }, + + /** + * Apply a text outline through a custom CSS property, by copying the text + * element and apply stroke to the copy. Used internally. Contrast checks + * at http://jsfiddle.net/highcharts/43soe9m1/2/ . + * + * @private + * @param {String} textOutline A custom CSS `text-outline` setting, defined + * by `width color`. + * @example + * // Specific color + * text.css({ + * textOutline: '1px black' + * }); + * // Automatic contrast + * text.css({ + * color: '#000000', // black text + * textOutline: '1px contrast' // => white outline + * }); + */ + applyTextOutline: function (textOutline) { + var elem = this.element, + tspans, + tspan, + hasContrast = textOutline.indexOf('contrast') !== -1, + styles = {}, + color, + strokeWidth, + firstRealChild, + i; + + // When the text shadow is set to contrast, use dark stroke for light + // text and vice versa. + if (hasContrast) { + styles.textOutline = textOutline = textOutline.replace( + /contrast/g, + this.renderer.getContrast(elem.style.fill) + ); + } + + // Extract the stroke width and color + textOutline = textOutline.split(' '); + color = textOutline[textOutline.length - 1]; + strokeWidth = textOutline[0]; + + if (strokeWidth && strokeWidth !== 'none' && H.svg) { + + this.fakeTS = true; // Fake text shadow + + tspans = [].slice.call(elem.getElementsByTagName('tspan')); + + // In order to get the right y position of the clone, + // copy over the y setter + this.ySetter = this.xSetter; + + // Since the stroke is applied on center of the actual outline, we + // need to double it to get the correct stroke-width outside the + // glyphs. + strokeWidth = strokeWidth.replace( + /(^[\d\.]+)(.*?)$/g, + function (match, digit, unit) { + return (2 * digit) + unit; + } + ); + + // Remove shadows from previous runs. Iterate from the end to + // support removing items inside the cycle (#6472). + i = tspans.length; + while (i--) { + tspan = tspans[i]; + if (tspan.getAttribute('class') === 'highcharts-text-outline') { + // Remove then erase + erase(tspans, elem.removeChild(tspan)); + } + } + + // For each of the tspans, create a stroked copy behind it. + firstRealChild = elem.firstChild; + each(tspans, function (tspan, y) { + var clone; + + // Let the first line start at the correct X position + if (y === 0) { + tspan.setAttribute('x', elem.getAttribute('x')); + y = elem.getAttribute('y'); + tspan.setAttribute('y', y || 0); + if (y === null) { + elem.setAttribute('y', 0); + } + } + + // Create the clone and apply outline properties + clone = tspan.cloneNode(1); + attr(clone, { + 'class': 'highcharts-text-outline', + 'fill': color, + 'stroke': color, + 'stroke-width': strokeWidth, + 'stroke-linejoin': 'round' + }); + elem.insertBefore(clone, firstRealChild); + }); + } + }, + + /** + * + * @typedef {Object} SVGAttributes An object of key-value pairs for SVG + * attributes. Attributes in Highcharts elements for the most parts + * correspond to SVG, but some are specific to Highcharts, like `zIndex`, + * `rotation`, `rotationOriginX`, `rotationOriginY`, `translateX`, + * `translateY`, `scaleX` and `scaleY`. SVG attributes containing a hyphen + * are _not_ camel-cased, they should be quoted to preserve the hyphen. + * + * @example + * { + * 'stroke': '#ff0000', // basic + * 'stroke-width': 2, // hyphenated + * 'rotation': 45 // custom + * 'd': ['M', 10, 10, 'L', 30, 30, 'z'] // path definition, note format + * } + */ + /** + * Apply native and custom attributes to the SVG elements. + * + * In order to set the rotation center for rotation, set x and y to 0 and + * use `translateX` and `translateY` attributes to position the element + * instead. + * + * Attributes frequently used in Highcharts are `fill`, `stroke`, + * `stroke-width`. + * + * @param {SVGAttributes|String} hash - The native and custom SVG + * attributes. + * @param {string} [val] - If the type of the first argument is `string`, + * the second can be a value, which will serve as a single attribute + * setter. If the first argument is a string and the second is undefined, + * the function serves as a getter and the current value of the property + * is returned. + * @param {Function} [complete] - A callback function to execute after + * setting the attributes. This makes the function compliant and + * interchangeable with the {@link SVGElement#animate} function. + * @param {boolean} [continueAnimation=true] Used internally when `.attr` is + * called as part of an animation step. Otherwise, calling `.attr` for an + * attribute will stop animation for that attribute. + * + * @returns {SVGElement|string|number} If used as a setter, it returns the + * current {@link SVGElement} so the calls can be chained. If used as a + * getter, the current value of the attribute is returned. + * + * @sample highcharts/members/renderer-rect/ + * Setting some attributes + * + * @example + * // Set multiple attributes + * element.attr({ + * stroke: 'red', + * fill: 'blue', + * x: 10, + * y: 10 + * }); + * + * // Set a single attribute + * element.attr('stroke', 'red'); + * + * // Get an attribute + * element.attr('stroke'); // => 'red' + * + */ + attr: function (hash, val, complete, continueAnimation) { + var key, + element = this.element, + hasSetSymbolSize, + ret = this, + skipAttr, + setter; + + // single key-value pair + if (typeof hash === 'string' && val !== undefined) { + key = hash; + hash = {}; + hash[key] = val; + } + + // used as a getter: first argument is a string, second is undefined + if (typeof hash === 'string') { + ret = (this[hash + 'Getter'] || this._defaultGetter).call( + this, + hash, + element + ); + + // setter + } else { + + objectEach(hash, function eachAttribute(val, key) { + skipAttr = false; + + // Unless .attr is from the animator update, stop current + // running animation of this property + if (!continueAnimation) { + stop(this, key); + } + + // Special handling of symbol attributes + if ( + this.symbolName && + /^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)$/ + .test(key) + ) { + if (!hasSetSymbolSize) { + this.symbolAttr(hash); + hasSetSymbolSize = true; + } + skipAttr = true; + } + + if (this.rotation && (key === 'x' || key === 'y')) { + this.doTransform = true; + } + + if (!skipAttr) { + setter = this[key + 'Setter'] || this._defaultSetter; + setter.call(this, val, key, element); + + + // Let the shadow follow the main element + if ( + this.shadows && + /^(width|height|visibility|x|y|d|transform|cx|cy|r)$/ + .test(key) + ) { + this.updateShadows(key, val, setter); + } + + } + }, this); + + this.afterSetters(); + } + + // In accordance with animate, run a complete callback + if (complete) { + complete(); + } + + return ret; + }, + + /** + * This method is executed in the end of `attr()`, after setting all + * attributes in the hash. In can be used to efficiently consolidate + * multiple attributes in one SVG property -- e.g., translate, rotate and + * scale are merged in one "transform" attribute in the SVG node. + * + * @private + */ + afterSetters: function () { + // Update transform. Do this outside the loop to prevent redundant + // updating for batch setting of attributes. + if (this.doTransform) { + this.updateTransform(); + this.doTransform = false; + } + }, + + + /** + * Update the shadow elements with new attributes. + * + * @private + * @param {String} key - The attribute name. + * @param {String|Number} value - The value of the attribute. + * @param {Function} setter - The setter function, inherited from the + * parent wrapper + * + */ + updateShadows: function (key, value, setter) { + var shadows = this.shadows, + i = shadows.length; + + while (i--) { + setter.call( + shadows[i], + key === 'height' ? + Math.max(value - (shadows[i].cutHeight || 0), 0) : + key === 'd' ? this.d : value, + key, + shadows[i] + ); + } + }, + + + /** + * Add a class name to an element. + * + * @param {string} className - The new class name to add. + * @param {boolean} [replace=false] - When true, the existing class name(s) + * will be overwritten with the new one. When false, the new one is + * added. + * @returns {SVGElement} Return the SVG element for chainability. + */ + addClass: function (className, replace) { + var currentClassName = this.attr('class') || ''; + if (currentClassName.indexOf(className) === -1) { + if (!replace) { + className = + (currentClassName + (currentClassName ? ' ' : '') + + className).replace(' ', ' '); + } + this.attr('class', className); + } + + return this; + }, + + /** + * Check if an element has the given class name. + * @param {string} className + * The class name to check for. + * @return {Boolean} + * Whether the class name is found. + */ + hasClass: function (className) { + return inArray( + className, + (this.attr('class') || '').split(' ') + ) !== -1; + }, + + /** + * Remove a class name from the element. + * @param {String|RegExp} className The class name to remove. + * @return {SVGElement} Returns the SVG element for chainability. + */ + removeClass: function (className) { + return this.attr( + 'class', + (this.attr('class') || '').replace(className, '') + ); + }, + + /** + * If one of the symbol size affecting parameters are changed, + * check all the others only once for each call to an element's + * .attr() method + * @param {Object} hash - The attributes to set. + * @private + */ + symbolAttr: function (hash) { + var wrapper = this; + + each([ + 'x', + 'y', + 'r', + 'start', + 'end', + 'width', + 'height', + 'innerR', + 'anchorX', + 'anchorY' + ], function (key) { + wrapper[key] = pick(hash[key], wrapper[key]); + }); + + wrapper.attr({ + d: wrapper.renderer.symbols[wrapper.symbolName]( + wrapper.x, + wrapper.y, + wrapper.width, + wrapper.height, + wrapper + ) + }); + }, + + /** + * Apply a clipping rectangle to this element. + * + * @param {ClipRect} [clipRect] - The clipping rectangle. If skipped, the + * current clip is removed. + * @returns {SVGElement} Returns the SVG element to allow chaining. + */ + clip: function (clipRect) { + return this.attr( + 'clip-path', + clipRect ? + 'url(' + this.renderer.url + '#' + clipRect.id + ')' : + 'none' + ); + }, + + /** + * Calculate the coordinates needed for drawing a rectangle crisply and + * return the calculated attributes. + * + * @param {Object} rect - A rectangle. + * @param {number} rect.x - The x position. + * @param {number} rect.y - The y position. + * @param {number} rect.width - The width. + * @param {number} rect.height - The height. + * @param {number} [strokeWidth] - The stroke width to consider when + * computing crisp positioning. It can also be set directly on the rect + * parameter. + * + * @returns {{x: Number, y: Number, width: Number, height: Number}} The + * modified rectangle arguments. + */ + crisp: function (rect, strokeWidth) { + + var wrapper = this, + normalizer; + + strokeWidth = strokeWidth || rect.strokeWidth || 0; + // Math.round because strokeWidth can sometimes have roundoff errors + normalizer = Math.round(strokeWidth) % 2 / 2; + + // normalize for crisp edges + rect.x = Math.floor(rect.x || wrapper.x || 0) + normalizer; + rect.y = Math.floor(rect.y || wrapper.y || 0) + normalizer; + rect.width = Math.floor( + (rect.width || wrapper.width || 0) - 2 * normalizer + ); + rect.height = Math.floor( + (rect.height || wrapper.height || 0) - 2 * normalizer + ); + if (defined(rect.strokeWidth)) { + rect.strokeWidth = strokeWidth; + } + return rect; + }, + + /** + * Set styles for the element. In addition to CSS styles supported by + * native SVG and HTML elements, there are also some custom made for + * Highcharts, like `width`, `ellipsis` and `textOverflow` for SVG text + * elements. + * @param {CSSObject} styles The new CSS styles. + * @returns {SVGElement} Return the SVG element for chaining. + * + * @sample highcharts/members/renderer-text-on-chart/ + * Styled text + */ + css: function (styles) { + var oldStyles = this.styles, + newStyles = {}, + elem = this.element, + textWidth, + serializedCss = '', + hyphenate, + hasNew = !oldStyles, + // These CSS properties are interpreted internally by the SVG + // renderer, but are not supported by SVG and should not be added to + // the DOM. In styled mode, no CSS should find its way to the DOM + // whatsoever (#6173, #6474). + svgPseudoProps = ['textOutline', 'textOverflow', 'width']; + + // convert legacy + if (styles && styles.color) { + styles.fill = styles.color; + } + + // Filter out existing styles to increase performance (#2640) + if (oldStyles) { + objectEach(styles, function (style, n) { + if (style !== oldStyles[n]) { + newStyles[n] = style; + hasNew = true; + } + }); + } + if (hasNew) { + + // Merge the new styles with the old ones + if (oldStyles) { + styles = extend( + oldStyles, + newStyles + ); + } + + // Get the text width from style + textWidth = this.textWidth = ( + styles && + styles.width && + styles.width !== 'auto' && + elem.nodeName.toLowerCase() === 'text' && + pInt(styles.width) + ); + + // store object + this.styles = styles; + + if (textWidth && (!svg && this.renderer.forExport)) { + delete styles.width; + } + + // serialize and set style attribute + if (isMS && !svg) { + css(this.element, styles); + } else { + hyphenate = function (a, b) { + return '-' + b.toLowerCase(); + }; + objectEach(styles, function (style, n) { + if (inArray(n, svgPseudoProps) === -1) { + serializedCss += + n.replace(/([A-Z])/g, hyphenate) + ':' + + style + ';'; + } + }); + if (serializedCss) { + attr(elem, 'style', serializedCss); // #1881 + } + } + + + if (this.added) { + + // Rebuild text after added. Cache mechanisms in the buildText + // will prevent building if there are no significant changes. + if (this.element.nodeName === 'text') { + this.renderer.buildText(this); + } + + // Apply text outline after added + if (styles && styles.textOutline) { + this.applyTextOutline(styles.textOutline); + } + } + } + + return this; + }, + + + /** + * Get the current stroke width. In classic mode, the setter registers it + * directly on the element. + * @returns {number} The stroke width in pixels. + * @ignore + */ + strokeWidth: function () { + return this['stroke-width'] || 0; + }, + + + /** + * Add an event listener. This is a simple setter that replaces all other + * events of the same type, opposed to the {@link Highcharts#addEvent} + * function. + * @param {string} eventType - The event type. If the type is `click`, + * Highcharts will internally translate it to a `touchstart` event on + * touch devices, to prevent the browser from waiting for a click event + * from firing. + * @param {Function} handler - The handler callback. + * @returns {SVGElement} The SVGElement for chaining. + * + * @sample highcharts/members/element-on/ + * A clickable rectangle + */ + on: function (eventType, handler) { + var svgElement = this, + element = svgElement.element; + + // touch + if (hasTouch && eventType === 'click') { + element.ontouchstart = function (e) { + svgElement.touchEventFired = Date.now(); // #2269 + e.preventDefault(); + handler.call(element, e); + }; + element.onclick = function (e) { + if (win.navigator.userAgent.indexOf('Android') === -1 || + Date.now() - (svgElement.touchEventFired || 0) > 1100) { + handler.call(element, e); + } + }; + } else { + // simplest possible event model for internal use + element['on' + eventType] = handler; + } + return this; + }, + + /** + * Set the coordinates needed to draw a consistent radial gradient across + * a shape regardless of positioning inside the chart. Used on pie slices + * to make all the slices have the same radial reference point. + * + * @param {Array} coordinates The center reference. The format is + * `[centerX, centerY, diameter]` in pixels. + * @returns {SVGElement} Returns the SVGElement for chaining. + */ + setRadialReference: function (coordinates) { + var existingGradient = this.renderer.gradients[this.element.gradient]; + + this.element.radialReference = coordinates; + + // On redrawing objects with an existing gradient, the gradient needs + // to be repositioned (#3801) + if (existingGradient && existingGradient.radAttr) { + existingGradient.animate( + this.renderer.getRadialAttr( + coordinates, + existingGradient.radAttr + ) + ); + } + + return this; + }, + + /** + * Move an object and its children by x and y values. + * + * @param {number} x - The x value. + * @param {number} y - The y value. + */ + translate: function (x, y) { + return this.attr({ + translateX: x, + translateY: y + }); + }, + + /** + * Invert a group, rotate and flip. This is used internally on inverted + * charts, where the points and graphs are drawn as if not inverted, then + * the series group elements are inverted. + * + * @param {boolean} inverted + * Whether to invert or not. An inverted shape can be un-inverted by + * setting it to false. + * @return {SVGElement} + * Return the SVGElement for chaining. + */ + invert: function (inverted) { + var wrapper = this; + wrapper.inverted = inverted; + wrapper.updateTransform(); + return wrapper; + }, + + /** + * Update the transform attribute based on internal properties. Deals with + * the custom `translateX`, `translateY`, `rotation`, `scaleX` and `scaleY` + * attributes and updates the SVG `transform` attribute. + * @private + * + */ + updateTransform: function () { + var wrapper = this, + translateX = wrapper.translateX || 0, + translateY = wrapper.translateY || 0, + scaleX = wrapper.scaleX, + scaleY = wrapper.scaleY, + inverted = wrapper.inverted, + rotation = wrapper.rotation, + matrix = wrapper.matrix, + element = wrapper.element, + transform; + + // Flipping affects translate as adjustment for flipping around the + // group's axis + if (inverted) { + translateX += wrapper.width; + translateY += wrapper.height; + } + + // Apply translate. Nearly all transformed elements have translation, + // so instead of checking for translate = 0, do it always (#1767, + // #1846). + transform = ['translate(' + translateX + ',' + translateY + ')']; + + // apply matrix + if (defined(matrix)) { + transform.push( + 'matrix(' + matrix.join(',') + ')' + ); + } + + // apply rotation + if (inverted) { + transform.push('rotate(90) scale(-1,1)'); + } else if (rotation) { // text rotation + transform.push( + 'rotate(' + rotation + ' ' + + pick(this.rotationOriginX, element.getAttribute('x'), 0) + + ' ' + + pick(this.rotationOriginY, element.getAttribute('y') || 0) + ')' + ); + } + + // apply scale + if (defined(scaleX) || defined(scaleY)) { + transform.push( + 'scale(' + pick(scaleX, 1) + ' ' + pick(scaleY, 1) + ')' + ); + } + + if (transform.length) { + element.setAttribute('transform', transform.join(' ')); + } + }, + + /** + * Bring the element to the front. Alternatively, a new zIndex can be set. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + * + * @sample highcharts/members/element-tofront/ + * Click an element to bring it to front + */ + toFront: function () { + var element = this.element; + element.parentNode.appendChild(element); + return this; + }, + + + /** + * Align the element relative to the chart or another box. + * + * @param {Object} [alignOptions] The alignment options. The function can be + * called without this parameter in order to re-align an element after the + * box has been updated. + * @param {string} [alignOptions.align=left] Horizontal alignment. Can be + * one of `left`, `center` and `right`. + * @param {string} [alignOptions.verticalAlign=top] Vertical alignment. Can + * be one of `top`, `middle` and `bottom`. + * @param {number} [alignOptions.x=0] Horizontal pixel offset from + * alignment. + * @param {number} [alignOptions.y=0] Vertical pixel offset from alignment. + * @param {Boolean} [alignByTranslate=false] Use the `transform` attribute + * with translateX and translateY custom attributes to align this elements + * rather than `x` and `y` attributes. + * @param {String|Object} box The box to align to, needs a width and height. + * When the box is a string, it refers to an object in the Renderer. For + * example, when box is `spacingBox`, it refers to `Renderer.spacingBox` + * which holds `width`, `height`, `x` and `y` properties. + * @returns {SVGElement} Returns the SVGElement for chaining. + */ + align: function (alignOptions, alignByTranslate, box) { + var align, + vAlign, + x, + y, + attribs = {}, + alignTo, + renderer = this.renderer, + alignedObjects = renderer.alignedObjects, + alignFactor, + vAlignFactor; + + // First call on instanciate + if (alignOptions) { + this.alignOptions = alignOptions; + this.alignByTranslate = alignByTranslate; + if (!box || isString(box)) { + this.alignTo = alignTo = box || 'renderer'; + // prevent duplicates, like legendGroup after resize + erase(alignedObjects, this); + alignedObjects.push(this); + box = null; // reassign it below + } + + // When called on resize, no arguments are supplied + } else { + alignOptions = this.alignOptions; + alignByTranslate = this.alignByTranslate; + alignTo = this.alignTo; + } + + box = pick(box, renderer[alignTo], renderer); + + // Assign variables + align = alignOptions.align; + vAlign = alignOptions.verticalAlign; + x = (box.x || 0) + (alignOptions.x || 0); // default: left align + y = (box.y || 0) + (alignOptions.y || 0); // default: top align + + // Align + if (align === 'right') { + alignFactor = 1; + } else if (align === 'center') { + alignFactor = 2; + } + if (alignFactor) { + x += (box.width - (alignOptions.width || 0)) / alignFactor; + } + attribs[alignByTranslate ? 'translateX' : 'x'] = Math.round(x); + + + // Vertical align + if (vAlign === 'bottom') { + vAlignFactor = 1; + } else if (vAlign === 'middle') { + vAlignFactor = 2; + } + if (vAlignFactor) { + y += (box.height - (alignOptions.height || 0)) / vAlignFactor; + } + attribs[alignByTranslate ? 'translateY' : 'y'] = Math.round(y); + + // Animate only if already placed + this[this.placed ? 'animate' : 'attr'](attribs); + this.placed = true; + this.alignAttr = attribs; + + return this; + }, + + /** + * Get the bounding box (width, height, x and y) for the element. Generally + * used to get rendered text size. Since this is called a lot in charts, + * the results are cached based on text properties, in order to save DOM + * traffic. The returned bounding box includes the rotation, so for example + * a single text line of rotation 90 will report a greater height, and a + * width corresponding to the line-height. + * + * @param {boolean} [reload] Skip the cache and get the updated DOM bouding + * box. + * @param {number} [rot] Override the element's rotation. This is internally + * used on axis labels with a value of 0 to find out what the bounding box + * would be have been if it were not rotated. + * @returns {Object} The bounding box with `x`, `y`, `width` and `height` + * properties. + * + * @sample highcharts/members/renderer-on-chart/ + * Draw a rectangle based on a text's bounding box + */ + getBBox: function (reload, rot) { + var wrapper = this, + bBox, // = wrapper.bBox, + renderer = wrapper.renderer, + width, + height, + rotation, + rad, + element = wrapper.element, + styles = wrapper.styles, + fontSize, + textStr = wrapper.textStr, + toggleTextShadowShim, + cache = renderer.cache, + cacheKeys = renderer.cacheKeys, + cacheKey; + + rotation = pick(rot, wrapper.rotation); + rad = rotation * deg2rad; + + + fontSize = styles && styles.fontSize; + + + // Avoid undefined and null (#7316) + if (defined(textStr)) { + + cacheKey = textStr.toString(); + + // Since numbers are monospaced, and numerical labels appear a lot + // in a chart, we assume that a label of n characters has the same + // bounding box as others of the same length. Unless there is inner + // HTML in the label. In that case, leave the numbers as is (#5899). + if (cacheKey.indexOf('<') === -1) { + cacheKey = cacheKey.replace(/[0-9]/g, '0'); + } + + // Properties that affect bounding box + cacheKey += [ + '', + rotation || 0, + fontSize, + styles && styles.width, + styles && styles.textOverflow // #5968 + ] + .join(','); + + } + + if (cacheKey && !reload) { + bBox = cache[cacheKey]; + } + + // No cache found + if (!bBox) { + + // SVG elements + if (element.namespaceURI === wrapper.SVG_NS || renderer.forExport) { + try { // Fails in Firefox if the container has display: none. + + // When the text shadow shim is used, we need to hide the + // fake shadows to get the correct bounding box (#3872) + toggleTextShadowShim = this.fakeTS && function (display) { + each( + element.querySelectorAll( + '.highcharts-text-outline' + ), + function (tspan) { + tspan.style.display = display; + } + ); + }; + + // Workaround for #3842, Firefox reporting wrong bounding + // box for shadows + if (toggleTextShadowShim) { + toggleTextShadowShim('none'); + } + + bBox = element.getBBox ? + // SVG: use extend because IE9 is not allowed to change + // width and height in case of rotation (below) + extend({}, element.getBBox()) : { + + // Legacy IE in export mode + width: element.offsetWidth, + height: element.offsetHeight + }; + + // #3842 + if (toggleTextShadowShim) { + toggleTextShadowShim(''); + } + } catch (e) {} + + // If the bBox is not set, the try-catch block above failed. The + // other condition is for Opera that returns a width of + // -Infinity on hidden elements. + if (!bBox || bBox.width < 0) { + bBox = { width: 0, height: 0 }; + } + + + // VML Renderer or useHTML within SVG + } else { + + bBox = wrapper.htmlGetBBox(); + + } + + // True SVG elements as well as HTML elements in modern browsers + // using the .useHTML option need to compensated for rotation + if (renderer.isSVG) { + width = bBox.width; + height = bBox.height; + + // Workaround for wrong bounding box in IE, Edge and Chrome on + // Windows. With Highcharts' default font, IE and Edge report + // a box height of 16.899 and Chrome rounds it to 17. If this + // stands uncorrected, it results in more padding added below + // the text than above when adding a label border or background. + // Also vertical positioning is affected. + // http://jsfiddle.net/highcharts/em37nvuj/ + // (#1101, #1505, #1669, #2568, #6213). + if ( + styles && + styles.fontSize === '11px' && + Math.round(height) === 17 + ) { + bBox.height = height = 14; + } + + // Adjust for rotated text + if (rotation) { + bBox.width = Math.abs(height * Math.sin(rad)) + + Math.abs(width * Math.cos(rad)); + bBox.height = Math.abs(height * Math.cos(rad)) + + Math.abs(width * Math.sin(rad)); + } + } + + // Cache it. When loading a chart in a hidden iframe in Firefox and + // IE/Edge, the bounding box height is 0, so don't cache it (#5620). + if (cacheKey && bBox.height > 0) { + + // Rotate (#4681) + while (cacheKeys.length > 250) { + delete cache[cacheKeys.shift()]; + } + + if (!cache[cacheKey]) { + cacheKeys.push(cacheKey); + } + cache[cacheKey] = bBox; + } + } + return bBox; + }, + + /** + * Show the element after it has been hidden. + * + * @param {boolean} [inherit=false] Set the visibility attribute to + * `inherit` rather than `visible`. The difference is that an element with + * `visibility="visible"` will be visible even if the parent is hidden. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + */ + show: function (inherit) { + return this.attr({ visibility: inherit ? 'inherit' : 'visible' }); + }, + + /** + * Hide the element, equivalent to setting the `visibility` attribute to + * `hidden`. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + */ + hide: function () { + return this.attr({ visibility: 'hidden' }); + }, + + /** + * Fade out an element by animating its opacity down to 0, and hide it on + * complete. Used internally for the tooltip. + * + * @param {number} [duration=150] The fade duration in milliseconds. + */ + fadeOut: function (duration) { + var elemWrapper = this; + elemWrapper.animate({ + opacity: 0 + }, { + duration: duration || 150, + complete: function () { + // #3088, assuming we're only using this for tooltips + elemWrapper.attr({ y: -9999 }); + } + }); + }, + + /** + * Add the element to the DOM. All elements must be added this way. + * + * @param {SVGElement|SVGDOMElement} [parent] The parent item to add it to. + * If undefined, the element is added to the {@link + * Highcharts.SVGRenderer.box}. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + * + * @sample highcharts/members/renderer-g - Elements added to a group + */ + add: function (parent) { + + var renderer = this.renderer, + element = this.element, + inserted; + + if (parent) { + this.parentGroup = parent; + } + + // mark as inverted + this.parentInverted = parent && parent.inverted; + + // build formatted text + if (this.textStr !== undefined) { + renderer.buildText(this); + } + + // Mark as added + this.added = true; + + // If we're adding to renderer root, or other elements in the group + // have a z index, we need to handle it + if (!parent || parent.handleZ || this.zIndex) { + inserted = this.zIndexSetter(); + } + + // If zIndex is not handled, append at the end + if (!inserted) { + (parent ? parent.element : renderer.box).appendChild(element); + } + + // fire an event for internal hooks + if (this.onAdd) { + this.onAdd(); + } + + return this; + }, + + /** + * Removes an element from the DOM. + * + * @private + * @param {SVGDOMElement|HTMLDOMElement} element The DOM node to remove. + */ + safeRemoveChild: function (element) { + var parentNode = element.parentNode; + if (parentNode) { + parentNode.removeChild(element); + } + }, + + /** + * Destroy the element and element wrapper and clear up the DOM and event + * hooks. + * + * + */ + destroy: function () { + var wrapper = this, + element = wrapper.element || {}, + parentToClean = + wrapper.renderer.isSVG && + element.nodeName === 'SPAN' && + wrapper.parentGroup, + grandParent, + ownerSVGElement = element.ownerSVGElement, + i; + + // remove events + element.onclick = element.onmouseout = element.onmouseover = + element.onmousemove = element.point = null; + stop(wrapper); // stop running animations + + if (wrapper.clipPath && ownerSVGElement) { + // Look for existing references to this clipPath and remove them + // before destroying the element (#6196). + each( + // The upper case version is for Edge + ownerSVGElement.querySelectorAll('[clip-path],[CLIP-PATH]'), + function (el) { + // Include the closing paranthesis in the test to rule out + // id's from 10 and above (#6550) + if (el + .getAttribute('clip-path') + .match(RegExp( + // Edge puts quotes inside the url, others not + '[\("]#' + wrapper.clipPath.element.id + '[\)"]' + )) + ) { + el.removeAttribute('clip-path'); + } + } + ); + wrapper.clipPath = wrapper.clipPath.destroy(); + } + + // Destroy stops in case this is a gradient object + if (wrapper.stops) { + for (i = 0; i < wrapper.stops.length; i++) { + wrapper.stops[i] = wrapper.stops[i].destroy(); + } + wrapper.stops = null; + } + + // remove element + wrapper.safeRemoveChild(element); + + + wrapper.destroyShadows(); + + + // In case of useHTML, clean up empty containers emulating SVG groups + // (#1960, #2393, #2697). + while ( + parentToClean && + parentToClean.div && + parentToClean.div.childNodes.length === 0 + ) { + grandParent = parentToClean.parentGroup; + wrapper.safeRemoveChild(parentToClean.div); + delete parentToClean.div; + parentToClean = grandParent; + } + + // remove from alignObjects + if (wrapper.alignTo) { + erase(wrapper.renderer.alignedObjects, wrapper); + } + + objectEach(wrapper, function (val, key) { + delete wrapper[key]; + }); + + return null; + }, + + + /** + * @typedef {Object} ShadowOptions + * @property {string} [color=#000000] The shadow color. + * @property {number} [offsetX=1] The horizontal offset from the element. + * @property {number} [offsetY=1] The vertical offset from the element. + * @property {number} [opacity=0.15] The shadow opacity. + * @property {number} [width=3] The shadow width or distance from the + * element. + */ + /** + * Add a shadow to the element. Must be called after the element is added to + * the DOM. In styled mode, this method is not used, instead use `defs` and + * filters. + * + * @param {boolean|ShadowOptions} shadowOptions The shadow options. If + * `true`, the default options are applied. If `false`, the current + * shadow will be removed. + * @param {SVGElement} [group] The SVG group element where the shadows will + * be applied. The default is to add it to the same parent as the current + * element. Internally, this is ised for pie slices, where all the + * shadows are added to an element behind all the slices. + * @param {boolean} [cutOff] Used internally for column shadows. + * + * @returns {SVGElement} Returns the SVGElement for chaining. + * + * @example + * renderer.rect(10, 100, 100, 100) + * .attr({ fill: 'red' }) + * .shadow(true); + */ + shadow: function (shadowOptions, group, cutOff) { + var shadows = [], + i, + shadow, + element = this.element, + strokeWidth, + shadowWidth, + shadowElementOpacity, + + // compensate for inverted plot area + transform; + + if (!shadowOptions) { + this.destroyShadows(); + + } else if (!this.shadows) { + shadowWidth = pick(shadowOptions.width, 3); + shadowElementOpacity = (shadowOptions.opacity || 0.15) / + shadowWidth; + transform = this.parentInverted ? + '(-1,-1)' : + '(' + pick(shadowOptions.offsetX, 1) + ', ' + + pick(shadowOptions.offsetY, 1) + ')'; + for (i = 1; i <= shadowWidth; i++) { + shadow = element.cloneNode(0); + strokeWidth = (shadowWidth * 2) + 1 - (2 * i); + attr(shadow, { + 'isShadow': 'true', + 'stroke': + shadowOptions.color || '#000000', + 'stroke-opacity': shadowElementOpacity * i, + 'stroke-width': strokeWidth, + 'transform': 'translate' + transform, + 'fill': 'none' + }); + if (cutOff) { + attr( + shadow, + 'height', + Math.max(attr(shadow, 'height') - strokeWidth, 0) + ); + shadow.cutHeight = strokeWidth; + } + + if (group) { + group.element.appendChild(shadow); + } else if (element.parentNode) { + element.parentNode.insertBefore(shadow, element); + } + + shadows.push(shadow); + } + + this.shadows = shadows; + } + return this; + + }, + + /** + * Destroy shadows on the element. + * @private + */ + destroyShadows: function () { + each(this.shadows || [], function (shadow) { + this.safeRemoveChild(shadow); + }, this); + this.shadows = undefined; + }, + + + + xGetter: function (key) { + if (this.element.nodeName === 'circle') { + if (key === 'x') { + key = 'cx'; + } else if (key === 'y') { + key = 'cy'; + } + } + return this._defaultGetter(key); + }, + + /** + * Get the current value of an attribute or pseudo attribute, used mainly + * for animation. Called internally from the {@link + * Highcharts.SVGRenderer#attr} + * function. + * + * @private + */ + _defaultGetter: function (key) { + var ret = pick( + this[key + 'Value'], // align getter + this[key], + this.element ? this.element.getAttribute(key) : null, + 0 + ); + + if (/^[\-0-9\.]+$/.test(ret)) { // is numerical + ret = parseFloat(ret); + } + return ret; + }, + + + dSetter: function (value, key, element) { + if (value && value.join) { // join path + value = value.join(' '); + } + if (/(NaN| {2}|^$)/.test(value)) { + value = 'M 0 0'; + } + + // Check for cache before resetting. Resetting causes disturbance in the + // DOM, causing flickering in some cases in Edge/IE (#6747). Also + // possible performance gain. + if (this[key] !== value) { + element.setAttribute(key, value); + this[key] = value; + } + + }, + + dashstyleSetter: function (value) { + var i, + strokeWidth = this['stroke-width']; + + // If "inherit", like maps in IE, assume 1 (#4981). With HC5 and the new + // strokeWidth function, we should be able to use that instead. + if (strokeWidth === 'inherit') { + strokeWidth = 1; + } + value = value && value.toLowerCase(); + if (value) { + value = value + .replace('shortdashdotdot', '3,1,1,1,1,1,') + .replace('shortdashdot', '3,1,1,1') + .replace('shortdot', '1,1,') + .replace('shortdash', '3,1,') + .replace('longdash', '8,3,') + .replace(/dot/g, '1,3,') + .replace('dash', '4,3,') + .replace(/,$/, '') + .split(','); // ending comma + + i = value.length; + while (i--) { + value[i] = pInt(value[i]) * strokeWidth; + } + value = value.join(',') + .replace(/NaN/g, 'none'); // #3226 + this.element.setAttribute('stroke-dasharray', value); + } + }, + + alignSetter: function (value) { + var convert = { left: 'start', center: 'middle', right: 'end' }; + this.alignValue = value; + this.element.setAttribute('text-anchor', convert[value]); + }, + opacitySetter: function (value, key, element) { + this[key] = value; + element.setAttribute(key, value); + }, + titleSetter: function (value) { + var titleNode = this.element.getElementsByTagName('title')[0]; + if (!titleNode) { + titleNode = doc.createElementNS(this.SVG_NS, 'title'); + this.element.appendChild(titleNode); + } + + // Remove text content if it exists + if (titleNode.firstChild) { + titleNode.removeChild(titleNode.firstChild); + } + + titleNode.appendChild( + doc.createTextNode( + // #3276, #3895 + (String(pick(value), '')).replace(/<[^>]*>/g, '') + ) + ); + }, + textSetter: function (value) { + if (value !== this.textStr) { + // Delete bBox memo when the text changes + delete this.bBox; + + this.textStr = value; + if (this.added) { + this.renderer.buildText(this); + } + } + }, + fillSetter: function (value, key, element) { + if (typeof value === 'string') { + element.setAttribute(key, value); + } else if (value) { + this.colorGradient(value, key, element); + } + }, + visibilitySetter: function (value, key, element) { + // IE9-11 doesn't handle visibilty:inherit well, so we remove the + // attribute instead (#2881, #3909) + if (value === 'inherit') { + element.removeAttribute(key); + } else if (this[key] !== value) { // #6747 + element.setAttribute(key, value); + } + this[key] = value; + }, + zIndexSetter: function (value, key) { + var renderer = this.renderer, + parentGroup = this.parentGroup, + parentWrapper = parentGroup || renderer, + parentNode = parentWrapper.element || renderer.box, + childNodes, + otherElement, + otherZIndex, + element = this.element, + inserted, + undefinedOtherZIndex, + svgParent = parentNode === renderer.box, + run = this.added, + i; + + if (defined(value)) { + // So we can read it for other elements in the group + element.zIndex = value; + + value = +value; + if (this[key] === value) { // Only update when needed (#3865) + run = false; + } + this[key] = value; + } + + // Insert according to this and other elements' zIndex. Before .add() is + // called, nothing is done. Then on add, or by later calls to + // zIndexSetter, the node is placed on the right place in the DOM. + if (run) { + value = this.zIndex; + + if (value && parentGroup) { + parentGroup.handleZ = true; + } + + childNodes = parentNode.childNodes; + for (i = childNodes.length - 1; i >= 0 && !inserted; i--) { + otherElement = childNodes[i]; + otherZIndex = otherElement.zIndex; + undefinedOtherZIndex = !defined(otherZIndex); + + if (otherElement !== element) { + if ( + // Negative zIndex versus no zIndex: + // On all levels except the highest. If the parent is , + // then we don't want to put items before or + (value < 0 && undefinedOtherZIndex && !svgParent && !i) + ) { + parentNode.insertBefore(element, childNodes[i]); + inserted = true; + } else if ( + // Insert after the first element with a lower zIndex + pInt(otherZIndex) <= value || + // If negative zIndex, add this before first undefined zIndex element + (undefinedOtherZIndex && (!defined(value) || value >= 0)) + ) { + parentNode.insertBefore( + element, + childNodes[i + 1] || null // null for oldIE export + ); + inserted = true; + } + } + } + + if (!inserted) { + parentNode.insertBefore( + element, + childNodes[svgParent ? 3 : 0] || null // null for oldIE + ); + inserted = true; + } + } + return inserted; + }, + _defaultSetter: function (value, key, element) { + element.setAttribute(key, value); + } +}); + +// Some shared setters and getters +SVGElement.prototype.yGetter = +SVGElement.prototype.xGetter; +SVGElement.prototype.translateXSetter = +SVGElement.prototype.translateYSetter = +SVGElement.prototype.rotationSetter = +SVGElement.prototype.verticalAlignSetter = +SVGElement.prototype.rotationOriginXSetter = +SVGElement.prototype.rotationOriginYSetter = +SVGElement.prototype.scaleXSetter = +SVGElement.prototype.scaleYSetter = +SVGElement.prototype.matrixSetter = function (value, key) { + this[key] = value; + this.doTransform = true; +}; + + +// WebKit and Batik have problems with a stroke-width of zero, so in this case +// we remove the stroke attribute altogether. #1270, #1369, #3065, #3072. +SVGElement.prototype['stroke-widthSetter'] = +SVGElement.prototype.strokeSetter = function (value, key, element) { + this[key] = value; + // Only apply the stroke attribute if the stroke width is defined and larger + // than 0 + if (this.stroke && this['stroke-width']) { + // Use prototype as instance may be overridden + SVGElement.prototype.fillSetter.call( + this, + this.stroke, + 'stroke', + element + ); + + element.setAttribute('stroke-width', this['stroke-width']); + this.hasStroke = true; + } else if (key === 'stroke-width' && value === 0 && this.hasStroke) { + element.removeAttribute('stroke'); + this.hasStroke = false; + } +}; + + +/** + * Allows direct access to the Highcharts rendering layer in order to draw + * primitive shapes like circles, rectangles, paths or text directly on a chart, + * or independent from any chart. The SVGRenderer represents a wrapper object + * for SVG in modern browsers. Through the VMLRenderer, part of the `oldie.js` + * module, it also brings vector graphics to IE <= 8. + * + * An existing chart's renderer can be accessed through {@link Chart.renderer}. + * The renderer can also be used completely decoupled from a chart. + * + * @param {HTMLDOMElement} container - Where to put the SVG in the web page. + * @param {number} width - The width of the SVG. + * @param {number} height - The height of the SVG. + * @param {boolean} [forExport=false] - Whether the rendered content is intended + * for export. + * @param {boolean} [allowHTML=true] - Whether the renderer is allowed to + * include HTML text, which will be projected on top of the SVG. + * + * @example + * // Use directly without a chart object. + * var renderer = new Highcharts.Renderer(parentNode, 600, 400); + * + * @sample highcharts/members/renderer-on-chart + * Annotating a chart programmatically. + * @sample highcharts/members/renderer-basic + * Independent SVG drawing. + * + * @class Highcharts.SVGRenderer + */ +SVGRenderer = H.SVGRenderer = function () { + this.init.apply(this, arguments); +}; +extend(SVGRenderer.prototype, /** @lends Highcharts.SVGRenderer.prototype */ { + /** + * A pointer to the renderer's associated Element class. The VMLRenderer + * will have a pointer to VMLElement here. + * @type {SVGElement} + */ + Element: SVGElement, + SVG_NS: SVG_NS, + /** + * Initialize the SVGRenderer. Overridable initiator function that takes + * the same parameters as the constructor. + */ + init: function (container, width, height, style, forExport, allowHTML) { + var renderer = this, + boxWrapper, + element, + desc; + + boxWrapper = renderer.createElement('svg') + .attr({ + 'version': '1.1', + 'class': 'highcharts-root' + }) + + .css(this.getStyle(style)) + ; + element = boxWrapper.element; + container.appendChild(element); + + // Always use ltr on the container, otherwise text-anchor will be + // flipped and text appear outside labels, buttons, tooltip etc (#3482) + attr(container, 'dir', 'ltr'); + + // For browsers other than IE, add the namespace attribute (#1978) + if (container.innerHTML.indexOf('xmlns') === -1) { + attr(element, 'xmlns', this.SVG_NS); + } + + // object properties + renderer.isSVG = true; + + /** + * The root `svg` node of the renderer. + * @name box + * @memberOf SVGRenderer + * @type {SVGDOMElement} + */ + this.box = element; + /** + * The wrapper for the root `svg` node of the renderer. + * + * @name boxWrapper + * @memberOf SVGRenderer + * @type {SVGElement} + */ + this.boxWrapper = boxWrapper; + renderer.alignedObjects = []; + + /** + * Page url used for internal references. + * @type {string} + */ + // #24, #672, #1070 + this.url = ( + (isFirefox || isWebKit) && + doc.getElementsByTagName('base').length + ) ? + win.location.href + .replace(/#.*?$/, '') // remove the hash + .replace(/<[^>]*>/g, '') // wing cut HTML + // escape parantheses and quotes + .replace(/([\('\)])/g, '\\$1') + // replace spaces (needed for Safari only) + .replace(/ /g, '%20') : + ''; + + // Add description + desc = this.createElement('desc').add(); + desc.element.appendChild( + doc.createTextNode('Created with Highcharts 5aaca1890988fc7073f112a2ff1556645b8dda5e') + ); + + /** + * A pointer to the `defs` node of the root SVG. + * @type {SVGElement} + * @name defs + * @memberOf SVGRenderer + */ + renderer.defs = this.createElement('defs').add(); + renderer.allowHTML = allowHTML; + renderer.forExport = forExport; + renderer.gradients = {}; // Object where gradient SvgElements are stored + renderer.cache = {}; // Cache for numerical bounding boxes + renderer.cacheKeys = []; + renderer.imgCount = 0; + + renderer.setSize(width, height, false); + + + + // Issue 110 workaround: + // In Firefox, if a div is positioned by percentage, its pixel position + // may land between pixels. The container itself doesn't display this, + // but an SVG element inside this container will be drawn at subpixel + // precision. In order to draw sharp lines, this must be compensated + // for. This doesn't seem to work inside iframes though (like in + // jsFiddle). + var subPixelFix, rect; + if (isFirefox && container.getBoundingClientRect) { + subPixelFix = function () { + css(container, { left: 0, top: 0 }); + rect = container.getBoundingClientRect(); + css(container, { + left: (Math.ceil(rect.left) - rect.left) + 'px', + top: (Math.ceil(rect.top) - rect.top) + 'px' + }); + }; + + // run the fix now + subPixelFix(); + + // run it on resize + renderer.unSubPixelFix = addEvent(win, 'resize', subPixelFix); + } + }, + + + + /** + * Get the global style setting for the renderer. + * @private + * @param {CSSObject} style - Style settings. + * @return {CSSObject} The style settings mixed with defaults. + */ + getStyle: function (style) { + this.style = extend({ + + fontFamily: '"Lucida Grande", "Lucida Sans Unicode", ' + + 'Arial, Helvetica, sans-serif', + fontSize: '12px' + + }, style); + return this.style; + }, + /** + * Apply the global style on the renderer, mixed with the default styles. + * + * @param {CSSObject} style - CSS to apply. + */ + setStyle: function (style) { + this.boxWrapper.css(this.getStyle(style)); + }, + + + /** + * Detect whether the renderer is hidden. This happens when one of the + * parent elements has `display: none`. Used internally to detect when we + * needto render preliminarily in another div to get the text bounding boxes + * right. + * + * @returns {boolean} True if it is hidden. + */ + isHidden: function () { // #608 + return !this.boxWrapper.getBBox().width; + }, + + /** + * Destroys the renderer and its allocated members. + */ + destroy: function () { + var renderer = this, + rendererDefs = renderer.defs; + renderer.box = null; + renderer.boxWrapper = renderer.boxWrapper.destroy(); + + // Call destroy on all gradient elements + destroyObjectProperties(renderer.gradients || {}); + renderer.gradients = null; + + // Defs are null in VMLRenderer + // Otherwise, destroy them here. + if (rendererDefs) { + renderer.defs = rendererDefs.destroy(); + } + + // Remove sub pixel fix handler (#982) + if (renderer.unSubPixelFix) { + renderer.unSubPixelFix(); + } + + renderer.alignedObjects = null; + + return null; + }, + + /** + * Create a wrapper for an SVG element. Serves as a factory for + * {@link SVGElement}, but this function is itself mostly called from + * primitive factories like {@link SVGRenderer#path}, {@link + * SVGRenderer#rect} or {@link SVGRenderer#text}. + * + * @param {string} nodeName - The node name, for example `rect`, `g` etc. + * @returns {SVGElement} The generated SVGElement. + */ + createElement: function (nodeName) { + var wrapper = new this.Element(); + wrapper.init(this, nodeName); + return wrapper; + }, + + /** + * Dummy function for plugins, called every time the renderer is updated. + * Prior to Highcharts 5, this was used for the canvg renderer. + * @function + */ + draw: noop, + + /** + * Get converted radial gradient attributes according to the radial + * reference. Used internally from the {@link SVGElement#colorGradient} + * function. + * + * @private + */ + getRadialAttr: function (radialReference, gradAttr) { + return { + cx: (radialReference[0] - radialReference[2] / 2) + + gradAttr.cx * radialReference[2], + cy: (radialReference[1] - radialReference[2] / 2) + + gradAttr.cy * radialReference[2], + r: gradAttr.r * radialReference[2] + }; + }, + + /** + * Extendable function to measure the tspan width. + * + * @private + */ + getSpanWidth: function (wrapper) { + return wrapper.getBBox(true).width; + }, + + applyEllipsis: function (wrapper, tspan, text, width) { + var renderer = this, + rotation = wrapper.rotation, + str = text, + currentIndex, + minIndex = 0, + maxIndex = text.length, + updateTSpan = function (s) { + tspan.removeChild(tspan.firstChild); + if (s) { + tspan.appendChild(doc.createTextNode(s)); + } + }, + actualWidth, + wasTooLong; + wrapper.rotation = 0; // discard rotation when computing box + actualWidth = renderer.getSpanWidth(wrapper, tspan); + wasTooLong = actualWidth > width; + if (wasTooLong) { + while (minIndex <= maxIndex) { + currentIndex = Math.ceil((minIndex + maxIndex) / 2); + str = text.substring(0, currentIndex) + '\u2026'; + updateTSpan(str); + actualWidth = renderer.getSpanWidth(wrapper, tspan); + if (minIndex === maxIndex) { + // Complete + minIndex = maxIndex + 1; + } else if (actualWidth > width) { + // Too large. Set max index to current. + maxIndex = currentIndex - 1; + } else { + // Within width. Set min index to current. + minIndex = currentIndex; + } + } + // If max index was 0 it means just ellipsis was also to large. + if (maxIndex === 0) { + // Remove ellipses. + updateTSpan(''); + } + } + wrapper.rotation = rotation; // Apply rotation again. + return wasTooLong; + }, + + /** + * A collection of characters mapped to HTML entities. When `useHTML` on an + * element is true, these entities will be rendered correctly by HTML. In + * the SVG pseudo-HTML, they need to be unescaped back to simple characters, + * so for example `<` will render as `<`. + * + * @example + * // Add support for unescaping quotes + * Highcharts.SVGRenderer.prototype.escapes['"'] = '"'; + * + * @type {Object} + */ + escapes: { + '&': '&', + '<': '<', + '>': '>', + "'": ''', // eslint-disable-line quotes + '"': '"' + }, + + /** + * Parse a simple HTML string into SVG tspans. Called internally when text + * is set on an SVGElement. The function supports a subset of HTML tags, + * CSS text features like `width`, `text-overflow`, `white-space`, and + * also attributes like `href` and `style`. + * @private + * @param {SVGElement} wrapper The parent SVGElement. + */ + buildText: function (wrapper) { + var textNode = wrapper.element, + renderer = this, + forExport = renderer.forExport, + textStr = pick(wrapper.textStr, '').toString(), + hasMarkup = textStr.indexOf('<') !== -1, + lines, + childNodes = textNode.childNodes, + clsRegex, + styleRegex, + hrefRegex, + wasTooLong, + parentX = attr(textNode, 'x'), + textStyles = wrapper.styles, + width = wrapper.textWidth, + textLineHeight = textStyles && textStyles.lineHeight, + textOutline = textStyles && textStyles.textOutline, + ellipsis = textStyles && textStyles.textOverflow === 'ellipsis', + noWrap = textStyles && textStyles.whiteSpace === 'nowrap', + fontSize = textStyles && textStyles.fontSize, + textCache, + isSubsequentLine, + i = childNodes.length, + tempParent = width && !wrapper.added && this.box, + getLineHeight = function (tspan) { + var fontSizeStyle; + + fontSizeStyle = /(px|em)$/.test(tspan && tspan.style.fontSize) ? + tspan.style.fontSize : + (fontSize || renderer.style.fontSize || 12); + + + return textLineHeight ? + pInt(textLineHeight) : + renderer.fontMetrics( + fontSizeStyle, + // Get the computed size from parent if not explicit + tspan.getAttribute('style') ? tspan : textNode + ).h; + }, + unescapeEntities = function (inputStr) { + objectEach(renderer.escapes, function (value, key) { + inputStr = inputStr.replace( + new RegExp(value, 'g'), + key + ); + }); + return inputStr; + }; + + // The buildText code is quite heavy, so if we're not changing something + // that affects the text, skip it (#6113). + textCache = [ + textStr, + ellipsis, + noWrap, + textLineHeight, + textOutline, + fontSize, + width + ].join(','); + if (textCache === wrapper.textCache) { + return; + } + wrapper.textCache = textCache; + + // Remove old text + while (i--) { + textNode.removeChild(childNodes[i]); + } + + // Skip tspans, add text directly to text node. The forceTSpan is a hook + // used in text outline hack. + if ( + !hasMarkup && + !textOutline && + !ellipsis && + !width && + textStr.indexOf(' ') === -1 + ) { + textNode.appendChild(doc.createTextNode(unescapeEntities(textStr))); + + // Complex strings, add more logic + } else { + + clsRegex = /<.*class="([^"]+)".*>/; + styleRegex = /<.*style="([^"]+)".*>/; + hrefRegex = /<.*href="([^"]+)".*>/; + + if (tempParent) { + // attach it to the DOM to read offset width + tempParent.appendChild(textNode); + } + + if (hasMarkup) { + lines = textStr + + .replace(/<(b|strong)>/g, '') + .replace(/<(i|em)>/g, '') + + .replace(//g, '') + .split(//g); + + } else { + lines = [textStr]; + } + + + // Trim empty lines (#5261) + lines = grep(lines, function (line) { + return line !== ''; + }); + + + // build the lines + each(lines, function buildTextLines(line, lineNo) { + var spans, + spanNo = 0; + line = line + // Trim to prevent useless/costly process on the spaces + // (#5258) + .replace(/^\s+|\s+$/g, '') + .replace(//g, '|||'); + spans = line.split('|||'); + + each(spans, function buildTextSpans(span) { + if (span !== '' || spans.length === 1) { + var attributes = {}, + tspan = doc.createElementNS( + renderer.SVG_NS, + 'tspan' + ), + spanCls, + spanStyle; // #390 + if (clsRegex.test(span)) { + spanCls = span.match(clsRegex)[1]; + attr(tspan, 'class', spanCls); + } + if (styleRegex.test(span)) { + spanStyle = span.match(styleRegex)[1].replace( + /(;| |^)color([ :])/, + '$1fill$2' + ); + attr(tspan, 'style', spanStyle); + } + + // Not for export - #1529 + if (hrefRegex.test(span) && !forExport) { + attr( + tspan, + 'onclick', + 'location.href=\"' + + span.match(hrefRegex)[1] + '\"' + ); + attr(tspan, 'class', 'highcharts-anchor'); + + css(tspan, { cursor: 'pointer' }); + + } + + // Strip away unsupported HTML tags (#7126) + span = unescapeEntities( + span.replace(/<[a-zA-Z\/](.|\n)*?>/g, '') || ' ' + ); + + // Nested tags aren't supported, and cause crash in + // Safari (#1596) + if (span !== ' ') { + + // add the text node + tspan.appendChild(doc.createTextNode(span)); + + // First span in a line, align it to the left + if (!spanNo) { + if (lineNo && parentX !== null) { + attributes.x = parentX; + } + } else { + attributes.dx = 0; // #16 + } + + // add attributes + attr(tspan, attributes); + + // Append it + textNode.appendChild(tspan); + + // first span on subsequent line, add the line + // height + if (!spanNo && isSubsequentLine) { + + // allow getting the right offset height in + // exporting in IE + if (!svg && forExport) { + css(tspan, { display: 'block' }); + } + + // Set the line height based on the font size of + // either the text element or the tspan element + attr( + tspan, + 'dy', + getLineHeight(tspan) + ); + } + + /* + // Experimental text wrapping based on + // getSubstringLength + if (width) { + var spans = renderer.breakText(wrapper, width); + + each(spans, function (span) { + + var dy = getLineHeight(tspan); + tspan = doc.createElementNS( + SVG_NS, + 'tspan' + ); + tspan.appendChild( + doc.createTextNode(span) + ); + attr(tspan, { + dy: dy, + x: parentX + }); + if (spanStyle) { // #390 + attr(tspan, 'style', spanStyle); + } + textNode.appendChild(tspan); + }); + + } + // */ + + // Check width and apply soft breaks or ellipsis + if (width) { + var words = span.replace( + /([^\^])-/g, + '$1- ' + ).split(' '), // #1273 + hasWhiteSpace = ( + spans.length > 1 || + lineNo || + (words.length > 1 && !noWrap) + ), + tooLong, + rest = [], + actualWidth, + dy = getLineHeight(tspan), + rotation = wrapper.rotation; + + if (ellipsis) { + wasTooLong = renderer.applyEllipsis( + wrapper, + tspan, + span, + width + ); + } + + while ( + !ellipsis && + hasWhiteSpace && + (words.length || rest.length) + ) { + // discard rotation when computing box + wrapper.rotation = 0; + actualWidth = renderer.getSpanWidth( + wrapper, + tspan + ); + tooLong = actualWidth > width; + + // For ellipsis, do a binary search for the + // correct string length + if (wasTooLong === undefined) { + wasTooLong = tooLong; // First time + } + + // Looping down, this is the first word + // sequence that is not too long, so we can + // move on to build the next line. + if (!tooLong || words.length === 1) { + words = rest; + rest = []; + + if (words.length && !noWrap) { + tspan = doc.createElementNS( + SVG_NS, + 'tspan' + ); + attr(tspan, { + dy: dy, + x: parentX + }); + if (spanStyle) { // #390 + attr(tspan, 'style', spanStyle); + } + textNode.appendChild(tspan); + } + + // a single word is pressing it out + if (actualWidth > width) { + width = actualWidth; + } + } else { // append to existing line tspan + tspan.removeChild(tspan.firstChild); + rest.unshift(words.pop()); + } + if (words.length) { + tspan.appendChild( + doc.createTextNode( + words.join(' ') + .replace(/- /g, '-') + ) + ); + } + } + wrapper.rotation = rotation; + } + + spanNo++; + // */ + } + } + }); + // To avoid beginning lines that doesn't add to the textNode + // (#6144) + isSubsequentLine = ( + isSubsequentLine || + textNode.childNodes.length + ); + }); + + if (wasTooLong) { + wrapper.attr( + 'title', + unescapeEntities(wrapper.textStr) // #7179 + ); + } + if (tempParent) { + tempParent.removeChild(textNode); + } + + // Apply the text outline + if (textOutline && wrapper.applyTextOutline) { + wrapper.applyTextOutline(textOutline); + } + } + }, + + + + /* + breakText: function (wrapper, width) { + var bBox = wrapper.getBBox(), + node = wrapper.element, + charnum = node.textContent.length, + stringWidth, + // try this position first, based on average character width + guessedLineCharLength = Math.round(width * charnum / bBox.width), + pos = guessedLineCharLength, + spans = [], + increment = 0, + startPos = 0, + endPos, + safe = 0; + + if (bBox.width > width) { + while (startPos < charnum && safe < 100) { + + while (endPos === undefined && safe < 100) { + stringWidth = node.getSubStringLength( + startPos, + pos - startPos + ); + + if (stringWidth <= width) { + if (increment === -1) { + endPos = pos; + } else { + increment = 1; + } + } else { + if (increment === 1) { + endPos = pos - 1; + } else { + increment = -1; + } + } + pos += increment; + safe++; + } + + spans.push(node.textContent.substr(startPos, endPos - startPos)); + + startPos = endPos; + pos = startPos + guessedLineCharLength; + endPos = undefined; + } + } + + return spans; + }, + // */ + + /** + * Returns white for dark colors and black for bright colors. + * + * @param {ColorString} rgba - The color to get the contrast for. + * @returns {string} The contrast color, either `#000000` or `#FFFFFF`. + */ + getContrast: function (rgba) { + rgba = color(rgba).rgba; + + // The threshold may be discussed. Here's a proposal for adding + // different weight to the color channels (#6216) + /* + rgba[0] *= 1; // red + rgba[1] *= 1.2; // green + rgba[2] *= 0.7; // blue + */ + + return rgba[0] + rgba[1] + rgba[2] > 2 * 255 ? '#000000' : '#FFFFFF'; + }, + + /** + * Create a button with preset states. + * @param {string} text - The text or HTML to draw. + * @param {number} x - The x position of the button's left side. + * @param {number} y - The y position of the button's top side. + * @param {Function} callback - The function to execute on button click or + * touch. + * @param {SVGAttributes} [normalState] - SVG attributes for the normal + * state. + * @param {SVGAttributes} [hoverState] - SVG attributes for the hover state. + * @param {SVGAttributes} [pressedState] - SVG attributes for the pressed + * state. + * @param {SVGAttributes} [disabledState] - SVG attributes for the disabled + * state. + * @param {Symbol} [shape=rect] - The shape type. + * @returns {SVGRenderer} The button element. + */ + button: function ( + text, + x, + y, + callback, + normalState, + hoverState, + pressedState, + disabledState, + shape + ) { + var label = this.label( + text, + x, + y, + shape, + null, + null, + null, + null, + 'button' + ), + curState = 0; + + // Default, non-stylable attributes + label.attr(merge({ + 'padding': 8, + 'r': 2 + }, normalState)); + + + // Presentational + var normalStyle, + hoverStyle, + pressedStyle, + disabledStyle; + + // Normal state - prepare the attributes + normalState = merge({ + fill: '#f7f7f7', + stroke: '#cccccc', + 'stroke-width': 1, + style: { + color: '#333333', + cursor: 'pointer', + fontWeight: 'normal' + } + }, normalState); + normalStyle = normalState.style; + delete normalState.style; + + // Hover state + hoverState = merge(normalState, { + fill: '#e6e6e6' + }, hoverState); + hoverStyle = hoverState.style; + delete hoverState.style; + + // Pressed state + pressedState = merge(normalState, { + fill: '#e6ebf5', + style: { + color: '#000000', + fontWeight: 'bold' + } + }, pressedState); + pressedStyle = pressedState.style; + delete pressedState.style; + + // Disabled state + disabledState = merge(normalState, { + style: { + color: '#cccccc' + } + }, disabledState); + disabledStyle = disabledState.style; + delete disabledState.style; + + + // Add the events. IE9 and IE10 need mouseover and mouseout to funciton + // (#667). + addEvent(label.element, isMS ? 'mouseover' : 'mouseenter', function () { + if (curState !== 3) { + label.setState(1); + } + }); + addEvent(label.element, isMS ? 'mouseout' : 'mouseleave', function () { + if (curState !== 3) { + label.setState(curState); + } + }); + + label.setState = function (state) { + // Hover state is temporary, don't record it + if (state !== 1) { + label.state = curState = state; + } + // Update visuals + label.removeClass( + /highcharts-button-(normal|hover|pressed|disabled)/ + ) + .addClass( + 'highcharts-button-' + + ['normal', 'hover', 'pressed', 'disabled'][state || 0] + ); + + + label.attr([ + normalState, + hoverState, + pressedState, + disabledState + ][state || 0]) + .css([ + normalStyle, + hoverStyle, + pressedStyle, + disabledStyle + ][state || 0]); + + }; + + + + // Presentational attributes + label + .attr(normalState) + .css(extend({ cursor: 'default' }, normalStyle)); + + + return label + .on('click', function (e) { + if (curState !== 3) { + callback.call(label, e); + } + }); + }, + + /** + * Make a straight line crisper by not spilling out to neighbour pixels. + * + * @param {Array} points - The original points on the format `['M', 0, 0, + * 'L', 100, 0]`. + * @param {number} width - The width of the line. + * @returns {Array} The original points array, but modified to render + * crisply. + */ + crispLine: function (points, width) { + // normalize to a crisp line + if (points[1] === points[4]) { + // Substract due to #1129. Now bottom and left axis gridlines behave + // the same. + points[1] = points[4] = Math.round(points[1]) - (width % 2 / 2); + } + if (points[2] === points[5]) { + points[2] = points[5] = Math.round(points[2]) + (width % 2 / 2); + } + return points; + }, + + + /** + * Draw a path, wraps the SVG `path` element. + * + * @param {Array} [path] An SVG path definition in array form. + * + * @example + * var path = renderer.path(['M', 10, 10, 'L', 30, 30, 'z']) + * .attr({ stroke: '#ff00ff' }) + * .add(); + * @returns {SVGElement} The generated wrapper element. + * + * @sample highcharts/members/renderer-path-on-chart/ + * Draw a path in a chart + * @sample highcharts/members/renderer-path/ + * Draw a path independent from a chart + * + *//** + * Draw a path, wraps the SVG `path` element. + * + * @param {SVGAttributes} [attribs] The initial attributes. + * @returns {SVGElement} The generated wrapper element. + */ + path: function (path) { + var attribs = { + + fill: 'none' + + }; + if (isArray(path)) { + attribs.d = path; + } else if (isObject(path)) { // attributes + extend(attribs, path); + } + return this.createElement('path').attr(attribs); + }, + + /** + * Draw a circle, wraps the SVG `circle` element. + * + * @param {number} [x] The center x position. + * @param {number} [y] The center y position. + * @param {number} [r] The radius. + * @returns {SVGElement} The generated wrapper element. + * + * @sample highcharts/members/renderer-circle/ Drawing a circle + *//** + * Draw a circle, wraps the SVG `circle` element. + * + * @param {SVGAttributes} [attribs] The initial attributes. + * @returns {SVGElement} The generated wrapper element. + */ + circle: function (x, y, r) { + var attribs = isObject(x) ? x : { x: x, y: y, r: r }, + wrapper = this.createElement('circle'); + + // Setting x or y translates to cx and cy + wrapper.xSetter = wrapper.ySetter = function (value, key, element) { + element.setAttribute('c' + key, value); + }; + + return wrapper.attr(attribs); + }, + + /** + * Draw and return an arc. + * @param {number} [x=0] Center X position. + * @param {number} [y=0] Center Y position. + * @param {number} [r=0] The outer radius of the arc. + * @param {number} [innerR=0] Inner radius like used in donut charts. + * @param {number} [start=0] The starting angle of the arc in radians, where + * 0 is to the right and `-Math.PI/2` is up. + * @param {number} [end=0] The ending angle of the arc in radians, where 0 + * is to the right and `-Math.PI/2` is up. + * @returns {SVGElement} The generated wrapper element. + * + * @sample highcharts/members/renderer-arc/ + * Drawing an arc + *//** + * Draw and return an arc. Overloaded function that takes arguments object. + * @param {SVGAttributes} attribs Initial SVG attributes. + * @returns {SVGElement} The generated wrapper element. + */ + arc: function (x, y, r, innerR, start, end) { + var arc, + options; + + if (isObject(x)) { + options = x; + y = options.y; + r = options.r; + innerR = options.innerR; + start = options.start; + end = options.end; + x = options.x; + } else { + options = { + innerR: innerR, + start: start, + end: end + }; + } + + // Arcs are defined as symbols for the ability to set + // attributes in attr and animate + arc = this.symbol('arc', x, y, r, r, options); + arc.r = r; // #959 + return arc; + }, + + /** + * Draw and return a rectangle. + * @param {number} [x] Left position. + * @param {number} [y] Top position. + * @param {number} [width] Width of the rectangle. + * @param {number} [height] Height of the rectangle. + * @param {number} [r] Border corner radius. + * @param {number} [strokeWidth] A stroke width can be supplied to allow + * crisp drawing. + * @returns {SVGElement} The generated wrapper element. + *//** + * Draw and return a rectangle. + * @param {SVGAttributes} [attributes] + * General SVG attributes for the rectangle. + * @return {SVGElement} + * The generated wrapper element. + * + * @sample highcharts/members/renderer-rect-on-chart/ + * Draw a rectangle in a chart + * @sample highcharts/members/renderer-rect/ + * Draw a rectangle independent from a chart + */ + rect: function (x, y, width, height, r, strokeWidth) { + + r = isObject(x) ? x.r : r; + + var wrapper = this.createElement('rect'), + attribs = isObject(x) ? x : x === undefined ? {} : { + x: x, + y: y, + width: Math.max(width, 0), + height: Math.max(height, 0) + }; + + + if (strokeWidth !== undefined) { + attribs.strokeWidth = strokeWidth; + attribs = wrapper.crisp(attribs); + } + attribs.fill = 'none'; + + + if (r) { + attribs.r = r; + } + + wrapper.rSetter = function (value, key, element) { + attr(element, { + rx: value, + ry: value + }); + }; + + return wrapper.attr(attribs); + }, + + /** + * Resize the {@link SVGRenderer#box} and re-align all aligned child + * elements. + * @param {number} width + * The new pixel width. + * @param {number} height + * The new pixel height. + * @param {Boolean|AnimationOptions} [animate=true] + * Whether and how to animate. + */ + setSize: function (width, height, animate) { + var renderer = this, + alignedObjects = renderer.alignedObjects, + i = alignedObjects.length; + + renderer.width = width; + renderer.height = height; + + renderer.boxWrapper.animate({ + width: width, + height: height + }, { + step: function () { + this.attr({ + viewBox: '0 0 ' + this.attr('width') + ' ' + + this.attr('height') + }); + }, + duration: pick(animate, true) ? undefined : 0 + }); + + while (i--) { + alignedObjects[i].align(); + } + }, + + /** + * Create and return an svg group element. Child + * {@link Highcharts.SVGElement} objects are added to the group by using the + * group as the first parameter + * in {@link Highcharts.SVGElement#add|add()}. + * + * @param {string} [name] The group will be given a class name of + * `highcharts-{name}`. This can be used for styling and scripting. + * @returns {SVGElement} The generated wrapper element. + * + * @sample highcharts/members/renderer-g/ + * Show and hide grouped objects + */ + g: function (name) { + var elem = this.createElement('g'); + return name ? elem.attr({ 'class': 'highcharts-' + name }) : elem; + }, + + /** + * Display an image. + * @param {string} src The image source. + * @param {number} [x] The X position. + * @param {number} [y] The Y position. + * @param {number} [width] The image width. If omitted, it defaults to the + * image file width. + * @param {number} [height] The image height. If omitted it defaults to the + * image file height. + * @returns {SVGElement} The generated wrapper element. + * + * @sample highcharts/members/renderer-image-on-chart/ + * Add an image in a chart + * @sample highcharts/members/renderer-image/ + * Add an image independent of a chart + */ + image: function (src, x, y, width, height) { + var attribs = { + preserveAspectRatio: 'none' + }, + elemWrapper; + + // optional properties + if (arguments.length > 1) { + extend(attribs, { + x: x, + y: y, + width: width, + height: height + }); + } + + elemWrapper = this.createElement('image').attr(attribs); + + // set the href in the xlink namespace + if (elemWrapper.element.setAttributeNS) { + elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', + 'href', src); + } else { + // could be exporting in IE + // using href throws "not supported" in ie7 and under, requries + // regex shim to fix later + elemWrapper.element.setAttribute('hc-svg-href', src); + } + return elemWrapper; + }, + + /** + * Draw a symbol out of pre-defined shape paths from + * {@link SVGRenderer#symbols}. + * It is used in Highcharts for point makers, which cake a `symbol` option, + * and label and button backgrounds like in the tooltip and stock flags. + * + * @param {Symbol} symbol - The symbol name. + * @param {number} x - The X coordinate for the top left position. + * @param {number} y - The Y coordinate for the top left position. + * @param {number} width - The pixel width. + * @param {number} height - The pixel height. + * @param {Object} [options] - Additional options, depending on the actual + * symbol drawn. + * @param {number} [options.anchorX] - The anchor X position for the + * `callout` symbol. This is where the chevron points to. + * @param {number} [options.anchorY] - The anchor Y position for the + * `callout` symbol. This is where the chevron points to. + * @param {number} [options.end] - The end angle of an `arc` symbol. + * @param {boolean} [options.open] - Whether to draw `arc` symbol open or + * closed. + * @param {number} [options.r] - The radius of an `arc` symbol, or the + * border radius for the `callout` symbol. + * @param {number} [options.start] - The start angle of an `arc` symbol. + */ + symbol: function (symbol, x, y, width, height, options) { + + var ren = this, + obj, + imageRegex = /^url\((.*?)\)$/, + isImage = imageRegex.test(symbol), + sym = !isImage && (this.symbols[symbol] ? symbol : 'circle'), + + + // get the symbol definition function + symbolFn = sym && this.symbols[sym], + + // check if there's a path defined for this symbol + path = defined(x) && symbolFn && symbolFn.call( + this.symbols, + Math.round(x), + Math.round(y), + width, + height, + options + ), + imageSrc, + centerImage; + + if (symbolFn) { + obj = this.path(path); + + + obj.attr('fill', 'none'); + + + // expando properties for use in animate and attr + extend(obj, { + symbolName: sym, + x: x, + y: y, + width: width, + height: height + }); + if (options) { + extend(obj, options); + } + + + // Image symbols + } else if (isImage) { + + + imageSrc = symbol.match(imageRegex)[1]; + + // Create the image synchronously, add attribs async + obj = this.image(imageSrc); + + // The image width is not always the same as the symbol width. The + // image may be centered within the symbol, as is the case when + // image shapes are used as label backgrounds, for example in flags. + obj.imgwidth = pick( + symbolSizes[imageSrc] && symbolSizes[imageSrc].width, + options && options.width + ); + obj.imgheight = pick( + symbolSizes[imageSrc] && symbolSizes[imageSrc].height, + options && options.height + ); + /** + * Set the size and position + */ + centerImage = function () { + obj.attr({ + width: obj.width, + height: obj.height + }); + }; + + /** + * Width and height setters that take both the image's physical size + * and the label size into consideration, and translates the image + * to center within the label. + */ + each(['width', 'height'], function (key) { + obj[key + 'Setter'] = function (value, key) { + var attribs = {}, + imgSize = this['img' + key], + trans = key === 'width' ? 'translateX' : 'translateY'; + this[key] = value; + if (defined(imgSize)) { + if (this.element) { + this.element.setAttribute(key, imgSize); + } + if (!this.alignByTranslate) { + attribs[trans] = ((this[key] || 0) - imgSize) / 2; + this.attr(attribs); + } + } + }; + }); + + + if (defined(x)) { + obj.attr({ + x: x, + y: y + }); + } + obj.isImg = true; + + if (defined(obj.imgwidth) && defined(obj.imgheight)) { + centerImage(); + } else { + // Initialize image to be 0 size so export will still function + // if there's no cached sizes. + obj.attr({ width: 0, height: 0 }); + + // Create a dummy JavaScript image to get the width and height. + createElement('img', { + onload: function () { + + var chart = charts[ren.chartIndex]; + + // Special case for SVGs on IE11, the width is not + // accessible until the image is part of the DOM + // (#2854). + if (this.width === 0) { + css(this, { + position: 'absolute', + top: '-999em' + }); + doc.body.appendChild(this); + } + + // Center the image + symbolSizes[imageSrc] = { // Cache for next + width: this.width, + height: this.height + }; + obj.imgwidth = this.width; + obj.imgheight = this.height; + + if (obj.element) { + centerImage(); + } + + // Clean up after #2854 workaround. + if (this.parentNode) { + this.parentNode.removeChild(this); + } + + // Fire the load event when all external images are + // loaded + ren.imgCount--; + if (!ren.imgCount && chart && chart.onload) { + chart.onload(); + } + }, + src: imageSrc + }); + this.imgCount++; + } + } + + return obj; + }, + + /** + * @typedef {string} Symbol + * + * Can be one of `arc`, `callout`, `circle`, `diamond`, `square`, + * `triangle`, `triangle-down`. Symbols are used internally for point + * markers, button and label borders and backgrounds, or custom shapes. + * Extendable by adding to {@link SVGRenderer#symbols}. + */ + /** + * An extendable collection of functions for defining symbol paths. + */ + symbols: { + 'circle': function (x, y, w, h) { + // Return a full arc + return this.arc(x + w / 2, y + h / 2, w / 2, h / 2, { + start: 0, + end: Math.PI * 2, + open: false + }); + }, + + 'square': function (x, y, w, h) { + return [ + 'M', x, y, + 'L', x + w, y, + x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle': function (x, y, w, h) { + return [ + 'M', x + w / 2, y, + 'L', x + w, y + h, + x, y + h, + 'Z' + ]; + }, + + 'triangle-down': function (x, y, w, h) { + return [ + 'M', x, y, + 'L', x + w, y, + x + w / 2, y + h, + 'Z' + ]; + }, + 'diamond': function (x, y, w, h) { + return [ + 'M', x + w / 2, y, + 'L', x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2, + 'Z' + ]; + }, + 'arc': function (x, y, w, h, options) { + var start = options.start, + rx = options.r || w, + ry = options.r || h || w, + proximity = 0.001, + fullCircle = + Math.abs(options.end - options.start - 2 * Math.PI) < + proximity, + // Substract a small number to prevent cos and sin of start and + // end from becoming equal on 360 arcs (related: #1561) + end = options.end - proximity, + innerRadius = options.innerR, + open = pick(options.open, fullCircle), + cosStart = Math.cos(start), + sinStart = Math.sin(start), + cosEnd = Math.cos(end), + sinEnd = Math.sin(end), + // Proximity takes care of rounding errors around PI (#6971) + longArc = options.end - start - Math.PI < proximity ? 0 : 1, + arc; + + arc = [ + 'M', + x + rx * cosStart, + y + ry * sinStart, + 'A', // arcTo + rx, // x radius + ry, // y radius + 0, // slanting + longArc, // long or short arc + 1, // clockwise + x + rx * cosEnd, + y + ry * sinEnd + ]; + + if (defined(innerRadius)) { + arc.push( + open ? 'M' : 'L', + x + innerRadius * cosEnd, + y + innerRadius * sinEnd, + 'A', // arcTo + innerRadius, // x radius + innerRadius, // y radius + 0, // slanting + longArc, // long or short arc + 0, // clockwise + x + innerRadius * cosStart, + y + innerRadius * sinStart + ); + } + + arc.push(open ? '' : 'Z'); // close + return arc; + }, + + /** + * Callout shape used for default tooltips, also used for rounded + * rectangles in VML + */ + callout: function (x, y, w, h, options) { + var arrowLength = 6, + halfDistance = 6, + r = Math.min((options && options.r) || 0, w, h), + safeDistance = r + halfDistance, + anchorX = options && options.anchorX, + anchorY = options && options.anchorY, + path; + + path = [ + 'M', x + r, y, + 'L', x + w - r, y, // top side + 'C', x + w, y, x + w, y, x + w, y + r, // top-right corner + 'L', x + w, y + h - r, // right side + 'C', x + w, y + h, x + w, y + h, x + w - r, y + h, // bottom-rgt + 'L', x + r, y + h, // bottom side + 'C', x, y + h, x, y + h, x, y + h - r, // bottom-left corner + 'L', x, y + r, // left side + 'C', x, y, x, y, x + r, y // top-left corner + ]; + + // Anchor on right side + if (anchorX && anchorX > w) { + + // Chevron + if ( + anchorY > y + safeDistance && + anchorY < y + h - safeDistance + ) { + path.splice(13, 3, + 'L', x + w, anchorY - halfDistance, + x + w + arrowLength, anchorY, + x + w, anchorY + halfDistance, + x + w, y + h - r + ); + + // Simple connector + } else { + path.splice(13, 3, + 'L', x + w, h / 2, + anchorX, anchorY, + x + w, h / 2, + x + w, y + h - r + ); + } + + // Anchor on left side + } else if (anchorX && anchorX < 0) { + + // Chevron + if ( + anchorY > y + safeDistance && + anchorY < y + h - safeDistance + ) { + path.splice(33, 3, + 'L', x, anchorY + halfDistance, + x - arrowLength, anchorY, + x, anchorY - halfDistance, + x, y + r + ); + + // Simple connector + } else { + path.splice(33, 3, + 'L', x, h / 2, + anchorX, anchorY, + x, h / 2, + x, y + r + ); + } + + } else if ( // replace bottom + anchorY && + anchorY > h && + anchorX > x + safeDistance && + anchorX < x + w - safeDistance + ) { + path.splice(23, 3, + 'L', anchorX + halfDistance, y + h, + anchorX, y + h + arrowLength, + anchorX - halfDistance, y + h, + x + r, y + h + ); + + } else if ( // replace top + anchorY && + anchorY < 0 && + anchorX > x + safeDistance && + anchorX < x + w - safeDistance + ) { + path.splice(3, 3, + 'L', anchorX - halfDistance, y, + anchorX, y - arrowLength, + anchorX + halfDistance, y, + w - r, y + ); + } + + return path; + } + }, + + /** + * @typedef {SVGElement} ClipRect - A clipping rectangle that can be applied + * to one or more {@link SVGElement} instances. It is instanciated with the + * {@link SVGRenderer#clipRect} function and applied with the {@link + * SVGElement#clip} function. + * + * @example + * var circle = renderer.circle(100, 100, 100) + * .attr({ fill: 'red' }) + * .add(); + * var clipRect = renderer.clipRect(100, 100, 100, 100); + * + * // Leave only the lower right quarter visible + * circle.clip(clipRect); + */ + /** + * Define a clipping rectangle. The clipping rectangle is later applied + * to {@link SVGElement} objects through the {@link SVGElement#clip} + * function. + * + * @param {String} id + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + * @returns {ClipRect} A clipping rectangle. + * + * @example + * var circle = renderer.circle(100, 100, 100) + * .attr({ fill: 'red' }) + * .add(); + * var clipRect = renderer.clipRect(100, 100, 100, 100); + * + * // Leave only the lower right quarter visible + * circle.clip(clipRect); + */ + clipRect: function (x, y, width, height) { + var wrapper, + id = H.uniqueKey(), + + clipPath = this.createElement('clipPath').attr({ + id: id + }).add(this.defs); + + wrapper = this.rect(x, y, width, height, 0).add(clipPath); + wrapper.id = id; + wrapper.clipPath = clipPath; + wrapper.count = 0; + + return wrapper; + }, + + + + + + /** + * Draw text. The text can contain a subset of HTML, like spans and anchors + * and some basic text styling of these. For more advanced features like + * border and background, use {@link Highcharts.SVGRenderer#label} instead. + * To update the text after render, run `text.attr({ text: 'New text' })`. + * @param {String} str + * The text of (subset) HTML to draw. + * @param {number} x + * The x position of the text's lower left corner. + * @param {number} y + * The y position of the text's lower left corner. + * @param {Boolean} [useHTML=false] + * Use HTML to render the text. + * + * @return {SVGElement} The text object. + * + * @sample highcharts/members/renderer-text-on-chart/ + * Annotate the chart freely + * @sample highcharts/members/renderer-on-chart/ + * Annotate with a border and in response to the data + * @sample highcharts/members/renderer-text/ + * Formatted text + */ + text: function (str, x, y, useHTML) { + + // declare variables + var renderer = this, + wrapper, + attribs = {}; + + if (useHTML && (renderer.allowHTML || !renderer.forExport)) { + return renderer.html(str, x, y); + } + + attribs.x = Math.round(x || 0); // X always needed for line-wrap logic + if (y) { + attribs.y = Math.round(y); + } + if (str || str === 0) { + attribs.text = str; + } + + wrapper = renderer.createElement('text') + .attr(attribs); + + if (!useHTML) { + wrapper.xSetter = function (value, key, element) { + var tspans = element.getElementsByTagName('tspan'), + tspan, + parentVal = element.getAttribute(key), + i; + for (i = 0; i < tspans.length; i++) { + tspan = tspans[i]; + // If the x values are equal, the tspan represents a + // linebreak + if (tspan.getAttribute(key) === parentVal) { + tspan.setAttribute(key, value); + } + } + element.setAttribute(key, value); + }; + } + + return wrapper; + }, + + /** + * Utility to return the baseline offset and total line height from the font + * size. + * + * @param {?string} fontSize The current font size to inspect. If not given, + * the font size will be found from the DOM element. + * @param {SVGElement|SVGDOMElement} [elem] The element to inspect for a + * current font size. + * @returns {Object} An object containing `h`: the line height, `b`: the + * baseline relative to the top of the box, and `f`: the font size. + */ + fontMetrics: function (fontSize, elem) { + var lineHeight, + baseline; + + + fontSize = fontSize || + // When the elem is a DOM element (#5932) + (elem && elem.style && elem.style.fontSize) || + // Fall back on the renderer style default + (this.style && this.style.fontSize); + + + + // Handle different units + if (/px/.test(fontSize)) { + fontSize = pInt(fontSize); + } else if (/em/.test(fontSize)) { + // The em unit depends on parent items + fontSize = parseFloat(fontSize) * + (elem ? this.fontMetrics(null, elem.parentNode).f : 16); + } else { + fontSize = 12; + } + + // Empirical values found by comparing font size and bounding box + // height. Applies to the default font family. + // http://jsfiddle.net/highcharts/7xvn7/ + lineHeight = fontSize < 24 ? fontSize + 3 : Math.round(fontSize * 1.2); + baseline = Math.round(lineHeight * 0.8); + + return { + h: lineHeight, + b: baseline, + f: fontSize + }; + }, + + /** + * Correct X and Y positioning of a label for rotation (#1764). + * + * @private + */ + rotCorr: function (baseline, rotation, alterY) { + var y = baseline; + if (rotation && alterY) { + y = Math.max(y * Math.cos(rotation * deg2rad), 4); + } + return { + x: (-baseline / 3) * Math.sin(rotation * deg2rad), + y: y + }; + }, + + /** + * Draw a label, which is an extended text element with support for border + * and background. Highcharts creates a `g` element with a text and a `path` + * or `rect` inside, to make it behave somewhat like a HTML div. Border and + * background are set through `stroke`, `stroke-width` and `fill` attributes + * using the {@link Highcharts.SVGElement#attr|attr} method. To update the + * text after render, run `label.attr({ text: 'New text' })`. + * + * @param {string} str + * The initial text string or (subset) HTML to render. + * @param {number} x + * The x position of the label's left side. + * @param {number} y + * The y position of the label's top side or baseline, depending on + * the `baseline` parameter. + * @param {String} shape + * The shape of the label's border/background, if any. Defaults to + * `rect`. Other possible values are `callout` or other shapes + * defined in {@link Highcharts.SVGRenderer#symbols}. + * @param {number} anchorX + * In case the `shape` has a pointer, like a flag, this is the + * coordinates it should be pinned to. + * @param {number} anchorY + * In case the `shape` has a pointer, like a flag, this is the + * coordinates it should be pinned to. + * @param {Boolean} baseline + * Whether to position the label relative to the text baseline, + * like {@link Highcharts.SVGRenderer#text|renderer.text}, or to the + * upper border of the rectangle. + * @param {String} className + * Class name for the group. + * + * @return {SVGElement} + * The generated label. + * + * @sample highcharts/members/renderer-label-on-chart/ + * A label on the chart + */ + label: function ( + str, + x, + y, + shape, + anchorX, + anchorY, + useHTML, + baseline, + className + ) { + + var renderer = this, + wrapper = renderer.g(className !== 'button' && 'label'), + text = wrapper.text = renderer.text('', 0, 0, useHTML) + .attr({ + zIndex: 1 + }), + box, + bBox, + alignFactor = 0, + padding = 3, + paddingLeft = 0, + width, + height, + wrapperX, + wrapperY, + textAlign, + deferredAttr = {}, + strokeWidth, + baselineOffset, + hasBGImage = /^url\((.*?)\)$/.test(shape), + needsBox = hasBGImage, + getCrispAdjust, + updateBoxSize, + updateTextPadding, + boxAttr; + + if (className) { + wrapper.addClass('highcharts-' + className); + } + + + needsBox = hasBGImage; + getCrispAdjust = function () { + return (strokeWidth || 0) % 2 / 2; + }; + + + + /** + * This function runs after the label is added to the DOM (when the + * bounding box is available), and after the text of the label is + * updated to detect the new bounding box and reflect it in the border + * box. + */ + updateBoxSize = function () { + var style = text.element.style, + crispAdjust, + attribs = {}; + + bBox = ( + (width === undefined || height === undefined || textAlign) && + defined(text.textStr) && + text.getBBox() + ); // #3295 && 3514 box failure when string equals 0 + wrapper.width = ( + (width || bBox.width || 0) + + 2 * padding + + paddingLeft + ); + wrapper.height = (height || bBox.height || 0) + 2 * padding; + + // Update the label-scoped y offset + baselineOffset = padding + + renderer.fontMetrics(style && style.fontSize, text).b; + + + if (needsBox) { + + // Create the border box if it is not already present + if (!box) { + // Symbol definition exists (#5324) + wrapper.box = box = renderer.symbols[shape] || hasBGImage ? + renderer.symbol(shape) : + renderer.rect(); + + box.addClass( // Don't use label className for buttons + (className === 'button' ? '' : 'highcharts-label-box') + + (className ? ' highcharts-' + className + '-box' : '') + ); + + box.add(wrapper); + + crispAdjust = getCrispAdjust(); + attribs.x = crispAdjust; + attribs.y = (baseline ? -baselineOffset : 0) + crispAdjust; + } + + // Apply the box attributes + attribs.width = Math.round(wrapper.width); + attribs.height = Math.round(wrapper.height); + + box.attr(extend(attribs, deferredAttr)); + deferredAttr = {}; + } + }; + + /** + * This function runs after setting text or padding, but only if padding + * is changed + */ + updateTextPadding = function () { + var textX = paddingLeft + padding, + textY; + + // determin y based on the baseline + textY = baseline ? 0 : baselineOffset; + + // compensate for alignment + if ( + defined(width) && + bBox && + (textAlign === 'center' || textAlign === 'right') + ) { + textX += { center: 0.5, right: 1 }[textAlign] * + (width - bBox.width); + } + + // update if anything changed + if (textX !== text.x || textY !== text.y) { + text.attr('x', textX); + if (textY !== undefined) { + text.attr('y', textY); + } + } + + // record current values + text.x = textX; + text.y = textY; + }; + + /** + * Set a box attribute, or defer it if the box is not yet created + * @param {Object} key + * @param {Object} value + */ + boxAttr = function (key, value) { + if (box) { + box.attr(key, value); + } else { + deferredAttr[key] = value; + } + }; + + /** + * After the text element is added, get the desired size of the border + * box and add it before the text in the DOM. + */ + wrapper.onAdd = function () { + text.add(wrapper); + wrapper.attr({ + // Alignment is available now (#3295, 0 not rendered if given + // as a value) + text: (str || str === 0) ? str : '', + x: x, + y: y + }); + + if (box && defined(anchorX)) { + wrapper.attr({ + anchorX: anchorX, + anchorY: anchorY + }); + } + }; + + /* + * Add specific attribute setters. + */ + + // only change local variables + wrapper.widthSetter = function (value) { + width = H.isNumber(value) ? value : null; // width:auto => null + }; + wrapper.heightSetter = function (value) { + height = value; + }; + wrapper['text-alignSetter'] = function (value) { + textAlign = value; + }; + wrapper.paddingSetter = function (value) { + if (defined(value) && value !== padding) { + padding = wrapper.padding = value; + updateTextPadding(); + } + }; + wrapper.paddingLeftSetter = function (value) { + if (defined(value) && value !== paddingLeft) { + paddingLeft = value; + updateTextPadding(); + } + }; + + + // change local variable and prevent setting attribute on the group + wrapper.alignSetter = function (value) { + value = { left: 0, center: 0.5, right: 1 }[value]; + if (value !== alignFactor) { + alignFactor = value; + // Bounding box exists, means we're dynamically changing + if (bBox) { + wrapper.attr({ x: wrapperX }); // #5134 + } + } + }; + + // apply these to the box and the text alike + wrapper.textSetter = function (value) { + if (value !== undefined) { + text.textSetter(value); + } + updateBoxSize(); + updateTextPadding(); + }; + + // apply these to the box but not to the text + wrapper['stroke-widthSetter'] = function (value, key) { + if (value) { + needsBox = true; + } + strokeWidth = this['stroke-width'] = value; + boxAttr(key, value); + }; + + wrapper.strokeSetter = + wrapper.fillSetter = + wrapper.rSetter = function (value, key) { + if (key !== 'r') { + if (key === 'fill' && value) { + needsBox = true; + } + // for animation getter (#6776) + wrapper[key] = value; + } + boxAttr(key, value); + }; + + wrapper.anchorXSetter = function (value, key) { + anchorX = wrapper.anchorX = value; + boxAttr(key, Math.round(value) - getCrispAdjust() - wrapperX); + }; + wrapper.anchorYSetter = function (value, key) { + anchorY = wrapper.anchorY = value; + boxAttr(key, value - wrapperY); + }; + + // rename attributes + wrapper.xSetter = function (value) { + wrapper.x = value; // for animation getter + if (alignFactor) { + value -= alignFactor * ((width || bBox.width) + 2 * padding); + } + wrapperX = Math.round(value); + wrapper.attr('translateX', wrapperX); + }; + wrapper.ySetter = function (value) { + wrapperY = wrapper.y = Math.round(value); + wrapper.attr('translateY', wrapperY); + }; + + // Redirect certain methods to either the box or the text + var baseCss = wrapper.css; + return extend(wrapper, { + /** + * Pick up some properties and apply them to the text instead of the + * wrapper. + * @ignore + */ + css: function (styles) { + if (styles) { + var textStyles = {}; + // Create a copy to avoid altering the original object + // (#537) + styles = merge(styles); + each(wrapper.textProps, function (prop) { + if (styles[prop] !== undefined) { + textStyles[prop] = styles[prop]; + delete styles[prop]; + } + }); + text.css(textStyles); + } + return baseCss.call(wrapper, styles); + }, + /** + * Return the bounding box of the box, not the group. + * @ignore + */ + getBBox: function () { + return { + width: bBox.width + 2 * padding, + height: bBox.height + 2 * padding, + x: bBox.x - padding, + y: bBox.y - padding + }; + }, + + /** + * Apply the shadow to the box. + * @ignore + */ + shadow: function (b) { + if (b) { + updateBoxSize(); + if (box) { + box.shadow(b); + } + } + return wrapper; + }, + + /** + * Destroy and release memory. + * @ignore + */ + destroy: function () { + + // Added by button implementation + removeEvent(wrapper.element, 'mouseenter'); + removeEvent(wrapper.element, 'mouseleave'); + + if (text) { + text = text.destroy(); + } + if (box) { + box = box.destroy(); + } + // Call base implementation to destroy the rest + SVGElement.prototype.destroy.call(wrapper); + + // Release local pointers (#1298) + wrapper = + renderer = + updateBoxSize = + updateTextPadding = + boxAttr = null; + } + }); + } +}); // end SVGRenderer + + +// general renderer +H.Renderer = SVGRenderer; + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +/* eslint max-len: ["warn", 80, 4] */ +var attr = H.attr, + createElement = H.createElement, + css = H.css, + defined = H.defined, + each = H.each, + extend = H.extend, + isFirefox = H.isFirefox, + isMS = H.isMS, + isWebKit = H.isWebKit, + pick = H.pick, + pInt = H.pInt, + SVGElement = H.SVGElement, + SVGRenderer = H.SVGRenderer, + win = H.win, + wrap = H.wrap; + +// Extend SvgElement for useHTML option +extend(SVGElement.prototype, /** @lends SVGElement.prototype */ { + /** + * Apply CSS to HTML elements. This is used in text within SVG rendering and + * by the VML renderer + */ + htmlCss: function (styles) { + var wrapper = this, + element = wrapper.element, + textWidth = styles && element.tagName === 'SPAN' && styles.width; + + if (textWidth) { + delete styles.width; + wrapper.textWidth = textWidth; + wrapper.updateTransform(); + } + if (styles && styles.textOverflow === 'ellipsis') { + styles.whiteSpace = 'nowrap'; + styles.overflow = 'hidden'; + } + wrapper.styles = extend(wrapper.styles, styles); + css(wrapper.element, styles); + + return wrapper; + }, + + /** + * VML and useHTML method for calculating the bounding box based on offsets + * @param {Boolean} refresh Whether to force a fresh value from the DOM or + * to use the cached value. + * + * @return {Object} A hash containing values for x, y, width and height + */ + + htmlGetBBox: function () { + var wrapper = this, + element = wrapper.element; + + return { + x: element.offsetLeft, + y: element.offsetTop, + width: element.offsetWidth, + height: element.offsetHeight + }; + }, + + /** + * VML override private method to update elements based on internal + * properties based on SVG transform + */ + htmlUpdateTransform: function () { + // aligning non added elements is expensive + if (!this.added) { + this.alignOnAdd = true; + return; + } + + var wrapper = this, + renderer = wrapper.renderer, + elem = wrapper.element, + translateX = wrapper.translateX || 0, + translateY = wrapper.translateY || 0, + x = wrapper.x || 0, + y = wrapper.y || 0, + align = wrapper.textAlign || 'left', + alignCorrection = { left: 0, center: 0.5, right: 1 }[align], + styles = wrapper.styles; + + // apply translate + css(elem, { + marginLeft: translateX, + marginTop: translateY + }); + + + if (wrapper.shadows) { // used in labels/tooltip + each(wrapper.shadows, function (shadow) { + css(shadow, { + marginLeft: translateX + 1, + marginTop: translateY + 1 + }); + }); + } + + + // apply inversion + if (wrapper.inverted) { // wrapper is a group + each(elem.childNodes, function (child) { + renderer.invertChild(child, elem); + }); + } + + if (elem.tagName === 'SPAN') { + + var rotation = wrapper.rotation, + baseline, + textWidth = pInt(wrapper.textWidth), + whiteSpace = styles && styles.whiteSpace, + currentTextTransform = [ + rotation, + align, + elem.innerHTML, + wrapper.textWidth, + wrapper.textAlign + ].join(','); + + // Do the calculations and DOM access only if properties changed + if (currentTextTransform !== wrapper.cTT) { + + + baseline = renderer.fontMetrics(elem.style.fontSize).b; + + // Renderer specific handling of span rotation + if (defined(rotation)) { + wrapper.setSpanRotation( + rotation, + alignCorrection, + baseline + ); + } + + // Reset multiline/ellipsis in order to read width (#4928, + // #5417) + css(elem, { + width: '', + whiteSpace: whiteSpace || 'nowrap' + }); + + // Update textWidth + if ( + elem.offsetWidth > textWidth && + /[ \-]/.test(elem.textContent || elem.innerText) + ) { // #983, #1254 + css(elem, { + width: textWidth + 'px', + display: 'block', + whiteSpace: whiteSpace || 'normal' // #3331 + }); + } + + + wrapper.getSpanCorrection( + elem.offsetWidth, + baseline, + alignCorrection, + rotation, + align + ); + } + + // apply position with correction + css(elem, { + left: (x + (wrapper.xCorr || 0)) + 'px', + top: (y + (wrapper.yCorr || 0)) + 'px' + }); + + // Force reflow in webkit to apply the left and top on useHTML + // element (#1249) + if (isWebKit) { + // Assigned to baseline for lint purpose + baseline = elem.offsetHeight; + } + + // record current text transform + wrapper.cTT = currentTextTransform; + } + }, + + /** + * Set the rotation of an individual HTML span + */ + setSpanRotation: function (rotation, alignCorrection, baseline) { + var rotationStyle = {}, + cssTransformKey = this.renderer.getTransformKey(); + + rotationStyle[cssTransformKey] = rotationStyle.transform = + 'rotate(' + rotation + 'deg)'; + rotationStyle[cssTransformKey + (isFirefox ? 'Origin' : '-origin')] = + rotationStyle.transformOrigin = + (alignCorrection * 100) + '% ' + baseline + 'px'; + css(this.element, rotationStyle); + }, + + /** + * Get the correction in X and Y positioning as the element is rotated. + */ + getSpanCorrection: function (width, baseline, alignCorrection) { + this.xCorr = -width * alignCorrection; + this.yCorr = -baseline; + } +}); + +// Extend SvgRenderer for useHTML option. +extend(SVGRenderer.prototype, /** @lends SVGRenderer.prototype */ { + + getTransformKey: function () { + return isMS && !/Edge/.test(win.navigator.userAgent) ? + '-ms-transform' : + isWebKit ? + '-webkit-transform' : + isFirefox ? + 'MozTransform' : + win.opera ? + '-o-transform' : + ''; + }, + + /** + * Create HTML text node. This is used by the VML renderer as well as the + * SVG renderer through the useHTML option. + * + * @param {String} str + * @param {Number} x + * @param {Number} y + */ + html: function (str, x, y) { + var wrapper = this.createElement('span'), + element = wrapper.element, + renderer = wrapper.renderer, + isSVG = renderer.isSVG, + addSetters = function (element, style) { + // These properties are set as attributes on the SVG group, and + // as identical CSS properties on the div. (#3542) + each(['opacity', 'visibility'], function (prop) { + wrap(element, prop + 'Setter', function ( + proceed, + value, + key, + elem + ) { + proceed.call(this, value, key, elem); + style[key] = value; + }); + }); + }; + + // Text setter + wrapper.textSetter = function (value) { + if (value !== element.innerHTML) { + delete this.bBox; + } + this.textStr = value; + element.innerHTML = pick(value, ''); + wrapper.htmlUpdateTransform(); + }; + + // Add setters for the element itself (#4938) + if (isSVG) { // #4938, only for HTML within SVG + addSetters(wrapper, wrapper.element.style); + } + + // Various setters which rely on update transform + wrapper.xSetter = + wrapper.ySetter = + wrapper.alignSetter = + wrapper.rotationSetter = + function (value, key) { + if (key === 'align') { + // Do not overwrite the SVGElement.align method. Same as VML. + key = 'textAlign'; + } + wrapper[key] = value; + wrapper.htmlUpdateTransform(); + }; + + // Set the default attributes + wrapper + .attr({ + text: str, + x: Math.round(x), + y: Math.round(y) + }) + .css({ + + fontFamily: this.style.fontFamily, + fontSize: this.style.fontSize, + + position: 'absolute' + }); + + // Keep the whiteSpace style outside the wrapper.styles collection + element.style.whiteSpace = 'nowrap'; + + // Use the HTML specific .css method + wrapper.css = wrapper.htmlCss; + + // This is specific for HTML within SVG + if (isSVG) { + wrapper.add = function (svgGroupWrapper) { + + var htmlGroup, + container = renderer.box.parentNode, + parentGroup, + parents = []; + + this.parentGroup = svgGroupWrapper; + + // Create a mock group to hold the HTML elements + if (svgGroupWrapper) { + htmlGroup = svgGroupWrapper.div; + if (!htmlGroup) { + + // Read the parent chain into an array and read from top + // down + parentGroup = svgGroupWrapper; + while (parentGroup) { + + parents.push(parentGroup); + + // Move up to the next parent group + parentGroup = parentGroup.parentGroup; + } + + // Ensure dynamically updating position when any parent + // is translated + each(parents.reverse(), function (parentGroup) { + var htmlGroupStyle, + cls = attr(parentGroup.element, 'class'); + + // Common translate setter for X and Y on the HTML + // group. Reverted the fix for #6957 du to + // positioning problems and offline export (#7254, + // #7280, #7529) + function translateSetter(value, key) { + parentGroup[key] = value; + + if (key === 'translateX') { + htmlGroupStyle.left = value + 'px'; + } else { + htmlGroupStyle.top = value + 'px'; + } + + parentGroup.doTransform = true; + } + + if (cls) { + cls = { className: cls }; + } // else null + + // Create a HTML div and append it to the parent div + // to emulate the SVG group structure + htmlGroup = + parentGroup.div = + parentGroup.div || createElement('div', cls, { + position: 'absolute', + left: (parentGroup.translateX || 0) + 'px', + top: (parentGroup.translateY || 0) + 'px', + display: parentGroup.display, + opacity: parentGroup.opacity, // #5075 + pointerEvents: ( + parentGroup.styles && + parentGroup.styles.pointerEvents + ) // #5595 + + // the top group is appended to container + }, htmlGroup || container); + + // Shortcut + htmlGroupStyle = htmlGroup.style; + + // Set listeners to update the HTML div's position + // whenever the SVG group position is changed. + extend(parentGroup, { + // (#7287) Pass htmlGroup to use + // the related group + classSetter: (function (htmlGroup) { + return function (value) { + this.element.setAttribute( + 'class', + value + ); + htmlGroup.className = value; + }; + }(htmlGroup)), + on: function () { + if (parents[0].div) { // #6418 + wrapper.on.apply( + { element: parents[0].div }, + arguments + ); + } + return parentGroup; + }, + translateXSetter: translateSetter, + translateYSetter: translateSetter + }); + addSetters(parentGroup, htmlGroupStyle); + }); + + } + } else { + htmlGroup = container; + } + + htmlGroup.appendChild(element); + + // Shared with VML: + wrapper.added = true; + if (wrapper.alignOnAdd) { + wrapper.htmlUpdateTransform(); + } + + return wrapper; + }; + } + return wrapper; + } +}); + +}(Highcharts)); +(function (Highcharts) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +/* eslint max-len: ["warn", { "ignoreUrls": true}] */ + +var H = Highcharts, + + merge = H.merge, + pick = H.pick, + win = H.win; + +/** + * The Time class. Time settings are applied in general for each page using + * `Highcharts.setOptions`, or individually for each Chart item through the + * [time](https://api.highcharts.com/highcharts/time) options set. + * + * The Time object is available from + * [Chart.time](http://api.highcharts.com/class-reference/Highcharts.Chart#.time), + * which refers to `Highcharts.time` if no individual time settings are + * applied. + * + * @example + * // Apply time settings globally + * Highcharts.setOptions({ + * time: { + * timezone: 'Europe/London' + * } + * }); + * + * // Apply time settings by instance + * var chart = Highcharts.chart('container', { + * time: { + * timezone: 'America/New_York' + * }, + * series: [{ + * data: [1, 4, 3, 5] + * }] + * }); + * + * // Use the Time object + * console.log( + * 'Current time in New York', + * chart.time.dateFormat('%Y-%m-%d %H:%M:%S', Date.now()) + * ); + * + * @param chart {Chart} + * The chart instance. The `Time` instance reads its config options from + * [chart.options.time](/highcharts/time). If omitted, it reads from + * `Highcharts.defaultOptions.time`. + * @since 6.0.5 + * @class + */ +Highcharts.Time = function (chart) { + this.init(chart); +}; + +Highcharts.Time.prototype = { + + init: function (chart) { + this.chart = chart; + this.update( + chart ? + chart.options.time : + merge(H.defaultOptions.global, H.defaultOptions.time), + false + ); + }, + + /** + * Time options that can apply globally or to individual charts. These + * settings affect how `datetime` axes are laid out, how tooltips are + * formatted, how series + * [pointIntervalUnit](#plotOptions.series.pointIntervalUnit) works and how + * the Highstock range selector handles time. + * + * The common use case is that all charts in the same Highcharts object + * share the same time settings, in which case the global settings are set + * using `setOptions`. + * + * ```js + * // Apply time settings globally + * Highcharts.setOptions({ + * time: { + * timezone: 'Europe/London' + * } + * }); + * // Apply time settings by instance + * var chart = Highcharts.chart('container', { + * time: { + * timezone: 'America/New_York' + * }, + * series: [{ + * data: [1, 4, 3, 5] + * }] + * }); + * + * // Use the Time object + * console.log( + * 'Current time in New York', + * chart.time.dateFormat('%Y-%m-%d %H:%M:%S', Date.now()) + * ); + * ``` + * + * Since v6.0.5, the time options were moved from the `global` obect to the + * `time` object, and time options can be set on each individual chart. + * + * @sample {highcharts|highstock} + * highcharts/time/timezone/ + * Set the timezone globally + * @sample {highcharts} + * highcharts/time/individual/ + * Set the timezone per chart instance + * @sample {highstock} + * stock/time/individual/ + * Set the timezone per chart instance + * @since 6.0.5 + * @apioption time + */ + defaultOptions: { + /** + * Whether to use UTC time for axis scaling, tickmark placement and + * time display in `Highcharts.dateFormat`. Advantages of using UTC + * is that the time displays equally regardless of the user agent's + * time zone settings. Local time can be used when the data is loaded + * in real time or when correct Daylight Saving Time transitions are + * required. + * + * @type {Boolean} + * @sample {highcharts} highcharts/time/useutc-true/ True by default + * @sample {highcharts} highcharts/time/useutc-false/ False + * @apioption time.useUTC + * @default true + */ + + /** + * A custom `Date` class for advanced date handling. For example, + * [JDate](https://githubcom/tahajahangir/jdate) can be hooked in to + * handle Jalali dates. + * + * @type {Object} + * @since 4.0.4 + * @product highcharts highstock + * @apioption time.Date + */ + + /** + * A callback to return the time zone offset for a given datetime. It + * takes the timestamp in terms of milliseconds since January 1 1970, + * and returns the timezone offset in minutes. This provides a hook + * for drawing time based charts in specific time zones using their + * local DST crossover dates, with the help of external libraries. + * + * @type {Function} + * @see [global.timezoneOffset](#global.timezoneOffset) + * @sample {highcharts|highstock} + * highcharts/time/gettimezoneoffset/ + * Use moment.js to draw Oslo time regardless of browser locale + * @since 4.1.0 + * @product highcharts highstock + * @apioption time.getTimezoneOffset + */ + + /** + * Requires [moment.js](http://momentjs.com/). If the timezone option + * is specified, it creates a default + * [getTimezoneOffset](#time.getTimezoneOffset) function that looks + * up the specified timezone in moment.js. If moment.js is not included, + * this throws a Highcharts error in the console, but does not crash the + * chart. + * + * @type {String} + * @see [getTimezoneOffset](#time.getTimezoneOffset) + * @sample {highcharts|highstock} + * highcharts/time/timezone/ + * Europe/Oslo + * @default undefined + * @since 5.0.7 + * @product highcharts highstock + * @apioption time.timezone + */ + + /** + * The timezone offset in minutes. Positive values are west, negative + * values are east of UTC, as in the ECMAScript + * [getTimezoneOffset](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset) + * method. Use this to display UTC based data in a predefined time zone. + * + * @type {Number} + * @see [time.getTimezoneOffset](#time.getTimezoneOffset) + * @sample {highcharts|highstock} + * highcharts/time/timezoneoffset/ + * Timezone offset + * @default 0 + * @since 3.0.8 + * @product highcharts highstock + * @apioption time.timezoneOffset + */ + + }, + + /** + * Update the Time object with current options. It is called internally on + * initiating Highcharts, after running `Highcharts.setOptions` and on + * `Chart.update`. + * + * @private + */ + update: function (options) { + var useUTC = pick(options.useUTC, true), + getters = ['Minutes', 'Hours', 'Day', 'Date', 'Month', 'FullYear'], + setters = getters.concat(['Milliseconds', 'Seconds']), + n; + + this.options = merge(true, this.options || {}, options); + + // Allow using a different Date class + this.Date = options.Date || win.Date; + + this.useUTC = useUTC; + this.timezoneOffset = useUTC && options.timezoneOffset; + + /** + * Get the time zone offset based on the current timezone information as + * set in the global options. + * + * @function #getTimezoneOffset + * @memberOf Highcharts.Time + * @param {Number} timestamp + * The JavaScript timestamp to inspect. + * @return {Number} + * The timezone offset in minutes compared to UTC. + */ + this.getTimezoneOffset = this.timezoneOffsetFunction(); + + /* + * The time object has options allowing for variable time zones, meaning + * the axis ticks or series data needs to consider this. + */ + this.variableTimezone = !!( + options.getTimezoneOffset || + options.timezone + ); + + // Dynamically set setters and getters. Sets strings pointing to the + // appropriate Date function to use depending on useUTC. Use `for` loop, + // H.each is not yet overridden in oldIE. + for (n = 0; n < getters.length; n++) { + this['get' + getters[n]] = (useUTC ? 'getUTC' : 'get') + getters[n]; + } + for (n = 0; n < setters.length; n++) { + this['set' + setters[n]] = (useUTC ? 'setUTC' : 'set') + setters[n]; + } + + }, + + /** + * Make a time and returns milliseconds. Interprets the inputs as UTC time, + * local time or a specific timezone time depending on the current time + * settings. + * + * @param {Number} year + * The year + * @param {Number} month + * The month. Zero-based, so January is 0. + * @param {Number} date + * The day of the month + * @param {Number} hours + * The hour of the day, 0-23. + * @param {Number} minutes + * The minutes + * @param {Number} seconds + * The seconds + * + * @return {Number} + * The time in milliseconds since January 1st 1970. + */ + makeTime: function (year, month, date, hours, minutes, seconds) { + var d; + if (this.useUTC) { + d = this.Date.UTC.apply(0, arguments); + d += this.getTimezoneOffset(d); + } else { + d = new this.Date( + year, + month, + pick(date, 1), + pick(hours, 0), + pick(minutes, 0), + pick(seconds, 0) + ).getTime(); + } + return d; + }, + + /** + * Sets the getTimezoneOffset function. If the `timezone` option is set, a + * default getTimezoneOffset function with that timezone is returned. If + * a `getTimezoneOffset` option is defined, it is returned. If neither are + * specified, the function using the `timezoneOffset` option or 0 offset is + * returned. + * + * @private + * @return {Function} A getTimezoneOffset function + */ + timezoneOffsetFunction: function () { + var time = this, + options = this.options, + moment = win.moment; + + if (options.timezone) { + if (!moment) { + // getTimezoneOffset-function stays undefined because it depends + // on Moment.js + H.error(25); + + } else { + return function (timestamp) { + return -moment.tz( + timestamp, + options.timezone + ).utcOffset() * 60000; + }; + } + } + + // If not timezone is set, look for the getTimezoneOffset callback + if (this.useUTC && options.getTimezoneOffset) { + return function (timestamp) { + return options.getTimezoneOffset(timestamp) * 60000; + }; + } + + // Last, use the `timezoneOffset` option if set + return function () { + return (time.timezoneOffset || 0) * 60000; + }; + }, + + /** + * Formats a JavaScript date timestamp (milliseconds since Jan 1st 1970) + * into a human readable date string. The format is a subset of the formats + * for PHP's [strftime](http://www.php.net/manual/en/function.strftime.php) + * function. Additional formats can be given in the + * {@link Highcharts.dateFormats} hook. + * + * @param {String} format + * The desired format where various time + * representations are prefixed with %. + * @param {Number} timestamp + * The JavaScript timestamp. + * @param {Boolean} [capitalize=false] + * Upper case first letter in the return. + * @returns {String} The formatted date. + */ + dateFormat: function (format, timestamp, capitalize) { + if (!H.defined(timestamp) || isNaN(timestamp)) { + return H.defaultOptions.lang.invalidDate || ''; + } + format = H.pick(format, '%Y-%m-%d %H:%M:%S'); + + var time = this, + D = this.Date, + date = new D(timestamp - this.getTimezoneOffset(timestamp)), + // get the basic time values + hours = date[this.getHours](), + day = date[this.getDay](), + dayOfMonth = date[this.getDate](), + month = date[this.getMonth](), + fullYear = date[this.getFullYear](), + lang = H.defaultOptions.lang, + langWeekdays = lang.weekdays, + shortWeekdays = lang.shortWeekdays, + pad = H.pad, + + // List all format keys. Custom formats can be added from the + // outside. + replacements = H.extend( + { + + // Day + // Short weekday, like 'Mon' + 'a': shortWeekdays ? + shortWeekdays[day] : + langWeekdays[day].substr(0, 3), + // Long weekday, like 'Monday' + 'A': langWeekdays[day], + // Two digit day of the month, 01 to 31 + 'd': pad(dayOfMonth), + // Day of the month, 1 through 31 + 'e': pad(dayOfMonth, 2, ' '), + 'w': day, + + // Week (none implemented) + // 'W': weekNumber(), + + // Month + // Short month, like 'Jan' + 'b': lang.shortMonths[month], + // Long month, like 'January' + 'B': lang.months[month], + // Two digit month number, 01 through 12 + 'm': pad(month + 1), + + // Year + // Two digits year, like 09 for 2009 + 'y': fullYear.toString().substr(2, 2), + // Four digits year, like 2009 + 'Y': fullYear, + + // Time + // Two digits hours in 24h format, 00 through 23 + 'H': pad(hours), + // Hours in 24h format, 0 through 23 + 'k': hours, + // Two digits hours in 12h format, 00 through 11 + 'I': pad((hours % 12) || 12), + // Hours in 12h format, 1 through 12 + 'l': (hours % 12) || 12, + // Two digits minutes, 00 through 59 + 'M': pad(date[this.getMinutes]()), + // Upper case AM or PM + 'p': hours < 12 ? 'AM' : 'PM', + // Lower case AM or PM + 'P': hours < 12 ? 'am' : 'pm', + // Two digits seconds, 00 through 59 + 'S': pad(date.getSeconds()), + // Milliseconds (naming from Ruby) + 'L': pad(Math.round(timestamp % 1000), 3) + }, + + /** + * A hook for defining additional date format specifiers. New + * specifiers are defined as key-value pairs by using the + * specifier as key, and a function which takes the timestamp as + * value. This function returns the formatted portion of the + * date. + * + * @type {Object} + * @name dateFormats + * @memberOf Highcharts + * @sample highcharts/global/dateformats/ + * Adding support for week + * number + */ + H.dateFormats + ); + + + // Do the replaces + H.objectEach(replacements, function (val, key) { + // Regex would do it in one line, but this is faster + while (format.indexOf('%' + key) !== -1) { + format = format.replace( + '%' + key, + typeof val === 'function' ? val.call(time, timestamp) : val + ); + } + + }); + + // Optionally capitalize the string and return + return capitalize ? + format.substr(0, 1).toUpperCase() + format.substr(1) : + format; + } + +}; // end of Time + + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ + +var color = H.color, + isTouchDevice = H.isTouchDevice, + merge = H.merge, + svg = H.svg; + +/* **************************************************************************** + * Handle the options * + *****************************************************************************/ +/** + * @optionparent + */ +H.defaultOptions = { + + + /** + * An array containing the default colors for the chart's series. When + * all colors are used, new colors are pulled from the start again. + * + * Default colors can also be set on a series or series.type basis, + * see [column.colors](#plotOptions.column.colors), [pie.colors](#plotOptions. + * pie.colors). + * + * In styled mode, the colors option doesn't exist. Instead, colors + * are defined in CSS and applied either through series or point class + * names, or through the [chart.colorCount](#chart.colorCount) option. + * + * + * ### Legacy + * + * In Highcharts 3.x, the default colors were: + * + *
colors: ['#2f7ed8', '#0d233a', '#8bbc21', '#910000', '#1aadce', 
+	 *     '#492970', '#f28f43', '#77a1e5', '#c42525', '#a6c96a']
+ * + * In Highcharts 2.x, the default colors were: + * + *
colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', 
+	 *    '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92']
+ * + * @type {Array} + * @sample {highcharts} highcharts/chart/colors/ Assign a global color theme + * @default ["#7cb5ec", "#434348", "#90ed7d", "#f7a35c", "#8085e9", + * "#f15c80", "#e4d354", "#2b908f", "#f45b5b", "#91e8e1"] + */ + colors: '#7cb5ec #434348 #90ed7d #f7a35c #8085e9 #f15c80 #e4d354 #2b908f #f45b5b #91e8e1'.split(' '), + + + + /** + * Styled mode only. Configuration object for adding SVG definitions for + * reusable elements. See [gradients, shadows and patterns](http://www. + * highcharts.com/docs/chart-design-and-style/gradients-shadows-and- + * patterns) for more information and code examples. + * + * @type {Object} + * @since 5.0.0 + * @apioption defs + */ + + /** + * @ignore + */ + symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], + lang: { + + /** + * The loading text that appears when the chart is set into the loading + * state following a call to `chart.showLoading`. + * + * @type {String} + * @default Loading... + */ + loading: 'Loading...', + + /** + * An array containing the months names. Corresponds to the `%B` format + * in `Highcharts.dateFormat()`. + * + * @type {Array} + * @default [ "January" , "February" , "March" , "April" , "May" , + * "June" , "July" , "August" , "September" , "October" , + * "November" , "December"] + */ + months: [ + 'January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December' + ], + + /** + * An array containing the months names in abbreviated form. Corresponds + * to the `%b` format in `Highcharts.dateFormat()`. + * + * @type {Array} + * @default [ "Jan" , "Feb" , "Mar" , "Apr" , "May" , "Jun" , + * "Jul" , "Aug" , "Sep" , "Oct" , "Nov" , "Dec"] + */ + shortMonths: [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', + 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ], + + /** + * An array containing the weekday names. + * + * @type {Array} + * @default ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", + * "Friday", "Saturday"] + */ + weekdays: [ + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', + 'Thursday', 'Friday', 'Saturday' + ], + + /** + * Short week days, starting Sunday. If not specified, Highcharts uses + * the first three letters of the `lang.weekdays` option. + * + * @type {Array} + * @sample highcharts/lang/shortweekdays/ + * Finnish two-letter abbreviations + * @since 4.2.4 + * @apioption lang.shortWeekdays + */ + + /** + * What to show in a date field for invalid dates. Defaults to an empty + * string. + * + * @type {String} + * @since 4.1.8 + * @product highcharts highstock + * @apioption lang.invalidDate + */ + + /** + * The default decimal point used in the `Highcharts.numberFormat` + * method unless otherwise specified in the function arguments. + * + * @type {String} + * @default . + * @since 1.2.2 + */ + decimalPoint: '.', + + /** + * [Metric prefixes](http://en.wikipedia.org/wiki/Metric_prefix) used + * to shorten high numbers in axis labels. Replacing any of the positions + * with `null` causes the full number to be written. Setting `numericSymbols` + * to `null` disables shortening altogether. + * + * @type {Array} + * @sample {highcharts} highcharts/lang/numericsymbols/ + * Replacing the symbols with text + * @sample {highstock} highcharts/lang/numericsymbols/ + * Replacing the symbols with text + * @default [ "k" , "M" , "G" , "T" , "P" , "E"] + * @since 2.3.0 + */ + numericSymbols: ['k', 'M', 'G', 'T', 'P', 'E'], + + /** + * The magnitude of [numericSymbols](#lang.numericSymbol) replacements. + * Use 10000 for Japanese, Korean and various Chinese locales, which + * use symbols for 10^4, 10^8 and 10^12. + * + * @type {Number} + * @sample highcharts/lang/numericsymbolmagnitude/ + * 10000 magnitude for Japanese + * @default 1000 + * @since 5.0.3 + * @apioption lang.numericSymbolMagnitude + */ + + /** + * The text for the label appearing when a chart is zoomed. + * + * @type {String} + * @default Reset zoom + * @since 1.2.4 + */ + resetZoom: 'Reset zoom', + + /** + * The tooltip title for the label appearing when a chart is zoomed. + * + * @type {String} + * @default Reset zoom level 1:1 + * @since 1.2.4 + */ + resetZoomTitle: 'Reset zoom level 1:1', + + /** + * The default thousands separator used in the `Highcharts.numberFormat` + * method unless otherwise specified in the function arguments. Since + * Highcharts 4.1 it defaults to a single space character, which is + * compatible with ISO and works across Anglo-American and continental + * European languages. + * + * The default is a single space. + * + * @type {String} + * @default + * @since 1.2.2 + */ + thousandsSep: ' ' + }, + + /** + * Global options that don't apply to each chart. These options, like + * the `lang` options, must be set using the `Highcharts.setOptions` + * method. + * + *
Highcharts.setOptions({
+	 *     global: {
+	 *         useUTC: false
+	 *     }
+	 * });
+ * + */ + global: { + + /** + * _Canvg rendering for Android 2.x is removed as of Highcharts 5.0\. + * Use the [libURL](#exporting.libURL) option to configure exporting._ + * + * The URL to the additional file to lazy load for Android 2.x devices. + * These devices don't support SVG, so we download a helper file that + * contains [canvg](http://code.google.com/p/canvg/), its dependency + * rbcolor, and our own CanVG Renderer class. To avoid hotlinking to + * our site, you can install canvas-tools.js on your own server and + * change this option accordingly. + * + * @type {String} + * @deprecated + * @default http://code.highcharts.com/{version}/modules/canvas-tools.js + * @product highcharts highmaps + * @apioption global.canvasToolsURL + */ + + /** + * This option is deprecated since v6.0.5. Instead, use + * [time.useUTC](#time.useUTC) that supports individual time settings + * per chart. + * + * @deprecated + * @apioption global.useUTC + */ + + /** + * This option is deprecated since v6.0.5. Instead, use + * [time.Date](#time.Date) that supports individual time settings + * per chart. + * + * @deprecated + * @product highcharts highstock + * @apioption global.Date + */ + + /** + * This option is deprecated since v6.0.5. Instead, use + * [time.getTimezoneOffset](#time.getTimezoneOffset) that supports + * individual time settings per chart. + * + * @deprecated + * @product highcharts highstock + * @apioption global.getTimezoneOffset + */ + + /** + * This option is deprecated since v6.0.5. Instead, use + * [time.timezone](#time.timezone) that supports individual time + * settings per chart. + * + * @deprecated + * @product highcharts highstock + * @apioption global.timezone + */ + + /** + * This option is deprecated since v6.0.5. Instead, use + * [time.timezoneOffset](#time.timezoneOffset) that supports individual + * time settings per chart. + * + * @deprecated + * @product highcharts highstock + * @apioption global.timezoneOffset + */ + }, + + + time: H.Time.prototype.defaultOptions, + chart: { + + /** + * When using multiple axis, the ticks of two or more opposite axes + * will automatically be aligned by adding ticks to the axis or axes + * with the least ticks, as if `tickAmount` were specified. + * + * This can be prevented by setting `alignTicks` to false. If the grid + * lines look messy, it's a good idea to hide them for the secondary + * axis by setting `gridLineWidth` to 0. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/alignticks-true/ True by default + * @sample {highcharts} highcharts/chart/alignticks-false/ False + * @sample {highstock} stock/chart/alignticks-true/ + * True by default + * @sample {highstock} stock/chart/alignticks-false/ + * False + * @default true + * @product highcharts highstock + * @apioption chart.alignTicks + */ + + + /** + * Set the overall animation for all chart updating. Animation can be + * disabled throughout the chart by setting it to false here. It can + * be overridden for each individual API method as a function parameter. + * The only animation not affected by this option is the initial series + * animation, see [plotOptions.series.animation](#plotOptions.series. + * animation). + * + * The animation can either be set as a boolean or a configuration + * object. If `true`, it will use the 'swing' jQuery easing and a + * duration of 500 ms. If used as a configuration object, the following + * properties are supported: + * + *
+ * + *
duration
+ * + *
The duration of the animation in milliseconds.
+ * + *
easing
+ * + *
A string reference to an easing function set on the `Math` object. + * See [the easing demo](http://jsfiddle.net/gh/get/library/pure/ + * highcharts/highcharts/tree/master/samples/highcharts/plotoptions/ + * series-animation-easing/).
+ * + *
+ * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/chart/animation-none/ + * Updating with no animation + * @sample {highcharts} highcharts/chart/animation-duration/ + * With a longer duration + * @sample {highcharts} highcharts/chart/animation-easing/ + * With a jQuery UI easing + * @sample {highmaps} maps/chart/animation-none/ + * Updating with no animation + * @sample {highmaps} maps/chart/animation-duration/ + * With a longer duration + * @default true + * @apioption chart.animation + */ + + /** + * A CSS class name to apply to the charts container `div`, allowing + * unique CSS styling for each chart. + * + * @type {String} + * @apioption chart.className + */ + + /** + * Event listeners for the chart. + * + * @apioption chart.events + */ + + /** + * Fires when a series is added to the chart after load time, using + * the `addSeries` method. One parameter, `event`, is passed to the + * function, containing common event information. + * Through `event.options` you can access the series options that was + * passed to the `addSeries` method. Returning false prevents the series + * from being added. + * + * @type {Function} + * @context Chart + * @sample {highcharts} highcharts/chart/events-addseries/ Alert on add series + * @sample {highstock} stock/chart/events-addseries/ Alert on add series + * @since 1.2.0 + * @apioption chart.events.addSeries + */ + + /** + * Fires when clicking on the plot background. One parameter, `event`, + * is passed to the function, containing common event information. + * + * Information on the clicked spot can be found through `event.xAxis` + * and `event.yAxis`, which are arrays containing the axes of each dimension + * and each axis' value at the clicked spot. The primary axes are `event. + * xAxis[0]` and `event.yAxis[0]`. Remember the unit of a datetime axis + * is milliseconds since 1970-01-01 00:00:00. + * + *
click: function(e) {
+		 *     console.log(
+		 *         Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', e.xAxis[0].value),
+		 *         e.yAxis[0].value
+		 *     )
+		 * }
+ * + * @type {Function} + * @context Chart + * @sample {highcharts} highcharts/chart/events-click/ + * Alert coordinates on click + * @sample {highcharts} highcharts/chart/events-container/ + * Alternatively, attach event to container + * @sample {highstock} stock/chart/events-click/ + * Alert coordinates on click + * @sample {highstock} highcharts/chart/events-container/ + * Alternatively, attach event to container + * @sample {highmaps} maps/chart/events-click/ + * Record coordinates on click + * @sample {highmaps} highcharts/chart/events-container/ + * Alternatively, attach event to container + * @since 1.2.0 + * @apioption chart.events.click + */ + + + /** + * Fires when the chart is finished loading. Since v4.2.2, it also waits + * for images to be loaded, for example from point markers. One parameter, + * `event`, is passed to the function, containing common event information. + * + * There is also a second parameter to the chart constructor where a + * callback function can be passed to be executed on chart.load. + * + * @type {Function} + * @context Chart + * @sample {highcharts} highcharts/chart/events-load/ + * Alert on chart load + * @sample {highstock} stock/chart/events-load/ + * Alert on chart load + * @sample {highmaps} maps/chart/events-load/ + * Add series on chart load + * @apioption chart.events.load + */ + + /** + * Fires when the chart is redrawn, either after a call to chart.redraw() + * or after an axis, series or point is modified with the `redraw` option + * set to true. One parameter, `event`, is passed to the function, containing common event information. + * + * @type {Function} + * @context Chart + * @sample {highcharts} highcharts/chart/events-redraw/ + * Alert on chart redraw + * @sample {highstock} stock/chart/events-redraw/ + * Alert on chart redraw when adding a series or moving the + * zoomed range + * @sample {highmaps} maps/chart/events-redraw/ + * Set subtitle on chart redraw + * @since 1.2.0 + * @apioption chart.events.redraw + */ + + /** + * Fires after initial load of the chart (directly after the `load` + * event), and after each redraw (directly after the `redraw` event). + * + * @type {Function} + * @context Chart + * @since 5.0.7 + * @apioption chart.events.render + */ + + /** + * Fires when an area of the chart has been selected. Selection is enabled + * by setting the chart's zoomType. One parameter, `event`, is passed + * to the function, containing common event information. The default action for the selection event is to + * zoom the chart to the selected area. It can be prevented by calling + * `event.preventDefault()`. + * + * Information on the selected area can be found through `event.xAxis` + * and `event.yAxis`, which are arrays containing the axes of each dimension + * and each axis' min and max values. The primary axes are `event.xAxis[0]` + * and `event.yAxis[0]`. Remember the unit of a datetime axis is milliseconds + * since 1970-01-01 00:00:00. + * + *
selection: function(event) {
+		 *     // log the min and max of the primary, datetime x-axis
+		 *     console.log(
+		 *         Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', event.xAxis[0].min),
+		 *         Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', event.xAxis[0].max)
+		 *     );
+		 *     // log the min and max of the y axis
+		 *     console.log(event.yAxis[0].min, event.yAxis[0].max);
+		 * }
+ * + * @type {Function} + * @sample {highcharts} highcharts/chart/events-selection/ + * Report on selection and reset + * @sample {highcharts} highcharts/chart/events-selection-points/ + * Select a range of points through a drag selection + * @sample {highstock} stock/chart/events-selection/ + * Report on selection and reset + * @sample {highstock} highcharts/chart/events-selection-points/ + * Select a range of points through a drag selection (Highcharts) + * @apioption chart.events.selection + */ + + /** + * The margin between the outer edge of the chart and the plot area. + * The numbers in the array designate top, right, bottom and left + * respectively. Use the options `marginTop`, `marginRight`, + * `marginBottom` and `marginLeft` for shorthand setting of one option. + * + * By default there is no margin. The actual space is dynamically calculated + * from the offset of axis labels, axis title, title, subtitle and legend + * in addition to the `spacingTop`, `spacingRight`, `spacingBottom` + * and `spacingLeft` options. + * + * @type {Array} + * @sample {highcharts} highcharts/chart/margins-zero/ + * Zero margins + * @sample {highstock} stock/chart/margin-zero/ + * Zero margins + * + * @defaults {all} null + * @apioption chart.margin + */ + + /** + * The margin between the bottom outer edge of the chart and the plot + * area. Use this to set a fixed pixel value for the margin as opposed + * to the default dynamic margin. See also `spacingBottom`. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/marginbottom/ + * 100px bottom margin + * @sample {highstock} stock/chart/marginbottom/ + * 100px bottom margin + * @sample {highmaps} maps/chart/margin/ + * 100px margins + * @since 2.0 + * @apioption chart.marginBottom + */ + + /** + * The margin between the left outer edge of the chart and the plot + * area. Use this to set a fixed pixel value for the margin as opposed + * to the default dynamic margin. See also `spacingLeft`. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/marginleft/ + * 150px left margin + * @sample {highstock} stock/chart/marginleft/ + * 150px left margin + * @sample {highmaps} maps/chart/margin/ + * 100px margins + * @default null + * @since 2.0 + * @apioption chart.marginLeft + */ + + /** + * The margin between the right outer edge of the chart and the plot + * area. Use this to set a fixed pixel value for the margin as opposed + * to the default dynamic margin. See also `spacingRight`. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/marginright/ + * 100px right margin + * @sample {highstock} stock/chart/marginright/ + * 100px right margin + * @sample {highmaps} maps/chart/margin/ + * 100px margins + * @default null + * @since 2.0 + * @apioption chart.marginRight + */ + + /** + * The margin between the top outer edge of the chart and the plot area. + * Use this to set a fixed pixel value for the margin as opposed to + * the default dynamic margin. See also `spacingTop`. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/margintop/ 100px top margin + * @sample {highstock} stock/chart/margintop/ + * 100px top margin + * @sample {highmaps} maps/chart/margin/ + * 100px margins + * @default null + * @since 2.0 + * @apioption chart.marginTop + */ + + /** + * Allows setting a key to switch between zooming and panning. Can be + * one of `alt`, `ctrl`, `meta` (the command key on Mac and Windows + * key on Windows) or `shift`. The keys are mapped directly to the key + * properties of the click event argument (`event.altKey`, `event.ctrlKey`, + * `event.metaKey` and `event.shiftKey`). + * + * @validvalue [null, "alt", "ctrl", "meta", "shift"] + * @type {String} + * @since 4.0.3 + * @product highcharts + * @apioption chart.panKey + */ + + /** + * Allow panning in a chart. Best used with [panKey](#chart.panKey) + * to combine zooming and panning. + * + * On touch devices, when the [tooltip.followTouchMove](#tooltip.followTouchMove) + * option is `true` (default), panning requires two fingers. To allow + * panning with one finger, set `followTouchMove` to `false`. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/pankey/ Zooming and panning + * @default {highcharts} false + * @default {highstock} true + * @since 4.0.3 + * @product highcharts highstock + * @apioption chart.panning + */ + + + /** + * Equivalent to [zoomType](#chart.zoomType), but for multitouch gestures + * only. By default, the `pinchType` is the same as the `zoomType` setting. + * However, pinching can be enabled separately in some cases, for example + * in stock charts where a mouse drag pans the chart, while pinching + * is enabled. When [tooltip.followTouchMove](#tooltip.followTouchMove) + * is true, pinchType only applies to two-finger touches. + * + * @validvalue ["x", "y", "xy"] + * @type {String} + * @default {highcharts} null + * @default {highstock} x + * @since 3.0 + * @product highcharts highstock + * @apioption chart.pinchType + */ + + /** + * The corner radius of the outer chart border. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/borderradius/ 20px radius + * @sample {highstock} stock/chart/border/ 10px radius + * @sample {highmaps} maps/chart/border/ Border options + * @default 0 + */ + borderRadius: 0, + + + /** + * Alias of `type`. + * + * @validvalue ["line", "spline", "column", "area", "areaspline", "pie"] + * @type {String} + * @deprecated + * @sample {highcharts} highcharts/chart/defaultseriestype/ Bar + * @default line + * @product highcharts + */ + defaultSeriesType: 'line', + + /** + * If true, the axes will scale to the remaining visible series once + * one series is hidden. If false, hiding and showing a series will + * not affect the axes or the other series. For stacks, once one series + * within the stack is hidden, the rest of the stack will close in + * around it even if the axis is not affected. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/ignorehiddenseries-true/ + * True by default + * @sample {highcharts} highcharts/chart/ignorehiddenseries-false/ + * False + * @sample {highcharts} highcharts/chart/ignorehiddenseries-true-stacked/ + * True with stack + * @sample {highstock} stock/chart/ignorehiddenseries-true/ + * True by default + * @sample {highstock} stock/chart/ignorehiddenseries-false/ + * False + * @default true + * @since 1.2.0 + * @product highcharts highstock + */ + ignoreHiddenSeries: true, + + + /** + * Whether to invert the axes so that the x axis is vertical and y axis + * is horizontal. When `true`, the x axis is [reversed](#xAxis.reversed) + * by default. + * + * @productdesc {highcharts} + * If a bar series is present in the chart, it will be inverted + * automatically. Inverting the chart doesn't have an effect if there + * are no cartesian series in the chart, or if the chart is + * [polar](#chart.polar). + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/inverted/ + * Inverted line + * @sample {highstock} stock/navigator/inverted/ + * Inverted stock chart + * @default false + * @product highcharts highstock + * @apioption chart.inverted + */ + + /** + * The distance between the outer edge of the chart and the content, + * like title or legend, or axis title and labels if present. The + * numbers in the array designate top, right, bottom and left respectively. + * Use the options spacingTop, spacingRight, spacingBottom and spacingLeft + * options for shorthand setting of one option. + * + * @type {Array} + * @see [chart.margin](#chart.margin) + * @default [10, 10, 15, 10] + * @since 3.0.6 + */ + spacing: [10, 10, 15, 10], + + /** + * The button that appears after a selection zoom, allowing the user + * to reset zoom. + * + */ + resetZoomButton: { + + /** + * A collection of attributes for the button. The object takes SVG + * attributes like `fill`, `stroke`, `stroke-width` or `r`, the border + * radius. The theme also supports `style`, a collection of CSS properties + * for the text. Equivalent attributes for the hover state are given + * in `theme.states.hover`. + * + * @type {Object} + * @sample {highcharts} highcharts/chart/resetzoombutton-theme/ + * Theming the button + * @sample {highstock} highcharts/chart/resetzoombutton-theme/ + * Theming the button + * @since 2.2 + */ + theme: { + + /** + * The Z index for the reset zoom button. The default value + * places it below the tooltip that has Z index 7. + */ + zIndex: 6 + }, + + /** + * The position of the button. + * + * @type {Object} + * @sample {highcharts} highcharts/chart/resetzoombutton-position/ + * Above the plot area + * @sample {highstock} highcharts/chart/resetzoombutton-position/ + * Above the plot area + * @sample {highmaps} highcharts/chart/resetzoombutton-position/ + * Above the plot area + * @since 2.2 + */ + position: { + + /** + * The horizontal alignment of the button. + * + * @type {String} + */ + align: 'right', + + /** + * The horizontal offset of the button. + * + * @type {Number} + */ + x: -10, + + /** + * The vertical alignment of the button. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @default top + * @apioption chart.resetZoomButton.position.verticalAlign + */ + + /** + * The vertical offset of the button. + * + * @type {Number} + */ + y: 10 + } + + /** + * What frame the button should be placed related to. Can be either + * `plot` or `chart` + * + * @validvalue ["plot", "chart"] + * @type {String} + * @sample {highcharts} highcharts/chart/resetzoombutton-relativeto/ + * Relative to the chart + * @sample {highstock} highcharts/chart/resetzoombutton-relativeto/ + * Relative to the chart + * @default plot + * @since 2.2 + * @apioption chart.resetZoomButton.relativeTo + */ + }, + + /** + * An explicit width for the chart. By default (when `null`) the width + * is calculated from the offset width of the containing element. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/width/ 800px wide + * @sample {highstock} stock/chart/width/ 800px wide + * @sample {highmaps} maps/chart/size/ Chart with explicit size + * @default null + */ + width: null, + + /** + * An explicit height for the chart. If a _number_, the height is + * given in pixels. If given a _percentage string_ (for example `'56%'`), + * the height is given as the percentage of the actual chart width. + * This allows for preserving the aspect ratio across responsive + * sizes. + * + * By default (when `null`) the height is calculated from the offset + * height of the containing element, or 400 pixels if the containing + * element's height is 0. + * + * @type {Number|String} + * @sample {highcharts} highcharts/chart/height/ + * 500px height + * @sample {highstock} stock/chart/height/ + * 300px height + * @sample {highmaps} maps/chart/size/ + * Chart with explicit size + * @sample highcharts/chart/height-percent/ + * Highcharts with percentage height + * @default null + */ + height: null, + + + + /** + * The color of the outer chart border. + * + * @type {Color} + * @see In styled mode, the stroke is set with the `.highcharts-background` + * class. + * @sample {highcharts} highcharts/chart/bordercolor/ Brown border + * @sample {highstock} stock/chart/border/ Brown border + * @sample {highmaps} maps/chart/border/ Border options + * @default #335cad + */ + borderColor: '#335cad', + + /** + * The pixel width of the outer chart border. + * + * @type {Number} + * @see In styled mode, the stroke is set with the `.highcharts-background` + * class. + * @sample {highcharts} highcharts/chart/borderwidth/ 5px border + * @sample {highstock} stock/chart/border/ + * 2px border + * @sample {highmaps} maps/chart/border/ + * Border options + * @default 0 + * @apioption chart.borderWidth + */ + + /** + * The background color or gradient for the outer chart area. + * + * @type {Color} + * @see In styled mode, the background is set with the `.highcharts-background` class. + * @sample {highcharts} highcharts/chart/backgroundcolor-color/ Color + * @sample {highcharts} highcharts/chart/backgroundcolor-gradient/ Gradient + * @sample {highstock} stock/chart/backgroundcolor-color/ + * Color + * @sample {highstock} stock/chart/backgroundcolor-gradient/ + * Gradient + * @sample {highmaps} maps/chart/backgroundcolor-color/ + * Color + * @sample {highmaps} maps/chart/backgroundcolor-gradient/ + * Gradient + * @default #FFFFFF + */ + backgroundColor: '#ffffff', + + /** + * The background color or gradient for the plot area. + * + * @type {Color} + * @see In styled mode, the plot background is set with the `.highcharts-plot-background` class. + * @sample {highcharts} highcharts/chart/plotbackgroundcolor-color/ + * Color + * @sample {highcharts} highcharts/chart/plotbackgroundcolor-gradient/ + * Gradient + * @sample {highstock} stock/chart/plotbackgroundcolor-color/ + * Color + * @sample {highstock} stock/chart/plotbackgroundcolor-gradient/ + * Gradient + * @sample {highmaps} maps/chart/plotbackgroundcolor-color/ + * Color + * @sample {highmaps} maps/chart/plotbackgroundcolor-gradient/ + * Gradient + * @default null + * @apioption chart.plotBackgroundColor + */ + + + /** + * The URL for an image to use as the plot background. To set an image + * as the background for the entire chart, set a CSS background image + * to the container element. Note that for the image to be applied to + * exported charts, its URL needs to be accessible by the export server. + * + * @type {String} + * @see In styled mode, a plot background image can be set with the + * `.highcharts-plot-background` class and a [custom pattern](http://www. + * highcharts.com/docs/chart-design-and-style/gradients-shadows-and- + * patterns). + * @sample {highcharts} highcharts/chart/plotbackgroundimage/ Skies + * @sample {highstock} stock/chart/plotbackgroundimage/ Skies + * @default null + * @apioption chart.plotBackgroundImage + */ + + /** + * The color of the inner chart or plot area border. + * + * @type {Color} + * @see In styled mode, a plot border stroke can be set with the `. + * highcharts-plot-border` class. + * @sample {highcharts} highcharts/chart/plotbordercolor/ Blue border + * @sample {highstock} stock/chart/plotborder/ Blue border + * @sample {highmaps} maps/chart/plotborder/ Plot border options + * @default #cccccc + */ + plotBorderColor: '#cccccc' + + + /** + * The pixel width of the plot area border. + * + * @type {Number} + * @sample {highcharts} highcharts/chart/plotborderwidth/ 1px border + * @sample {highstock} stock/chart/plotborder/ + * 2px border + * @sample {highmaps} maps/chart/plotborder/ + * Plot border options + * @default 0 + * @apioption chart.plotBorderWidth + */ + + /** + * Whether to apply a drop shadow to the plot area. Requires that + * plotBackgroundColor be set. The shadow can be an object configuration + * containing `color`, `offsetX`, `offsetY`, `opacity` and `width`. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/chart/plotshadow/ Plot shadow + * @sample {highstock} stock/chart/plotshadow/ + * Plot shadow + * @sample {highmaps} maps/chart/plotborder/ + * Plot border options + * @default false + * @apioption chart.plotShadow + */ + + /** + * When true, cartesian charts like line, spline, area and column are + * transformed into the polar coordinate system. Requires `highcharts- + * more.js`. + * + * @type {Boolean} + * @default false + * @since 2.3.0 + * @product highcharts + * @apioption chart.polar + */ + + /** + * Whether to reflow the chart to fit the width of the container div + * on resizing the window. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/reflow-true/ True by default + * @sample {highcharts} highcharts/chart/reflow-false/ False + * @sample {highstock} stock/chart/reflow-true/ + * True by default + * @sample {highstock} stock/chart/reflow-false/ + * False + * @sample {highmaps} maps/chart/reflow-true/ + * True by default + * @sample {highmaps} maps/chart/reflow-false/ + * False + * @default true + * @since 2.1 + * @apioption chart.reflow + */ + + + + + /** + * The HTML element where the chart will be rendered. If it is a string, + * the element by that id is used. The HTML element can also be passed + * by direct reference, or as the first argument of the chart constructor, + * in which case the option is not needed. + * + * @type {String|Object} + * @sample {highcharts} highcharts/chart/reflow-true/ + * String + * @sample {highcharts} highcharts/chart/renderto-object/ + * Object reference + * @sample {highcharts} highcharts/chart/renderto-jquery/ + * Object reference through jQuery + * @sample {highstock} stock/chart/renderto-string/ + * String + * @sample {highstock} stock/chart/renderto-object/ + * Object reference + * @sample {highstock} stock/chart/renderto-jquery/ + * Object reference through jQuery + * @apioption chart.renderTo + */ + + /** + * The background color of the marker square when selecting (zooming + * in on) an area of the chart. + * + * @type {Color} + * @see In styled mode, the selection marker fill is set with the + * `.highcharts-selection-marker` class. + * @default rgba(51,92,173,0.25) + * @since 2.1.7 + * @apioption chart.selectionMarkerFill + */ + + /** + * Whether to apply a drop shadow to the outer chart area. Requires + * that backgroundColor be set. The shadow can be an object configuration + * containing `color`, `offsetX`, `offsetY`, `opacity` and `width`. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/chart/shadow/ Shadow + * @sample {highstock} stock/chart/shadow/ + * Shadow + * @sample {highmaps} maps/chart/border/ + * Chart border and shadow + * @default false + * @apioption chart.shadow + */ + + /** + * Whether to show the axes initially. This only applies to empty charts + * where series are added dynamically, as axes are automatically added + * to cartesian series. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/showaxes-false/ False by default + * @sample {highcharts} highcharts/chart/showaxes-true/ True + * @since 1.2.5 + * @product highcharts + * @apioption chart.showAxes + */ + + /** + * The space between the bottom edge of the chart and the content (plot + * area, axis title and labels, title, subtitle or legend in top position). + * + * @type {Number} + * @sample {highcharts} highcharts/chart/spacingbottom/ + * Spacing bottom set to 100 + * @sample {highstock} stock/chart/spacingbottom/ + * Spacing bottom set to 100 + * @sample {highmaps} maps/chart/spacing/ + * Spacing 100 all around + * @default 15 + * @since 2.1 + * @apioption chart.spacingBottom + */ + + /** + * The space between the left edge of the chart and the content (plot + * area, axis title and labels, title, subtitle or legend in top position). + * + * @type {Number} + * @sample {highcharts} highcharts/chart/spacingleft/ + * Spacing left set to 100 + * @sample {highstock} stock/chart/spacingleft/ + * Spacing left set to 100 + * @sample {highmaps} maps/chart/spacing/ + * Spacing 100 all around + * @default 10 + * @since 2.1 + * @apioption chart.spacingLeft + */ + + /** + * The space between the right edge of the chart and the content (plot + * area, axis title and labels, title, subtitle or legend in top + * position). + * + * @type {Number} + * @sample {highcharts} highcharts/chart/spacingright-100/ + * Spacing set to 100 + * @sample {highcharts} highcharts/chart/spacingright-legend/ + * Legend in right position with default spacing + * @sample {highstock} stock/chart/spacingright/ + * Spacing set to 100 + * @sample {highmaps} maps/chart/spacing/ + * Spacing 100 all around + * @default 10 + * @since 2.1 + * @apioption chart.spacingRight + */ + + /** + * The space between the top edge of the chart and the content (plot + * area, axis title and labels, title, subtitle or legend in top + * position). + * + * @type {Number} + * @sample {highcharts} highcharts/chart/spacingtop-100/ + * A top spacing of 100 + * @sample {highcharts} highcharts/chart/spacingtop-10/ + * Floating chart title makes the plot area align to the default + * spacingTop of 10. + * @sample {highstock} stock/chart/spacingtop/ + * A top spacing of 100 + * @sample {highmaps} maps/chart/spacing/ + * Spacing 100 all around + * @default 10 + * @since 2.1 + * @apioption chart.spacingTop + */ + + /** + * Additional CSS styles to apply inline to the container `div`. Note + * that since the default font styles are applied in the renderer, it + * is ignorant of the individual chart options and must be set globally. + * + * @type {CSSObject} + * @see In styled mode, general chart styles can be set with the `.highcharts-root` class. + * @sample {highcharts} highcharts/chart/style-serif-font/ + * Using a serif type font + * @sample {highcharts} highcharts/css/em/ + * Styled mode with relative font sizes + * @sample {highstock} stock/chart/style/ + * Using a serif type font + * @sample {highmaps} maps/chart/style-serif-font/ + * Using a serif type font + * @default {"fontFamily":"\"Lucida Grande\", \"Lucida Sans Unicode\", Verdana, Arial, Helvetica, sans-serif","fontSize":"12px"} + * @apioption chart.style + */ + + /** + * The default series type for the chart. Can be any of the chart types + * listed under [plotOptions](#plotOptions). + * + * @validvalue ["line", "spline", "column", "bar", "area", "areaspline", "pie", "arearange", "areasplinerange", "boxplot", "bubble", "columnrange", "errorbar", "funnel", "gauge", "heatmap", "polygon", "pyramid", "scatter", "solidgauge", "treemap", "waterfall"] + * @type {String} + * @sample {highcharts} highcharts/chart/type-bar/ Bar + * @sample {highstock} stock/chart/type/ + * Areaspline + * @sample {highmaps} maps/chart/type-mapline/ + * Mapline + * @default {highcharts} line + * @default {highstock} line + * @default {highmaps} map + * @since 2.1.0 + * @apioption chart.type + */ + + /** + * Decides in what dimensions the user can zoom by dragging the mouse. + * Can be one of `x`, `y` or `xy`. + * + * @validvalue [null, "x", "y", "xy"] + * @type {String} + * @see [panKey](#chart.panKey) + * @sample {highcharts} highcharts/chart/zoomtype-none/ None by default + * @sample {highcharts} highcharts/chart/zoomtype-x/ X + * @sample {highcharts} highcharts/chart/zoomtype-y/ Y + * @sample {highcharts} highcharts/chart/zoomtype-xy/ Xy + * @sample {highstock} stock/demo/basic-line/ None by default + * @sample {highstock} stock/chart/zoomtype-x/ X + * @sample {highstock} stock/chart/zoomtype-y/ Y + * @sample {highstock} stock/chart/zoomtype-xy/ Xy + * @product highcharts highstock + * @apioption chart.zoomType + */ + }, + + /** + * The chart's main title. + * + * @sample {highmaps} maps/title/title/ Title options demonstrated + */ + title: { + + /** + * The title of the chart. To disable the title, set the `text` to + * `null`. + * + * @type {String} + * @sample {highcharts} highcharts/title/text/ Custom title + * @sample {highstock} stock/chart/title-text/ Custom title + * @default {highcharts|highmaps} Chart title + * @default {highstock} null + */ + text: 'Chart title', + + /** + * The horizontal alignment of the title. Can be one of "left", "center" + * and "right". + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/title/align/ Aligned to the plot area (x = 70px = margin left - spacing left) + * @sample {highstock} stock/chart/title-align/ Aligned to the plot area (x = 50px = margin left - spacing left) + * @default center + * @since 2.0 + */ + align: 'center', + + /** + * The margin between the title and the plot area, or if a subtitle + * is present, the margin between the subtitle and the plot area. + * + * @type {Number} + * @sample {highcharts} highcharts/title/margin-50/ A chart title margin of 50 + * @sample {highcharts} highcharts/title/margin-subtitle/ The same margin applied with a subtitle + * @sample {highstock} stock/chart/title-margin/ A chart title margin of 50 + * @default 15 + * @since 2.1 + */ + margin: 15, + + /** + * Adjustment made to the title width, normally to reserve space for + * the exporting burger menu. + * + * @type {Number} + * @sample {highcharts} highcharts/title/widthadjust/ Wider menu, greater padding + * @sample {highstock} highcharts/title/widthadjust/ Wider menu, greater padding + * @sample {highmaps} highcharts/title/widthadjust/ Wider menu, greater padding + * @default -44 + * @since 4.2.5 + */ + widthAdjust: -44 + + /** + * When the title is floating, the plot area will not move to make space + * for it. + * + * @type {Boolean} + * @sample {highcharts} highcharts/chart/zoomtype-none/ False by default + * @sample {highcharts} highcharts/title/floating/ + * True - title on top of the plot area + * @sample {highstock} stock/chart/title-floating/ + * True - title on top of the plot area + * @default false + * @since 2.1 + * @apioption title.floating + */ + + /** + * CSS styles for the title. Use this for font styling, but use `align`, + * `x` and `y` for text alignment. + * + * In styled mode, the title style is given in the `.highcharts-title` class. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/title/style/ Custom color and weight + * @sample {highstock} stock/chart/title-style/ Custom color and weight + * @sample highcharts/css/titles/ Styled mode + * @default {highcharts|highmaps} { "color": "#333333", "fontSize": "18px" } + * @default {highstock} { "color": "#333333", "fontSize": "16px" } + * @apioption title.style + */ + + /** + * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels- + * and-string-formatting#html) to render the text. + * + * @type {Boolean} + * @default false + * @apioption title.useHTML + */ + + /** + * The vertical alignment of the title. Can be one of `"top"`, `"middle"` + * and `"bottom"`. When a value is given, the title behaves as if [floating](#title. + * floating) were `true`. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @sample {highcharts} highcharts/title/verticalalign/ + * Chart title in bottom right corner + * @sample {highstock} stock/chart/title-verticalalign/ + * Chart title in bottom right corner + * @since 2.1 + * @apioption title.verticalAlign + */ + + /** + * The x position of the title relative to the alignment within chart. + * spacingLeft and chart.spacingRight. + * + * @type {Number} + * @sample {highcharts} highcharts/title/align/ + * Aligned to the plot area (x = 70px = margin left - spacing left) + * @sample {highstock} stock/chart/title-align/ + * Aligned to the plot area (x = 50px = margin left - spacing left) + * @default 0 + * @since 2.0 + * @apioption title.x + */ + + /** + * The y position of the title relative to the alignment within [chart. + * spacingTop](#chart.spacingTop) and [chart.spacingBottom](#chart.spacingBottom). + * By default it depends on the font size. + * + * @type {Number} + * @sample {highcharts} highcharts/title/y/ + * Title inside the plot area + * @sample {highstock} stock/chart/title-verticalalign/ + * Chart title in bottom right corner + * @since 2.0 + * @apioption title.y + */ + + }, + + /** + * The chart's subtitle. This can be used both to display a subtitle below + * the main title, and to display random text anywhere in the chart. The + * subtitle can be updated after chart initialization through the + * `Chart.setTitle` method. + * + * @sample {highmaps} maps/title/subtitle/ Subtitle options demonstrated + */ + subtitle: { + + /** + * The subtitle of the chart. + * + * @type {String} + * @sample {highcharts} highcharts/subtitle/text/ Custom subtitle + * @sample {highcharts} highcharts/subtitle/text-formatted/ Formatted and linked text. + * @sample {highstock} stock/chart/subtitle-text Custom subtitle + * @sample {highstock} stock/chart/subtitle-text-formatted Formatted and linked text. + */ + text: '', + + /** + * The horizontal alignment of the subtitle. Can be one of "left", + * "center" and "right". + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/subtitle/align/ Footnote at right of plot area + * @sample {highstock} stock/chart/subtitle-footnote Footnote at bottom right of plot area + * @default center + * @since 2.0 + */ + align: 'center', + + /** + * Adjustment made to the subtitle width, normally to reserve space + * for the exporting burger menu. + * + * @type {Number} + * @see [title.widthAdjust](#title.widthAdjust) + * @sample {highcharts} highcharts/title/widthadjust/ Wider menu, greater padding + * @sample {highstock} highcharts/title/widthadjust/ Wider menu, greater padding + * @sample {highmaps} highcharts/title/widthadjust/ Wider menu, greater padding + * @default -44 + * @since 4.2.5 + */ + widthAdjust: -44 + + /** + * When the subtitle is floating, the plot area will not move to make + * space for it. + * + * @type {Boolean} + * @sample {highcharts} highcharts/subtitle/floating/ + * Floating title and subtitle + * @sample {highstock} stock/chart/subtitle-footnote + * Footnote floating at bottom right of plot area + * @default false + * @since 2.1 + * @apioption subtitle.floating + */ + + /** + * CSS styles for the title. + * + * In styled mode, the subtitle style is given in the `.highcharts-subtitle` class. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/subtitle/style/ + * Custom color and weight + * @sample {highcharts} highcharts/css/titles/ + * Styled mode + * @sample {highstock} stock/chart/subtitle-style + * Custom color and weight + * @sample {highstock} highcharts/css/titles/ + * Styled mode + * @sample {highmaps} highcharts/css/titles/ + * Styled mode + * @default { "color": "#666666" } + * @apioption subtitle.style + */ + + /** + * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels- + * and-string-formatting#html) to render the text. + * + * @type {Boolean} + * @default false + * @apioption subtitle.useHTML + */ + + /** + * The vertical alignment of the title. Can be one of "top", "middle" + * and "bottom". When a value is given, the title behaves as floating. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @sample {highcharts} highcharts/subtitle/verticalalign/ + * Footnote at the bottom right of plot area + * @sample {highstock} stock/chart/subtitle-footnote + * Footnote at the bottom right of plot area + * @default + * @since 2.1 + * @apioption subtitle.verticalAlign + */ + + /** + * The x position of the subtitle relative to the alignment within chart. + * spacingLeft and chart.spacingRight. + * + * @type {Number} + * @sample {highcharts} highcharts/subtitle/align/ + * Footnote at right of plot area + * @sample {highstock} stock/chart/subtitle-footnote + * Footnote at the bottom right of plot area + * @default 0 + * @since 2.0 + * @apioption subtitle.x + */ + + /** + * The y position of the subtitle relative to the alignment within chart. + * spacingTop and chart.spacingBottom. By default the subtitle is laid + * out below the title unless the title is floating. + * + * @type {Number} + * @sample {highcharts} highcharts/subtitle/verticalalign/ + * Footnote at the bottom right of plot area + * @sample {highstock} stock/chart/subtitle-footnote + * Footnote at the bottom right of plot area + * @default {highcharts} null + * @default {highstock} null + * @default {highmaps} + * @since 2.0 + * @apioption subtitle.y + */ + }, + + /** + * The plotOptions is a wrapper object for config objects for each series + * type. The config objects for each series can also be overridden for + * each series item as given in the series array. + * + * Configuration options for the series are given in three levels. Options + * for all series in a chart are given in the [plotOptions.series](#plotOptions. + * series) object. Then options for all series of a specific type are + * given in the plotOptions of that type, for example plotOptions.line. + * Next, options for one single series are given in [the series array](#series). + * + */ + plotOptions: {}, + + /** + * HTML labels that can be positioned anywhere in the chart area. + * + */ + labels: { + + /** + * A HTML label that can be positioned anywhere in the chart area. + * + * @type {Array} + * @apioption labels.items + */ + + /** + * Inner HTML or text for the label. + * + * @type {String} + * @apioption labels.items.html + */ + + /** + * CSS styles for each label. To position the label, use left and top + * like this: + * + *
style: {
+		 *     left: '100px',
+		 *     top: '100px'
+		 * }
+ * + * @type {CSSObject} + * @apioption labels.items.style + */ + + /** + * Shared CSS styles for all labels. + * + * @type {CSSObject} + * @default { "color": "#333333" } + */ + style: { + position: 'absolute', + color: '#333333' + } + }, + + /** + * The legend is a box containing a symbol and name for each series + * item or point item in the chart. Each series (or points in case + * of pie charts) is represented by a symbol and its name in the legend. + * + * It is possible to override the symbol creator function and + * create [custom legend symbols](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/studies/legend- + * custom-symbol/). + * + * @productdesc {highmaps} + * A Highmaps legend by default contains one legend item per series, but if + * a `colorAxis` is defined, the axis will be displayed in the legend. + * Either as a gradient, or as multiple legend items for `dataClasses`. + */ + legend: { + + /** + * The background color of the legend. + * + * @type {Color} + * @see In styled mode, the legend background fill can be applied with + * the `.highcharts-legend-box` class. + * @sample {highcharts} highcharts/legend/backgroundcolor/ Yellowish background + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/border-background/ Border and background options + * @apioption legend.backgroundColor + */ + + /** + * The width of the drawn border around the legend. + * + * @type {Number} + * @see In styled mode, the legend border stroke width can be applied + * with the `.highcharts-legend-box` class. + * @sample {highcharts} highcharts/legend/borderwidth/ 2px border width + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/border-background/ Border and background options + * @default 0 + * @apioption legend.borderWidth + */ + + /** + * Enable or disable the legend. + * + * @type {Boolean} + * @sample {highcharts} highcharts/legend/enabled-false/ Legend disabled + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/enabled-false/ Legend disabled + * @default {highstock} false + * @default {highmaps} true + */ + enabled: true, + + /** + * The horizontal alignment of the legend box within the chart area. + * Valid values are `left`, `center` and `right`. + * + * In the case that the legend is aligned in a corner position, the + * `layout` option will determine whether to place it above/below + * or on the side of the plot area. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/legend/align/ + * Legend at the right of the chart + * @sample {highstock} stock/legend/align/ + * Various legend options + * @sample {highmaps} maps/legend/alignment/ + * Legend alignment + * @since 2.0 + */ + align: 'center', + + /** + * When the legend is floating, the plot area ignores it and is allowed + * to be placed below it. + * + * @type {Boolean} + * @sample {highcharts} highcharts/legend/floating-false/ False by default + * @sample {highcharts} highcharts/legend/floating-true/ True + * @sample {highmaps} maps/legend/alignment/ Floating legend + * @default false + * @since 2.1 + * @apioption legend.floating + */ + + /** + * The layout of the legend items. Can be one of "horizontal" or "vertical". + * + * @validvalue ["horizontal", "vertical"] + * @type {String} + * @sample {highcharts} highcharts/legend/layout-horizontal/ Horizontal by default + * @sample {highcharts} highcharts/legend/layout-vertical/ Vertical + * @sample {highstock} stock/legend/layout-horizontal/ Horizontal by default + * @sample {highmaps} maps/legend/padding-itemmargin/ Vertical with data classes + * @sample {highmaps} maps/legend/layout-vertical/ Vertical with color axis gradient + * @default horizontal + */ + layout: 'horizontal', + + /** + * In a legend with horizontal layout, the itemDistance defines the + * pixel distance between each item. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/layout-horizontal/ 50px item distance + * @sample {highstock} highcharts/legend/layout-horizontal/ 50px item distance + * @default {highcharts} 20 + * @default {highstock} 20 + * @default {highmaps} 8 + * @since 3.0.3 + * @apioption legend.itemDistance + */ + + /** + * The pixel bottom margin for each legend item. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated + * @sample {highstock} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated + * @sample {highmaps} maps/legend/padding-itemmargin/ Padding and item margins demonstrated + * @default 0 + * @since 2.2.0 + * @apioption legend.itemMarginBottom + */ + + /** + * The pixel top margin for each legend item. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated + * @sample {highstock} highcharts/legend/padding-itemmargin/ Padding and item margins demonstrated + * @sample {highmaps} maps/legend/padding-itemmargin/ Padding and item margins demonstrated + * @default 0 + * @since 2.2.0 + * @apioption legend.itemMarginTop + */ + + /** + * The width for each legend item. This is useful in a horizontal layout + * with many items when you want the items to align vertically. . + * + * @type {Number} + * @sample {highcharts} highcharts/legend/itemwidth-default/ Null by default + * @sample {highcharts} highcharts/legend/itemwidth-80/ 80 for aligned legend items + * @default null + * @since 2.0 + * @apioption legend.itemWidth + */ + + /** + * A [format string](http://www.highcharts.com/docs/chart-concepts/labels- + * and-string-formatting) for each legend label. Available variables + * relates to properties on the series, or the point in case of pies. + * + * @type {String} + * @default {name} + * @since 1.3 + * @apioption legend.labelFormat + */ + + /** + * Callback function to format each of the series' labels. The `this` + * keyword refers to the series object, or the point object in case + * of pie charts. By default the series or point name is printed. + * + * @productdesc {highmaps} + * In Highmaps the context can also be a data class in case + * of a `colorAxis`. + * + * @type {Function} + * @sample {highcharts} highcharts/legend/labelformatter/ Add text + * @sample {highmaps} maps/legend/labelformatter/ Data classes with label formatter + * @context {Series|Point} + */ + labelFormatter: function () { + return this.name; + }, + + /** + * Line height for the legend items. Deprecated as of 2.1\. Instead, + * the line height for each item can be set using itemStyle.lineHeight, + * and the padding between items using itemMarginTop and itemMarginBottom. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/lineheight/ Setting padding + * @default 16 + * @since 2.0 + * @product highcharts + * @apioption legend.lineHeight + */ + + /** + * If the plot area sized is calculated automatically and the legend + * is not floating, the legend margin is the space between the legend + * and the axis labels or plot area. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/margin-default/ 12 pixels by default + * @sample {highcharts} highcharts/legend/margin-30/ 30 pixels + * @default 12 + * @since 2.1 + * @apioption legend.margin + */ + + /** + * Maximum pixel height for the legend. When the maximum height is extended, + * navigation will show. + * + * @type {Number} + * @default undefined + * @since 2.3.0 + * @apioption legend.maxHeight + */ + + /** + * The color of the drawn border around the legend. + * + * @type {Color} + * @see In styled mode, the legend border stroke can be applied with + * the `.highcharts-legend-box` class. + * @sample {highcharts} highcharts/legend/bordercolor/ Brown border + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/border-background/ Border and background options + * @default #999999 + */ + borderColor: '#999999', + + /** + * The border corner radius of the legend. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/borderradius-default/ Square by default + * @sample {highcharts} highcharts/legend/borderradius-round/ 5px rounded + * @sample {highmaps} maps/legend/border-background/ Border and background options + * @default 0 + */ + borderRadius: 0, + + /** + * Options for the paging or navigation appearing when the legend + * is overflown. Navigation works well on screen, but not in static + * exported images. One way of working around that is to [increase + * the chart height in export](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/legend/navigation- + * enabled-false/). + * + */ + navigation: { + + + /** + * The color for the active up or down arrow in the legend page navigation. + * + * @type {Color} + * @see In styled mode, the active arrow be styled with the `.highcharts-legend-nav-active` class. + * @sample {highcharts} highcharts/legend/navigation/ Legend page navigation demonstrated + * @sample {highstock} highcharts/legend/navigation/ Legend page navigation demonstrated + * @default #003399 + * @since 2.2.4 + */ + activeColor: '#003399', + + /** + * The color of the inactive up or down arrow in the legend page + * navigation. . + * + * @type {Color} + * @see In styled mode, the inactive arrow be styled with the + * `.highcharts-legend-nav-inactive` class. + * @sample {highcharts} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @sample {highstock} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @default {highcharts} #cccccc + * @default {highstock} #cccccc + * @default {highmaps} ##cccccc + * @since 2.2.4 + */ + inactiveColor: '#cccccc' + + + /** + * How to animate the pages when navigating up or down. A value of `true` + * applies the default navigation given in the chart.animation option. + * Additional options can be given as an object containing values for + * easing and duration. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @sample {highstock} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @default true + * @since 2.2.4 + * @apioption legend.navigation.animation + */ + + /** + * The pixel size of the up and down arrows in the legend paging + * navigation. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @sample {highstock} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @default 12 + * @since 2.2.4 + * @apioption legend.navigation.arrowSize + */ + + /** + * Whether to enable the legend navigation. In most cases, disabling + * the navigation results in an unwanted overflow. + * + * See also the [adapt chart to legend](http://www.highcharts.com/plugin- + * registry/single/8/Adapt-Chart-To-Legend) plugin for a solution to + * extend the chart height to make room for the legend, optionally in + * exported charts only. + * + * @type {Boolean} + * @default true + * @since 4.2.4 + * @apioption legend.navigation.enabled + */ + + /** + * Text styles for the legend page navigation. + * + * @type {CSSObject} + * @see In styled mode, the navigation items are styled with the + * `.highcharts-legend-navigation` class. + * @sample {highcharts} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @sample {highstock} highcharts/legend/navigation/ + * Legend page navigation demonstrated + * @since 2.2.4 + * @apioption legend.navigation.style + */ + }, + + /** + * The inner padding of the legend box. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/padding-itemmargin/ + * Padding and item margins demonstrated + * @sample {highstock} highcharts/legend/padding-itemmargin/ + * Padding and item margins demonstrated + * @sample {highmaps} maps/legend/padding-itemmargin/ + * Padding and item margins demonstrated + * @default 8 + * @since 2.2.0 + * @apioption legend.padding + */ + + /** + * Whether to reverse the order of the legend items compared to the + * order of the series or points as defined in the configuration object. + * + * @type {Boolean} + * @see [yAxis.reversedStacks](#yAxis.reversedStacks), + * [series.legendIndex](#series.legendIndex) + * @sample {highcharts} highcharts/legend/reversed/ + * Stacked bar with reversed legend + * @default false + * @since 1.2.5 + * @apioption legend.reversed + */ + + /** + * Whether to show the symbol on the right side of the text rather than + * the left side. This is common in Arabic and Hebraic. + * + * @type {Boolean} + * @sample {highcharts} highcharts/legend/rtl/ Symbol to the right + * @default false + * @since 2.2 + * @apioption legend.rtl + */ + + /** + * CSS styles for the legend area. In the 1.x versions the position + * of the legend area was determined by CSS. In 2.x, the position is + * determined by properties like `align`, `verticalAlign`, `x` and `y`, + * but the styles are still parsed for backwards compatibility. + * + * @type {CSSObject} + * @deprecated + * @product highcharts highstock + * @apioption legend.style + */ + + + + /** + * CSS styles for each legend item. Only a subset of CSS is supported, + * notably those options related to text. The default `textOverflow` + * property makes long texts truncate. Set it to `null` to wrap text + * instead. A `width` property can be added to control the text width. + * + * @type {CSSObject} + * @see In styled mode, the legend items can be styled with the `. + * highcharts-legend-item` class. + * @sample {highcharts} highcharts/legend/itemstyle/ Bold black text + * @sample {highmaps} maps/legend/itemstyle/ Item text styles + * @default { "color": "#333333", "cursor": "pointer", "fontSize": "12px", "fontWeight": "bold", "textOverflow": "ellipsis" } + */ + itemStyle: { + color: '#333333', + fontSize: '12px', + fontWeight: 'bold', + textOverflow: 'ellipsis' + }, + + /** + * CSS styles for each legend item in hover mode. Only a subset of + * CSS is supported, notably those options related to text. Properties + * are inherited from `style` unless overridden here. + * + * @type {CSSObject} + * @see In styled mode, the hovered legend items can be styled with + * the `.highcharts-legend-item:hover` pesudo-class. + * @sample {highcharts} highcharts/legend/itemhoverstyle/ Red on hover + * @sample {highmaps} maps/legend/itemstyle/ Item text styles + * @default { "color": "#000000" } + */ + itemHoverStyle: { + color: '#000000' + }, + + /** + * CSS styles for each legend item when the corresponding series or + * point is hidden. Only a subset of CSS is supported, notably those + * options related to text. Properties are inherited from `style` + * unless overridden here. + * + * @type {CSSObject} + * @see In styled mode, the hidden legend items can be styled with + * the `.highcharts-legend-item-hidden` class. + * @sample {highcharts} highcharts/legend/itemhiddenstyle/ Darker gray color + * @default { "color": "#cccccc" } + */ + itemHiddenStyle: { + color: '#cccccc' + }, + + /** + * Whether to apply a drop shadow to the legend. A `backgroundColor` + * also needs to be applied for this to take effect. The shadow can be + * an object configuration containing `color`, `offsetX`, `offsetY`, + * `opacity` and `width`. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/legend/shadow/ + * White background and drop shadow + * @sample {highstock} stock/legend/align/ + * Various legend options + * @sample {highmaps} maps/legend/border-background/ + * Border and background options + * @default false + */ + shadow: false, + + + /** + * Default styling for the checkbox next to a legend item when + * `showCheckbox` is true. + */ + itemCheckboxStyle: { + position: 'absolute', + width: '13px', // for IE precision + height: '13px' + }, + // itemWidth: undefined, + + /** + * When this is true, the legend symbol width will be the same as + * the symbol height, which in turn defaults to the font size of the + * legend items. + * + * @type {Boolean} + * @default true + * @since 5.0.0 + */ + squareSymbol: true, + + /** + * The pixel height of the symbol for series types that use a rectangle + * in the legend. Defaults to the font size of legend items. + * + * @productdesc {highmaps} + * In Highmaps, when the symbol is the gradient of a vertical color + * axis, the height defaults to 200. + * + * @type {Number} + * @sample {highmaps} maps/legend/layout-vertical-sized/ + * Sized vertical gradient + * @sample {highmaps} maps/legend/padding-itemmargin/ + * No distance between data classes + * @since 3.0.8 + * @apioption legend.symbolHeight + */ + + /** + * The border radius of the symbol for series types that use a rectangle + * in the legend. Defaults to half the `symbolHeight`. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/symbolradius/ Round symbols + * @sample {highstock} highcharts/legend/symbolradius/ Round symbols + * @sample {highmaps} highcharts/legend/symbolradius/ Round symbols + * @since 3.0.8 + * @apioption legend.symbolRadius + */ + + /** + * The pixel width of the legend item symbol. When the `squareSymbol` + * option is set, this defaults to the `symbolHeight`, otherwise 16. + * + * @productdesc {highmaps} + * In Highmaps, when the symbol is the gradient of a horizontal color + * axis, the width defaults to 200. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/symbolwidth/ + * Greater symbol width and padding + * @sample {highmaps} maps/legend/padding-itemmargin/ + * Padding and item margins demonstrated + * @sample {highmaps} maps/legend/layout-vertical-sized/ + * Sized vertical gradient + * @apioption legend.symbolWidth + */ + + /** + * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels- + * and-string-formatting#html) to render the legend item texts. Prior + * to 4.1.7, when using HTML, [legend.navigation](#legend.navigation) + * was disabled. + * + * @type {Boolean} + * @default false + * @apioption legend.useHTML + */ + + /** + * The width of the legend box. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/width/ Aligned to the plot area + * @default null + * @since 2.0 + * @apioption legend.width + */ + + /** + * The pixel padding between the legend item symbol and the legend + * item text. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/symbolpadding/ Greater symbol width and padding + * @default 5 + */ + symbolPadding: 5, + + /** + * The vertical alignment of the legend box. Can be one of `top`, + * `middle` or `bottom`. Vertical position can be further determined + * by the `y` option. + * + * In the case that the legend is aligned in a corner position, the + * `layout` option will determine whether to place it above/below + * or on the side of the plot area. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @sample {highcharts} highcharts/legend/verticalalign/ Legend 100px from the top of the chart + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/alignment/ Legend alignment + * @default bottom + * @since 2.0 + */ + verticalAlign: 'bottom', + // width: undefined, + + /** + * The x offset of the legend relative to its horizontal alignment + * `align` within chart.spacingLeft and chart.spacingRight. Negative + * x moves it to the left, positive x moves it to the right. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/width/ Aligned to the plot area + * @default 0 + * @since 2.0 + */ + x: 0, + + /** + * The vertical offset of the legend relative to it's vertical alignment + * `verticalAlign` within chart.spacingTop and chart.spacingBottom. + * Negative y moves it up, positive y moves it down. + * + * @type {Number} + * @sample {highcharts} highcharts/legend/verticalalign/ Legend 100px from the top of the chart + * @sample {highstock} stock/legend/align/ Various legend options + * @sample {highmaps} maps/legend/alignment/ Legend alignment + * @default 0 + * @since 2.0 + */ + y: 0, + + /** + * A title to be added on top of the legend. + * + * @sample {highcharts} highcharts/legend/title/ Legend title + * @sample {highmaps} maps/legend/alignment/ Legend with title + * @since 3.0 + */ + title: { + /** + * A text or HTML string for the title. + * + * @type {String} + * @default null + * @since 3.0 + * @apioption legend.title.text + */ + + + + /** + * Generic CSS styles for the legend title. + * + * @type {CSSObject} + * @see In styled mode, the legend title is styled with the + * `.highcharts-legend-title` class. + * @default {"fontWeight":"bold"} + * @since 3.0 + */ + style: { + fontWeight: 'bold' + } + + } + }, + + + /** + * The loading options control the appearance of the loading screen + * that covers the plot area on chart operations. This screen only + * appears after an explicit call to `chart.showLoading()`. It is a + * utility for developers to communicate to the end user that something + * is going on, for example while retrieving new data via an XHR connection. + * The "Loading..." text itself is not part of this configuration + * object, but part of the `lang` object. + * + */ + loading: { + + /** + * The duration in milliseconds of the fade out effect. + * + * @type {Number} + * @sample highcharts/loading/hideduration/ Fade in and out over a second + * @default 100 + * @since 1.2.0 + * @apioption loading.hideDuration + */ + + /** + * The duration in milliseconds of the fade in effect. + * + * @type {Number} + * @sample highcharts/loading/hideduration/ Fade in and out over a second + * @default 100 + * @since 1.2.0 + * @apioption loading.showDuration + */ + + + /** + * CSS styles for the loading label `span`. + * + * @type {CSSObject} + * @see In styled mode, the loading label is styled with the + * `.highcharts-legend-loading-inner` class. + * @sample {highcharts|highmaps} highcharts/loading/labelstyle/ Vertically centered + * @sample {highstock} stock/loading/general/ Label styles + * @default { "fontWeight": "bold", "position": "relative", "top": "45%" } + * @since 1.2.0 + */ + labelStyle: { + fontWeight: 'bold', + position: 'relative', + top: '45%' + }, + + /** + * CSS styles for the loading screen that covers the plot area. + * + * @type {CSSObject} + * @see In styled mode, the loading label is styled with the `.highcharts-legend-loading` class. + * @sample {highcharts|highmaps} highcharts/loading/style/ Gray plot area, white text + * @sample {highstock} stock/loading/general/ Gray plot area, white text + * @default { "position": "absolute", "backgroundColor": "#ffffff", "opacity": 0.5, "textAlign": "center" } + * @since 1.2.0 + */ + style: { + position: 'absolute', + backgroundColor: '#ffffff', + opacity: 0.5, + textAlign: 'center' + } + + }, + + + /** + * Options for the tooltip that appears when the user hovers over a + * series or point. + * + */ + tooltip: { + + /** + * Enable or disable the tooltip. + * + * @type {Boolean} + * @sample {highcharts} highcharts/tooltip/enabled/ Disabled + * @sample {highcharts} highcharts/plotoptions/series-point-events-mouseover/ Disable tooltip and show values on chart instead + * @default true + */ + enabled: true, + + /** + * Enable or disable animation of the tooltip. In slow legacy IE browsers + * the animation is disabled by default. + * + * @type {Boolean} + * @default true + * @since 2.3.0 + */ + animation: svg, + + /** + * The radius of the rounded border corners. + * + * @type {Number} + * @sample {highcharts} highcharts/tooltip/bordercolor-default/ 5px by default + * @sample {highcharts} highcharts/tooltip/borderradius-0/ Square borders + * @sample {highmaps} maps/tooltip/background-border/ Background and border demo + * @default 3 + */ + borderRadius: 3, + + /** + * For series on a datetime axes, the date format in the tooltip's + * header will by default be guessed based on the closest data points. + * This member gives the default string representations used for + * each unit. For an overview of the replacement codes, see [dateFormat](#Highcharts. + * dateFormat). + * + * Defaults to: + * + *
{
+		 *     millisecond:"%A, %b %e, %H:%M:%S.%L",
+		 *     second:"%A, %b %e, %H:%M:%S",
+		 *     minute:"%A, %b %e, %H:%M",
+		 *     hour:"%A, %b %e, %H:%M",
+		 *     day:"%A, %b %e, %Y",
+		 *     week:"Week from %A, %b %e, %Y",
+		 *     month:"%B %Y",
+		 *     year:"%Y"
+		 * }
+ * + * @type {Object} + * @see [xAxis.dateTimeLabelFormats](#xAxis.dateTimeLabelFormats) + * @product highcharts highstock + */ + dateTimeLabelFormats: { + millisecond: '%A, %b %e, %H:%M:%S.%L', + second: '%A, %b %e, %H:%M:%S', + minute: '%A, %b %e, %H:%M', + hour: '%A, %b %e, %H:%M', + day: '%A, %b %e, %Y', + week: 'Week from %A, %b %e, %Y', + month: '%B %Y', + year: '%Y' + }, + + /** + * A string to append to the tooltip format. + * + * @sample {highcharts} highcharts/tooltip/footerformat/ A table for value alignment + * @sample {highmaps} maps/tooltip/format/ Format demo + * @since 2.2 + */ + footerFormat: '', + + /** + * Padding inside the tooltip, in pixels. + * + * @type {Number} + * @default 8 + * @since 5.0.0 + */ + padding: 8, + + /** + * Proximity snap for graphs or single points. It defaults to 10 for + * mouse-powered devices and 25 for touch devices. + * + * Note that in most cases the whole plot area captures the mouse + * movement, and in these cases `tooltip.snap` doesn't make sense. + * This applies when [stickyTracking](#plotOptions.series.stickyTracking) + * is `true` (default) and when the tooltip is [shared](#tooltip.shared) + * or [split](#tooltip.split). + * + * @type {Number} + * @sample {highcharts} highcharts/tooltip/bordercolor-default/ 10 px by default + * @sample {highcharts} highcharts/tooltip/snap-50/ 50 px on graph + * @default 10/25 + * @since 1.2.0 + * @product highcharts highstock + */ + snap: isTouchDevice ? 25 : 10, + + + /** + * The background color or gradient for the tooltip. + * + * In styled mode, the stroke width is set in the `.highcharts-tooltip-box` class. + * + * @type {Color} + * @sample {highcharts} highcharts/tooltip/backgroundcolor-solid/ Yellowish background + * @sample {highcharts} highcharts/tooltip/backgroundcolor-gradient/ Gradient + * @sample {highcharts} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @sample {highstock} stock/tooltip/general/ Custom tooltip + * @sample {highstock} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @sample {highmaps} maps/tooltip/background-border/ Background and border demo + * @sample {highmaps} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @default rgba(247,247,247,0.85) + */ + backgroundColor: color('#f7f7f7').setOpacity(0.85).get(), + + /** + * The pixel width of the tooltip border. + * + * In styled mode, the stroke width is set in the `.highcharts-tooltip-box` class. + * + * @type {Number} + * @sample {highcharts} highcharts/tooltip/bordercolor-default/ 2px by default + * @sample {highcharts} highcharts/tooltip/borderwidth/ No border (shadow only) + * @sample {highcharts} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @sample {highstock} stock/tooltip/general/ Custom tooltip + * @sample {highstock} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @sample {highmaps} maps/tooltip/background-border/ Background and border demo + * @sample {highmaps} highcharts/css/tooltip-border-background/ Tooltip in styled mode + * @default 1 + */ + borderWidth: 1, + + /** + * The HTML of the tooltip header line. Variables are enclosed by + * curly brackets. Available variables are `point.key`, `series.name`, + * `series.color` and other members from the `point` and `series` + * objects. The `point.key` variable contains the category name, x + * value or datetime string depending on the type of axis. For datetime + * axes, the `point.key` date format can be set using tooltip.xDateFormat. + * + * @type {String} + * @sample {highcharts} highcharts/tooltip/footerformat/ + * A HTML table in the tooltip + * @sample {highstock} highcharts/tooltip/footerformat/ + * A HTML table in the tooltip + * @sample {highmaps} maps/tooltip/format/ Format demo + */ + headerFormat: '{point.key}
', + + /** + * The HTML of the point's line in the tooltip. Variables are enclosed + * by curly brackets. Available variables are point.x, point.y, series. + * name and series.color and other properties on the same form. Furthermore, + * point.y can be extended by the `tooltip.valuePrefix` and `tooltip. + * valueSuffix` variables. This can also be overridden for each series, + * which makes it a good hook for displaying units. + * + * In styled mode, the dot is colored by a class name rather + * than the point color. + * + * @type {String} + * @sample {highcharts} highcharts/tooltip/pointformat/ A different point format with value suffix + * @sample {highmaps} maps/tooltip/format/ Format demo + * @default \u25CF {series.name}: {point.y}
+ * @since 2.2 + */ + pointFormat: '\u25CF {series.name}: {point.y}
', + + /** + * Whether to apply a drop shadow to the tooltip. + * + * @type {Boolean} + * @sample {highcharts} highcharts/tooltip/bordercolor-default/ True by default + * @sample {highcharts} highcharts/tooltip/shadow/ False + * @sample {highmaps} maps/tooltip/positioner/ Fixed tooltip position, border and shadow disabled + * @default true + */ + shadow: true, + + /** + * CSS styles for the tooltip. The tooltip can also be styled through + * the CSS class `.highcharts-tooltip`. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/tooltip/style/ Greater padding, bold text + * @default { "color": "#333333", "cursor": "default", "fontSize": "12px", "pointerEvents": "none", "whiteSpace": "nowrap" } + */ + style: { + color: '#333333', + cursor: 'default', + fontSize: '12px', + pointerEvents: 'none', // #1686 http://caniuse.com/#feat=pointer-events + whiteSpace: 'nowrap' + } + + + + /** + * The color of the tooltip border. When `null`, the border takes the + * color of the corresponding series or point. + * + * @type {Color} + * @sample {highcharts} highcharts/tooltip/bordercolor-default/ + * Follow series by default + * @sample {highcharts} highcharts/tooltip/bordercolor-black/ + * Black border + * @sample {highstock} stock/tooltip/general/ + * Styled tooltip + * @sample {highmaps} maps/tooltip/background-border/ + * Background and border demo + * @default null + * @apioption tooltip.borderColor + */ + + /** + * Since 4.1, the crosshair definitions are moved to the Axis object + * in order for a better separation from the tooltip. See [xAxis.crosshair](#xAxis. + * crosshair). + * + * @type {Mixed} + * @deprecated + * @sample {highcharts} highcharts/tooltip/crosshairs-x/ + * Enable a crosshair for the x value + * @default true + * @apioption tooltip.crosshairs + */ + + /** + * Whether the tooltip should follow the mouse as it moves across columns, + * pie slices and other point types with an extent. By default it behaves + * this way for scatter, bubble and pie series by override in the `plotOptions` + * for those series types. + * + * For touch moves to behave the same way, [followTouchMove](#tooltip. + * followTouchMove) must be `true` also. + * + * @type {Boolean} + * @default {highcharts} false + * @default {highstock} false + * @default {highmaps} true + * @since 3.0 + * @apioption tooltip.followPointer + */ + + /** + * Whether the tooltip should follow the finger as it moves on a touch + * device. If this is `true` and [chart.panning](#chart.panning) is + * set,`followTouchMove` will take over one-finger touches, so the user + * needs to use two fingers for zooming and panning. + * + * @type {Boolean} + * @default {highcharts} true + * @default {highstock} true + * @default {highmaps} false + * @since 3.0.1 + * @apioption tooltip.followTouchMove + */ + + /** + * Callback function to format the text of the tooltip from scratch. Return + * `false` to disable tooltip for a specific point on series. + * + * A subset of HTML is supported. Unless `useHTML` is true, the HTML of the + * tooltip is parsed and converted to SVG, therefore this isn't a complete HTML + * renderer. The following tags are supported: ``, ``, ``, ``, + * `
`, ``. Spans can be styled with a `style` attribute, + * but only text-related CSS that is shared with SVG is handled. + * + * Since version 2.1 the tooltip can be shared between multiple series + * through the `shared` option. The available data in the formatter + * differ a bit depending on whether the tooltip is shared or not. In + * a shared tooltip, all properties except `x`, which is common for + * all points, are kept in an array, `this.points`. + * + * Available data are: + * + *
+ * + *
this.percentage (not shared) / this.points[i].percentage (shared)
+ * + *
Stacked series and pies only. The point's percentage of the total. + *
+ * + *
this.point (not shared) / this.points[i].point (shared)
+ * + *
The point object. The point name, if defined, is available through + * `this.point.name`.
+ * + *
this.points
+ * + *
In a shared tooltip, this is an array containing all other properties + * for each point.
+ * + *
this.series (not shared) / this.points[i].series (shared)
+ * + *
The series object. The series name is available through + * `this.series.name`.
+ * + *
this.total (not shared) / this.points[i].total (shared)
+ * + *
Stacked series only. The total value at this point's x value. + *
+ * + *
this.x
+ * + *
The x value. This property is the same regardless of the tooltip + * being shared or not.
+ * + *
this.y (not shared) / this.points[i].y (shared)
+ * + *
The y value.
+ * + *
+ * + * @type {Function} + * @sample {highcharts} highcharts/tooltip/formatter-simple/ + * Simple string formatting + * @sample {highcharts} highcharts/tooltip/formatter-shared/ + * Formatting with shared tooltip + * @sample {highstock} stock/tooltip/formatter/ + * Formatting with shared tooltip + * @sample {highmaps} maps/tooltip/formatter/ + * String formatting + * @apioption tooltip.formatter + */ + + /** + * The number of milliseconds to wait until the tooltip is hidden when + * mouse out from a point or chart. + * + * @type {Number} + * @default 500 + * @since 3.0 + * @apioption tooltip.hideDelay + */ + + /** + * A callback function for formatting the HTML output for a single point + * in the tooltip. Like the `pointFormat` string, but with more flexibility. + * + * @type {Function} + * @context Point + * @since 4.1.0 + * @apioption tooltip.pointFormatter + */ + + /** + * A callback function to place the tooltip in a default position. The + * callback receives three parameters: `labelWidth`, `labelHeight` and + * `point`, where point contains values for `plotX` and `plotY` telling + * where the reference point is in the plot area. Add `chart.plotLeft` + * and `chart.plotTop` to get the full coordinates. + * + * The return should be an object containing x and y values, for example + * `{ x: 100, y: 100 }`. + * + * @type {Function} + * @sample {highcharts} highcharts/tooltip/positioner/ A fixed tooltip position + * @sample {highstock} stock/tooltip/positioner/ A fixed tooltip position on top of the chart + * @sample {highmaps} maps/tooltip/positioner/ A fixed tooltip position + * @since 2.2.4 + * @apioption tooltip.positioner + */ + + /** + * The name of a symbol to use for the border around the tooltip. + * + * @type {String} + * @default callout + * @validvalue ["callout", "square"] + * @since 4.0 + * @apioption tooltip.shape + */ + + /** + * When the tooltip is shared, the entire plot area will capture mouse + * movement or touch events. Tooltip texts for series types with ordered + * data (not pie, scatter, flags etc) will be shown in a single bubble. + * This is recommended for single series charts and for tablet/mobile + * optimized charts. + * + * See also [tooltip.split](#tooltip.split), that is better suited for + * charts with many series, especially line-type series. + * + * @type {Boolean} + * @sample {highcharts} highcharts/tooltip/shared-false/ False by default + * @sample {highcharts} highcharts/tooltip/shared-true/ True + * @sample {highcharts} highcharts/tooltip/shared-x-crosshair/ True with x axis crosshair + * @sample {highcharts} highcharts/tooltip/shared-true-mixed-types/ True with mixed series types + * @default false + * @since 2.1 + * @product highcharts highstock + * @apioption tooltip.shared + */ + + /** + * Split the tooltip into one label per series, with the header close + * to the axis. This is recommended over [shared](#tooltip.shared) tooltips + * for charts with multiple line series, generally making them easier + * to read. + * + * @productdesc {highstock} In Highstock, tooltips are split by default + * since v6.0.0. Stock charts typically contain multi-dimension points + * and multiple panes, making split tooltips the preferred layout over + * the previous `shared` tooltip. + * + * @type {Boolean} + * @sample {highcharts} highcharts/tooltip/split/ Split tooltip + * @sample {highstock} highcharts/tooltip/split/ Split tooltip + * @sample {highmaps} highcharts/tooltip/split/ Split tooltip + * @default {highcharts} false + * @default {highstock} true + * @product highcharts highstock + * @since 5.0.0 + * @apioption tooltip.split + */ + + /** + * Use HTML to render the contents of the tooltip instead of SVG. Using + * HTML allows advanced formatting like tables and images in the tooltip. + * It is also recommended for rtl languages as it works around rtl + * bugs in early Firefox. + * + * @type {Boolean} + * @sample {highcharts} highcharts/tooltip/footerformat/ A table for value alignment + * @sample {highcharts} highcharts/tooltip/fullhtml/ Full HTML tooltip + * @sample {highstock} highcharts/tooltip/footerformat/ A table for value alignment + * @sample {highstock} highcharts/tooltip/fullhtml/ Full HTML tooltip + * @sample {highmaps} maps/tooltip/usehtml/ Pure HTML tooltip + * @default false + * @since 2.2 + * @apioption tooltip.useHTML + */ + + /** + * How many decimals to show in each series' y value. This is overridable + * in each series' tooltip options object. The default is to preserve + * all decimals. + * + * @type {Number} + * @sample {highcharts} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highstock} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highmaps} maps/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @since 2.2 + * @apioption tooltip.valueDecimals + */ + + /** + * A string to prepend to each series' y value. Overridable in each + * series' tooltip options object. + * + * @type {String} + * @sample {highcharts} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highstock} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highmaps} maps/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @since 2.2 + * @apioption tooltip.valuePrefix + */ + + /** + * A string to append to each series' y value. Overridable in each series' + * tooltip options object. + * + * @type {String} + * @sample {highcharts} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highstock} highcharts/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @sample {highmaps} maps/tooltip/valuedecimals/ Set decimals, prefix and suffix for the value + * @since 2.2 + * @apioption tooltip.valueSuffix + */ + + /** + * The format for the date in the tooltip header if the X axis is a + * datetime axis. The default is a best guess based on the smallest + * distance between points in the chart. + * + * @type {String} + * @sample {highcharts} highcharts/tooltip/xdateformat/ A different format + * @product highcharts highstock + * @apioption tooltip.xDateFormat + */ + }, + + + /** + * Highchart by default puts a credits label in the lower right corner + * of the chart. This can be changed using these options. + */ + credits: { + + /** + * Whether to show the credits text. + * + * @type {Boolean} + * @sample {highcharts} highcharts/credits/enabled-false/ Credits disabled + * @sample {highstock} stock/credits/enabled/ Credits disabled + * @sample {highmaps} maps/credits/enabled-false/ Credits disabled + * @default true + */ + enabled: true, + + /** + * The URL for the credits label. + * + * @type {String} + * @sample {highcharts} highcharts/credits/href/ Custom URL and text + * @sample {highmaps} maps/credits/customized/ Custom URL and text + * @default {highcharts} http://www.highcharts.com + * @default {highstock} "http://www.highcharts.com" + * @default {highmaps} http://www.highcharts.com + */ + href: 'http://www.highcharts.com', + + /** + * Position configuration for the credits label. + * + * @type {Object} + * @sample {highcharts} highcharts/credits/position-left/ Left aligned + * @sample {highcharts} highcharts/credits/position-left/ Left aligned + * @sample {highmaps} maps/credits/customized/ Left aligned + * @sample {highmaps} maps/credits/customized/ Left aligned + * @since 2.1 + */ + position: { + + /** + * Horizontal alignment of the credits. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @default right + */ + align: 'right', + + /** + * Horizontal pixel offset of the credits. + * + * @type {Number} + * @default -10 + */ + x: -10, + + /** + * Vertical alignment of the credits. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @default bottom + */ + verticalAlign: 'bottom', + + /** + * Vertical pixel offset of the credits. + * + * @type {Number} + * @default -5 + */ + y: -5 + }, + + + /** + * CSS styles for the credits label. + * + * @type {CSSObject} + * @see In styled mode, credits styles can be set with the + * `.highcharts-credits` class. + * @default { "cursor": "pointer", "color": "#999999", "fontSize": "10px" } + */ + style: { + + cursor: 'pointer', + color: '#999999', + fontSize: '9px' + }, + + + /** + * The text for the credits label. + * + * @productdesc {highmaps} + * If a map is loaded as GeoJSON, the text defaults to `Highcharts @ + * {map-credits}`. Otherwise, it defaults to `Highcharts.com`. + * + * @type {String} + * @sample {highcharts} highcharts/credits/href/ Custom URL and text + * @sample {highmaps} maps/credits/customized/ Custom URL and text + * @default {highcharts|highstock} Highcharts.com + */ + text: 'Highcharts.com' + } +}; + +/** + * Merge the default options with custom options and return the new options + * structure. Commonly used for defining reusable templates. + * + * @function #setOptions + * @memberOf Highcharts + * @sample highcharts/global/useutc-false Setting a global option + * @sample highcharts/members/setoptions Applying a global theme + * @param {Object} options The new custom chart options. + * @returns {Object} Updated options. + */ +H.setOptions = function (options) { + + // Copy in the default options + H.defaultOptions = merge(true, H.defaultOptions, options); + + // Re-initiate time + H.time.init(); + + return H.defaultOptions; +}; + +/** + * Get the updated default options. Until 3.0.7, merely exposing defaultOptions for outside modules + * wasn't enough because the setOptions method created a new object. + */ +H.getOptions = function () { + return H.defaultOptions; +}; + + +// Series defaults +H.defaultPlotOptions = H.defaultOptions.plotOptions; + + +// Time utilities +H.time = new H.Time(); + +/** + * Formats a JavaScript date timestamp (milliseconds since Jan 1st 1970) into a + * human readable date string. The format is a subset of the formats for PHP's + * [strftime]{@link + * http://www.php.net/manual/en/function.strftime.php} function. Additional + * formats can be given in the {@link Highcharts.dateFormats} hook. + * + * Since v6.0.5, all internal dates are formatted through the + * [Chart.time](Chart#time) instance to respect chart-level time settings. The + * `Highcharts.dateFormat` function only reflects global time settings set with + * `setOptions`. + * + * @function #dateFormat + * @memberOf Highcharts + * @param {String} format - The desired format where various time + * representations are prefixed with %. + * @param {Number} timestamp - The JavaScript timestamp. + * @param {Boolean} [capitalize=false] - Upper case first letter in the return. + * @returns {String} The formatted date. + */ +H.dateFormat = function (format, timestamp, capitalize) { + return H.time.dateFormat(format, timestamp, capitalize); +}; + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var correctFloat = H.correctFloat, + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + isNumber = H.isNumber, + merge = H.merge, + pick = H.pick, + deg2rad = H.deg2rad; + +/** + * The Tick class + */ +H.Tick = function (axis, pos, type, noLabel) { + this.axis = axis; + this.pos = pos; + this.type = type || ''; + this.isNew = true; + this.isNewLabel = true; + + if (!type && !noLabel) { + this.addLabel(); + } +}; + +H.Tick.prototype = { + /** + * Write the tick label + */ + addLabel: function () { + var tick = this, + axis = tick.axis, + options = axis.options, + chart = axis.chart, + categories = axis.categories, + names = axis.names, + pos = tick.pos, + labelOptions = options.labels, + str, + tickPositions = axis.tickPositions, + isFirst = pos === tickPositions[0], + isLast = pos === tickPositions[tickPositions.length - 1], + value = categories ? + pick(categories[pos], names[pos], pos) : + pos, + label = tick.label, + tickPositionInfo = tickPositions.info, + dateTimeLabelFormat; + + // Set the datetime label format. If a higher rank is set for this + // position, use that. If not, use the general format. + if (axis.isDatetimeAxis && tickPositionInfo) { + dateTimeLabelFormat = + options.dateTimeLabelFormats[ + tickPositionInfo.higherRanks[pos] || + tickPositionInfo.unitName + ]; + } + // set properties for access in render method + tick.isFirst = isFirst; + tick.isLast = isLast; + + // get the string + str = axis.labelFormatter.call({ + axis: axis, + chart: chart, + isFirst: isFirst, + isLast: isLast, + dateTimeLabelFormat: dateTimeLabelFormat, + value: axis.isLog ? correctFloat(axis.lin2log(value)) : value, + pos: pos + }); + + // first call + if (!defined(label)) { + + tick.label = label = + defined(str) && labelOptions.enabled ? + chart.renderer.text( + str, + 0, + 0, + labelOptions.useHTML + ) + + // without position absolute, IE export sometimes is + // wrong. + .css(merge(labelOptions.style)) + + .add(axis.labelGroup) : + null; + + // Un-rotated length + tick.labelLength = label && label.getBBox().width; + // Base value to detect change for new calls to getBBox + tick.rotation = 0; + + // update + } else if (label) { + label.attr({ text: str }); + } + }, + + /** + * Get the offset height or width of the label + */ + getLabelSize: function () { + return this.label ? + this.label.getBBox()[this.axis.horiz ? 'height' : 'width'] : + 0; + }, + + /** + * Handle the label overflow by adjusting the labels to the left and right + * edge, or hide them if they collide into the neighbour label. + */ + handleOverflow: function (xy) { + var axis = this.axis, + labelOptions = axis.options.labels, + pxPos = xy.x, + chartWidth = axis.chart.chartWidth, + spacing = axis.chart.spacing, + leftBound = pick(axis.labelLeft, Math.min(axis.pos, spacing[3])), + rightBound = pick( + axis.labelRight, + Math.max( + !axis.isRadial ? axis.pos + axis.len : 0, + chartWidth - spacing[1] + ) + ), + label = this.label, + rotation = this.rotation, + factor = { left: 0, center: 0.5, right: 1 }[ + axis.labelAlign || label.attr('align') + ], + labelWidth = label.getBBox().width, + slotWidth = axis.getSlotWidth(), + modifiedSlotWidth = slotWidth, + xCorrection = factor, + goRight = 1, + leftPos, + rightPos, + textWidth, + css = {}; + + // Check if the label overshoots the chart spacing box. If it does, move + // it. If it now overshoots the slotWidth, add ellipsis. + if (!rotation && labelOptions.overflow !== false) { + leftPos = pxPos - factor * labelWidth; + rightPos = pxPos + (1 - factor) * labelWidth; + + if (leftPos < leftBound) { + modifiedSlotWidth = xy.x + modifiedSlotWidth * (1 - factor) - leftBound; + } else if (rightPos > rightBound) { + modifiedSlotWidth = + rightBound - xy.x + modifiedSlotWidth * factor; + goRight = -1; + } + + modifiedSlotWidth = Math.min(slotWidth, modifiedSlotWidth); // #4177 + if (modifiedSlotWidth < slotWidth && axis.labelAlign === 'center') { + xy.x += ( + goRight * + ( + slotWidth - + modifiedSlotWidth - + xCorrection * ( + slotWidth - Math.min(labelWidth, modifiedSlotWidth) + ) + ) + ); + } + // If the label width exceeds the available space, set a text width + // to be picked up below. Also, if a width has been set before, we + // need to set a new one because the reported labelWidth will be + // limited by the box (#3938). + if ( + labelWidth > modifiedSlotWidth || + (axis.autoRotation && (label.styles || {}).width) + ) { + textWidth = modifiedSlotWidth; + } + + // Add ellipsis to prevent rotated labels to be clipped against the edge + // of the chart + } else if (rotation < 0 && pxPos - factor * labelWidth < leftBound) { + textWidth = Math.round( + pxPos / Math.cos(rotation * deg2rad) - leftBound + ); + } else if (rotation > 0 && pxPos + factor * labelWidth > rightBound) { + textWidth = Math.round( + (chartWidth - pxPos) / Math.cos(rotation * deg2rad) + ); + } + + if (textWidth) { + css.width = textWidth; + if (!(labelOptions.style || {}).textOverflow) { + css.textOverflow = 'ellipsis'; + } + label.css(css); + } + }, + + /** + * Get the x and y position for ticks and labels + */ + getPosition: function (horiz, pos, tickmarkOffset, old) { + var axis = this.axis, + chart = axis.chart, + cHeight = (old && chart.oldChartHeight) || chart.chartHeight; + + return { + x: horiz ? + ( + axis.translate(pos + tickmarkOffset, null, null, old) + + axis.transB + ) : + ( + axis.left + + axis.offset + + ( + axis.opposite ? + ( + ( + (old && chart.oldChartWidth) || + chart.chartWidth + ) - + axis.right - + axis.left + ) : + 0 + ) + ), + + y: horiz ? + ( + cHeight - + axis.bottom + + axis.offset - + (axis.opposite ? axis.height : 0) + ) : + ( + cHeight - + axis.translate(pos + tickmarkOffset, null, null, old) - + axis.transB + ) + }; + + }, + + /** + * Get the x, y position of the tick label + */ + getLabelPosition: function ( + x, + y, + label, + horiz, + labelOptions, + tickmarkOffset, + index, + step + ) { + var axis = this.axis, + transA = axis.transA, + reversed = axis.reversed, + staggerLines = axis.staggerLines, + rotCorr = axis.tickRotCorr || { x: 0, y: 0 }, + yOffset = labelOptions.y, + + // Adjust for label alignment if we use reserveSpace: true (#5286) + labelOffsetCorrection = ( + !horiz && !axis.reserveSpaceDefault ? + -axis.labelOffset * ( + axis.labelAlign === 'center' ? 0.5 : 1 + ) : + 0 + ), + line; + + if (!defined(yOffset)) { + if (axis.side === 0) { + yOffset = label.rotation ? -8 : -label.getBBox().height; + } else if (axis.side === 2) { + yOffset = rotCorr.y + 8; + } else { + // #3140, #3140 + yOffset = Math.cos(label.rotation * deg2rad) * + (rotCorr.y - label.getBBox(false, 0).height / 2); + } + } + + x = x + + labelOptions.x + + labelOffsetCorrection + + rotCorr.x - + ( + tickmarkOffset && horiz ? + tickmarkOffset * transA * (reversed ? -1 : 1) : + 0 + ); + y = y + yOffset - (tickmarkOffset && !horiz ? + tickmarkOffset * transA * (reversed ? 1 : -1) : 0); + + // Correct for staggered labels + if (staggerLines) { + line = (index / (step || 1) % staggerLines); + if (axis.opposite) { + line = staggerLines - line - 1; + } + y += line * (axis.labelOffset / staggerLines); + } + + return { + x: x, + y: Math.round(y) + }; + }, + + /** + * Extendible method to return the path of the marker + */ + getMarkPath: function (x, y, tickLength, tickWidth, horiz, renderer) { + return renderer.crispLine([ + 'M', + x, + y, + 'L', + x + (horiz ? 0 : -tickLength), + y + (horiz ? tickLength : 0) + ], tickWidth); + }, + + /** + * Renders the gridLine. + * @param {Boolean} old Whether or not the tick is old + * @param {number} opacity The opacity of the grid line + * @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1 + * @return {undefined} + */ + renderGridLine: function (old, opacity, reverseCrisp) { + var tick = this, + axis = tick.axis, + options = axis.options, + gridLine = tick.gridLine, + gridLinePath, + attribs = {}, + pos = tick.pos, + type = tick.type, + tickmarkOffset = axis.tickmarkOffset, + renderer = axis.chart.renderer; + + + var gridPrefix = type ? type + 'Grid' : 'grid', + gridLineWidth = options[gridPrefix + 'LineWidth'], + gridLineColor = options[gridPrefix + 'LineColor'], + dashStyle = options[gridPrefix + 'LineDashStyle']; + + + if (!gridLine) { + + attribs.stroke = gridLineColor; + attribs['stroke-width'] = gridLineWidth; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + + if (!type) { + attribs.zIndex = 1; + } + if (old) { + attribs.opacity = 0; + } + tick.gridLine = gridLine = renderer.path() + .attr(attribs) + .addClass( + 'highcharts-' + (type ? type + '-' : '') + 'grid-line' + ) + .add(axis.gridGroup); + } + + // If the parameter 'old' is set, the current call will be followed + // by another call, therefore do not do any animations this time + if (!old && gridLine) { + gridLinePath = axis.getPlotLinePath( + pos + tickmarkOffset, + gridLine.strokeWidth() * reverseCrisp, + old, true + ); + if (gridLinePath) { + gridLine[tick.isNew ? 'attr' : 'animate']({ + d: gridLinePath, + opacity: opacity + }); + } + } + }, + + /** + * Renders the tick mark. + * @param {Object} xy The position vector of the mark + * @param {number} xy.x The x position of the mark + * @param {number} xy.y The y position of the mark + * @param {number} opacity The opacity of the mark + * @param {number} reverseCrisp Modifier for avoiding overlapping 1 or -1 + * @return {undefined} + */ + renderMark: function (xy, opacity, reverseCrisp) { + var tick = this, + axis = tick.axis, + options = axis.options, + renderer = axis.chart.renderer, + type = tick.type, + tickPrefix = type ? type + 'Tick' : 'tick', + tickSize = axis.tickSize(tickPrefix), + mark = tick.mark, + isNewMark = !mark, + x = xy.x, + y = xy.y; + + + var tickWidth = pick( + options[tickPrefix + 'Width'], + !type && axis.isXAxis ? 1 : 0 + ), // X axis defaults to 1 + tickColor = options[tickPrefix + 'Color']; + + + if (tickSize) { + + // negate the length + if (axis.opposite) { + tickSize[0] = -tickSize[0]; + } + + // First time, create it + if (isNewMark) { + tick.mark = mark = renderer.path() + .addClass('highcharts-' + (type ? type + '-' : '') + 'tick') + .add(axis.axisGroup); + + + mark.attr({ + stroke: tickColor, + 'stroke-width': tickWidth + }); + + } + mark[isNewMark ? 'attr' : 'animate']({ + d: tick.getMarkPath( + x, + y, + tickSize[0], + mark.strokeWidth() * reverseCrisp, + axis.horiz, + renderer), + opacity: opacity + }); + + } + }, + + /** + * Renders the tick label. + * Note: The label should already be created in init(), so it should only + * have to be moved into place. + * @param {Object} xy The position vector of the label + * @param {number} xy.x The x position of the label + * @param {number} xy.y The y position of the label + * @param {Boolean} old Whether or not the tick is old + * @param {number} opacity The opacity of the label + * @param {number} index The index of the tick + * @return {undefined} + */ + renderLabel: function (xy, old, opacity, index) { + var tick = this, + axis = tick.axis, + horiz = axis.horiz, + options = axis.options, + label = tick.label, + labelOptions = options.labels, + step = labelOptions.step, + tickmarkOffset = axis.tickmarkOffset, + show = true, + x = xy.x, + y = xy.y; + if (label && isNumber(x)) { + label.xy = xy = tick.getLabelPosition( + x, + y, + label, + horiz, + labelOptions, + tickmarkOffset, + index, + step + ); + + // Apply show first and show last. If the tick is both first and + // last, it is a single centered tick, in which case we show the + // label anyway (#2100). + if ( + ( + tick.isFirst && + !tick.isLast && + !pick(options.showFirstLabel, 1) + ) || + ( + tick.isLast && + !tick.isFirst && + !pick(options.showLastLabel, 1) + ) + ) { + show = false; + + // Handle label overflow and show or hide accordingly + } else if ( + horiz && + !labelOptions.step && + !labelOptions.rotation && + !old && + opacity !== 0 + ) { + tick.handleOverflow(xy); + } + + // apply step + if (step && index % step) { + // show those indices dividable by step + show = false; + } + + // Set the new position, and show or hide + if (show && isNumber(xy.y)) { + xy.opacity = opacity; + label[tick.isNewLabel ? 'attr' : 'animate'](xy); + tick.isNewLabel = false; + } else { + label.attr('y', -9999); // #1338 + tick.isNewLabel = true; + } + } + }, + + /** + * Put everything in place + * + * @param index {Number} + * @param old {Boolean} Use old coordinates to prepare an animation into new + * position + */ + render: function (index, old, opacity) { + var tick = this, + axis = tick.axis, + horiz = axis.horiz, + pos = tick.pos, + tickmarkOffset = axis.tickmarkOffset, + xy = tick.getPosition(horiz, pos, tickmarkOffset, old), + x = xy.x, + y = xy.y, + reverseCrisp = ((horiz && x === axis.pos + axis.len) || + (!horiz && y === axis.pos)) ? -1 : 1; // #1480, #1687 + + opacity = pick(opacity, 1); + this.isActive = true; + + // Create the grid line + this.renderGridLine(old, opacity, reverseCrisp); + + // create the tick mark + this.renderMark(xy, opacity, reverseCrisp); + + // the label is created on init - now move it into place + this.renderLabel(xy, old, opacity, index); + + tick.isNew = false; + }, + + /** + * Destructor for the tick prototype + */ + destroy: function () { + destroyObjectProperties(this, this.axis); + } +}; + +}(Highcharts)); +var Axis = (function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ + +var addEvent = H.addEvent, + animObject = H.animObject, + arrayMax = H.arrayMax, + arrayMin = H.arrayMin, + color = H.color, + correctFloat = H.correctFloat, + defaultOptions = H.defaultOptions, + defined = H.defined, + deg2rad = H.deg2rad, + destroyObjectProperties = H.destroyObjectProperties, + each = H.each, + extend = H.extend, + fireEvent = H.fireEvent, + format = H.format, + getMagnitude = H.getMagnitude, + grep = H.grep, + inArray = H.inArray, + isArray = H.isArray, + isNumber = H.isNumber, + isString = H.isString, + merge = H.merge, + normalizeTickInterval = H.normalizeTickInterval, + objectEach = H.objectEach, + pick = H.pick, + removeEvent = H.removeEvent, + splat = H.splat, + syncTimeout = H.syncTimeout, + Tick = H.Tick; + +/** + * Create a new axis object. Called internally when instanciating a new chart or + * adding axes by {@link Highcharts.Chart#addAxis}. + * + * A chart can have from 0 axes (pie chart) to multiples. In a normal, single + * series cartesian chart, there is one X axis and one Y axis. + * + * The X axis or axes are referenced by {@link Highcharts.Chart.xAxis}, which is + * an array of Axis objects. If there is only one axis, it can be referenced + * through `chart.xAxis[0]`, and multiple axes have increasing indices. The same + * pattern goes for Y axes. + * + * If you need to get the axes from a series object, use the `series.xAxis` and + * `series.yAxis` properties. These are not arrays, as one series can only be + * associated to one X and one Y axis. + * + * A third way to reference the axis programmatically is by `id`. Add an `id` in + * the axis configuration options, and get the axis by + * {@link Highcharts.Chart#get}. + * + * Configuration options for the axes are given in options.xAxis and + * options.yAxis. + * + * @class Highcharts.Axis + * @memberOf Highcharts + * @param {Highcharts.Chart} chart - The Chart instance to apply the axis on. + * @param {Object} options - Axis options + */ +var Axis = function () { + this.init.apply(this, arguments); +}; + +H.extend(Axis.prototype, /** @lends Highcharts.Axis.prototype */{ + + /** + * The X axis or category axis. Normally this is the horizontal axis, + * though if the chart is inverted this is the vertical axis. In case of + * multiple axes, the xAxis node is an array of configuration objects. + * + * See [the Axis object](#Axis) for programmatic access to the axis. + * + * @productdesc {highmaps} + * In Highmaps, the axis is hidden, but it is used behind the scenes to + * control features like zooming and panning. Zooming is in effect the same + * as setting the extremes of one of the exes. + * + * @optionparent xAxis + */ + defaultOptions: { + /** + * Whether to allow decimals in this axis' ticks. When counting + * integers, like persons or hits on a web page, decimals should + * be avoided in the labels. + * + * @type {Boolean} + * @see [minTickInterval](#xAxis.minTickInterval) + * @sample {highcharts|highstock} + * highcharts/yaxis/allowdecimals-true/ + * True by default + * @sample {highcharts|highstock} + * highcharts/yaxis/allowdecimals-false/ + * False + * @default true + * @since 2.0 + * @apioption xAxis.allowDecimals + */ + // allowDecimals: null, + + + /** + * When using an alternate grid color, a band is painted across the + * plot area between every other grid line. + * + * @type {Color} + * @sample {highcharts} highcharts/yaxis/alternategridcolor/ + * Alternate grid color on the Y axis + * @sample {highstock} stock/xaxis/alternategridcolor/ + * Alternate grid color on the Y axis + * @default null + * @apioption xAxis.alternateGridColor + */ + // alternateGridColor: null, + + /** + * An array defining breaks in the axis, the sections defined will be + * left out and all the points shifted closer to each other. + * + * @productdesc {highcharts} + * Requires that the broken-axis.js module is loaded. + * + * @type {Array} + * @sample {highcharts} + * highcharts/axisbreak/break-simple/ + * Simple break + * @sample {highcharts|highstock} + * highcharts/axisbreak/break-visualized/ + * Advanced with callback + * @sample {highstock} + * stock/demo/intraday-breaks/ + * Break on nights and weekends + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.breaks + */ + + /** + * A number indicating how much space should be left between the start + * and the end of the break. The break size is given in axis units, + * so for instance on a `datetime` axis, a break size of 3600000 would + * indicate the equivalent of an hour. + * + * @type {Number} + * @default 0 + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.breaks.breakSize + */ + + /** + * The point where the break starts. + * + * @type {Number} + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.breaks.from + */ + + /** + * Defines an interval after which the break appears again. By default + * the breaks do not repeat. + * + * @type {Number} + * @default 0 + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.breaks.repeat + */ + + /** + * The point where the break ends. + * + * @type {Number} + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.breaks.to + */ + + /** + * If categories are present for the xAxis, names are used instead of + * numbers for that axis. Since Highcharts 3.0, categories can also + * be extracted by giving each point a [name](#series.data) and setting + * axis [type](#xAxis.type) to `category`. However, if you have multiple + * series, best practice remains defining the `categories` array. + * + * Example: + * + *
categories: ['Apples', 'Bananas', 'Oranges']
+ * + * @type {Array} + * @sample {highcharts} highcharts/chart/reflow-true/ + * With + * @sample {highcharts} highcharts/xaxis/categories/ + * Without + * @product highcharts + * @default null + * @apioption xAxis.categories + */ + // categories: [], + + /** + * The highest allowed value for automatically computed axis extremes. + * + * @type {Number} + * @see [floor](#xAxis.floor) + * @sample {highcharts|highstock} highcharts/yaxis/floor-ceiling/ + * Floor and ceiling + * @since 4.0 + * @product highcharts highstock + * @apioption xAxis.ceiling + */ + + /** + * A class name that opens for styling the axis by CSS, especially in + * Highcharts styled mode. The class name is applied to group elements + * for the grid, axis elements and labels. + * + * @type {String} + * @sample {highcharts|highstock|highmaps} + * highcharts/css/axis/ + * Multiple axes with separate styling + * @since 5.0.0 + * @apioption xAxis.className + */ + + /** + * Configure a crosshair that follows either the mouse pointer or the + * hovered point. + * + * In styled mode, the crosshairs are styled in the + * `.highcharts-crosshair`, `.highcharts-crosshair-thin` or + * `.highcharts-xaxis-category` classes. + * + * @productdesc {highstock} + * In Highstock, bu default, the crosshair is enabled on the X axis and + * disabled on the Y axis. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/xaxis/crosshair-both/ + * Crosshair on both axes + * @sample {highstock} stock/xaxis/crosshairs-xy/ + * Crosshair on both axes + * @sample {highmaps} highcharts/xaxis/crosshair-both/ + * Crosshair on both axes + * @default false + * @since 4.1 + * @apioption xAxis.crosshair + */ + + /** + * A class name for the crosshair, especially as a hook for styling. + * + * @type {String} + * @since 5.0.0 + * @apioption xAxis.crosshair.className + */ + + /** + * The color of the crosshair. Defaults to `#cccccc` for numeric and + * datetime axes, and `rgba(204,214,235,0.25)` for category axes, where + * the crosshair by default highlights the whole category. + * + * @type {Color} + * @sample {highcharts|highstock|highmaps} + * highcharts/xaxis/crosshair-customized/ + * Customized crosshairs + * @default #cccccc + * @since 4.1 + * @apioption xAxis.crosshair.color + */ + + /** + * The dash style for the crosshair. See + * [series.dashStyle](#plotOptions.series.dashStyle) + * for possible values. + * + * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", + * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", + * "DashDot", "LongDashDot", "LongDashDotDot"] + * @type {String} + * @sample {highcharts|highmaps} highcharts/xaxis/crosshair-dotted/ + * Dotted crosshair + * @sample {highstock} stock/xaxis/crosshair-dashed/ + * Dashed X axis crosshair + * @default Solid + * @since 4.1 + * @apioption xAxis.crosshair.dashStyle + */ + + /** + * Whether the crosshair should snap to the point or follow the pointer + * independent of points. + * + * @type {Boolean} + * @sample {highcharts|highstock} + * highcharts/xaxis/crosshair-snap-false/ + * True by default + * @sample {highmaps} + * maps/demo/latlon-advanced/ + * Snap is false + * @default true + * @since 4.1 + * @apioption xAxis.crosshair.snap + */ + + /** + * The pixel width of the crosshair. Defaults to 1 for numeric or + * datetime axes, and for one category width for category axes. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/crosshair-customized/ + * Customized crosshairs + * @sample {highstock} highcharts/xaxis/crosshair-customized/ + * Customized crosshairs + * @sample {highmaps} highcharts/xaxis/crosshair-customized/ + * Customized crosshairs + * @default 1 + * @since 4.1 + * @apioption xAxis.crosshair.width + */ + + /** + * The Z index of the crosshair. Higher Z indices allow drawing the + * crosshair on top of the series or behind the grid lines. + * + * @type {Number} + * @default 2 + * @since 4.1 + * @apioption xAxis.crosshair.zIndex + */ + + /** + * For a datetime axis, the scale will automatically adjust to the + * appropriate unit. This member gives the default string + * representations used for each unit. For intermediate values, + * different units may be used, for example the `day` unit can be used + * on midnight and `hour` unit be used for intermediate values on the + * same axis. For an overview of the replacement codes, see + * [dateFormat](#Highcharts.dateFormat). Defaults to: + * + *
{
+		 *     millisecond: '%H:%M:%S.%L',
+		 *     second: '%H:%M:%S',
+		 *     minute: '%H:%M',
+		 *     hour: '%H:%M',
+		 *     day: '%e. %b',
+		 *     week: '%e. %b',
+		 *     month: '%b \'%y',
+		 *     year: '%Y'
+		 * }
+ * + * @type {Object} + * @sample {highcharts} highcharts/xaxis/datetimelabelformats/ + * Different day format on X axis + * @sample {highstock} stock/xaxis/datetimelabelformats/ + * More information in x axis labels + * @product highcharts highstock + */ + dateTimeLabelFormats: { + millisecond: '%H:%M:%S.%L', + second: '%H:%M:%S', + minute: '%H:%M', + hour: '%H:%M', + day: '%e. %b', + week: '%e. %b', + month: '%b \'%y', + year: '%Y' + }, + + /** + * _Requires Accessibility module_ + * + * Description of the axis to screen reader users. + * + * @type {String} + * @default undefined + * @since 5.0.0 + * @apioption xAxis.description + */ + + /** + * Whether to force the axis to end on a tick. Use this option with + * the `maxPadding` option to control the axis end. + * + * @productdesc {highstock} + * In Highstock, `endOnTick` is always false when the navigator is + * enabled, to prevent jumpy scrolling. + * + * @sample {highcharts} highcharts/chart/reflow-true/ + * True by default + * @sample {highcharts} highcharts/yaxis/endontick/ + * False + * @sample {highstock} stock/demo/basic-line/ + * True by default + * @sample {highstock} stock/xaxis/endontick/ + * False + * @since 1.2.0 + */ + endOnTick: false, + + /** + * Event handlers for the axis. + * + * @apioption xAxis.events + */ + + /** + * An event fired after the breaks have rendered. + * + * @type {Function} + * @see [breaks](#xAxis.breaks) + * @sample {highcharts} highcharts/axisbreak/break-event/ + * AfterBreak Event + * @since 4.1.0 + * @product highcharts + * @apioption xAxis.events.afterBreaks + */ + + /** + * As opposed to the `setExtremes` event, this event fires after the + * final min and max values are computed and corrected for `minRange`. + * + * + * Fires when the minimum and maximum is set for the axis, either by + * calling the `.setExtremes()` method or by selecting an area in the + * chart. One parameter, `event`, is passed to the function, containing + * common event information. + * + * The new user set minimum and maximum values can be found by `event. + * min` and `event.max`. These reflect the axis minimum and maximum + * in axis values. The actual data extremes are found in `event.dataMin` + * and `event.dataMax`. + * + * @type {Function} + * @context Axis + * @since 2.3 + * @apioption xAxis.events.afterSetExtremes + */ + + /** + * An event fired when a break from this axis occurs on a point. + * + * @type {Function} + * @see [breaks](#xAxis.breaks) + * @context Axis + * @sample {highcharts} highcharts/axisbreak/break-visualized/ + * Visualization of a Break + * @since 4.1.0 + * @product highcharts + * @apioption xAxis.events.pointBreak + */ + + /** + * An event fired when a point falls inside a break from this axis. + * + * @type {Function} + * @context Axis + * @product highcharts highstock + * @apioption xAxis.events.pointInBreak + */ + + /** + * Fires when the minimum and maximum is set for the axis, either by + * calling the `.setExtremes()` method or by selecting an area in the + * chart. One parameter, `event`, is passed to the function, + * containing common event information. + * + * The new user set minimum and maximum values can be found by `event. + * min` and `event.max`. These reflect the axis minimum and maximum + * in data values. When an axis is zoomed all the way out from the + * "Reset zoom" button, `event.min` and `event.max` are null, and + * the new extremes are set based on `this.dataMin` and `this.dataMax`. + * + * @type {Function} + * @context Axis + * @sample {highstock} stock/xaxis/events-setextremes/ + * Log new extremes on x axis + * @since 1.2.0 + * @apioption xAxis.events.setExtremes + */ + + /** + * The lowest allowed value for automatically computed axis extremes. + * + * @type {Number} + * @see [ceiling](#yAxis.ceiling) + * @sample {highcharts} highcharts/yaxis/floor-ceiling/ + * Floor and ceiling + * @sample {highstock} stock/demo/lazy-loading/ + * Prevent negative stock price on Y axis + * @default null + * @since 4.0 + * @product highcharts highstock + * @apioption xAxis.floor + */ + + /** + * The dash or dot style of the grid lines. For possible values, see + * [this demonstration](http://jsfiddle.net/gh/get/library/pure/ + *highcharts/highcharts/tree/master/samples/highcharts/plotoptions/ + *series-dashstyle-all/). + * + * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", + * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", + * "DashDot", "LongDashDot", "LongDashDotDot"] + * @type {String} + * @sample {highcharts} highcharts/yaxis/gridlinedashstyle/ + * Long dashes + * @sample {highstock} stock/xaxis/gridlinedashstyle/ + * Long dashes + * @default Solid + * @since 1.2 + * @apioption xAxis.gridLineDashStyle + */ + + /** + * The Z index of the grid lines. + * + * @type {Number} + * @sample {highcharts|highstock} highcharts/xaxis/gridzindex/ + * A Z index of 4 renders the grid above the graph + * @default 1 + * @product highcharts highstock + * @apioption xAxis.gridZIndex + */ + + /** + * An id for the axis. This can be used after render time to get + * a pointer to the axis object through `chart.get()`. + * + * @type {String} + * @sample {highcharts} highcharts/xaxis/id/ + * Get the object + * @sample {highstock} stock/xaxis/id/ + * Get the object + * @default null + * @since 1.2.0 + * @apioption xAxis.id + */ + + /** + * The axis labels show the number or category for each tick. + * + * @productdesc {highmaps} + * X and Y axis labels are by default disabled in Highmaps, but the + * functionality is inherited from Highcharts and used on `colorAxis`, + * and can be enabled on X and Y axes too. + */ + labels: { + /** + * What part of the string the given position is anchored to. + * If `left`, the left side of the string is at the axis position. + * Can be one of `"left"`, `"center"` or `"right"`. Defaults to + * an intelligent guess based on which side of the chart the axis + * is on and the rotation of the label. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/xaxis/labels-align-left/ + * Left + * @sample {highcharts} highcharts/xaxis/labels-align-right/ + * Right + * @sample {highcharts} + * highcharts/xaxis/labels-reservespace-true/ + * Left-aligned labels on a vertical category axis + * @see [reserveSpace](#xAxis.labels.reserveSpace) + * @apioption xAxis.labels.align + */ + // align: 'center', + + /** + * For horizontal axes, the allowed degrees of label rotation + * to prevent overlapping labels. If there is enough space, + * labels are not rotated. As the chart gets narrower, it + * will start rotating the labels -45 degrees, then remove + * every second label and try again with rotations 0 and -45 etc. + * Set it to `false` to disable rotation, which will + * cause the labels to word-wrap if possible. + * + * @type {Array} + * @sample {highcharts|highstock} + * highcharts/xaxis/labels-autorotation-default/ + * Default auto rotation of 0 or -45 + * @sample {highcharts|highstock} + * highcharts/xaxis/labels-autorotation-0-90/ + * Custom graded auto rotation + * @default [-45] + * @since 4.1.0 + * @product highcharts highstock + * @apioption xAxis.labels.autoRotation + */ + + /** + * When each category width is more than this many pixels, we don't + * apply auto rotation. Instead, we lay out the axis label with word + * wrap. A lower limit makes sense when the label contains multiple + * short words that don't extend the available horizontal space for + * each label. + * + * @type {Number} + * @sample {highcharts} + * highcharts/xaxis/labels-autorotationlimit/ + * Lower limit + * @default 80 + * @since 4.1.5 + * @product highcharts + * @apioption xAxis.labels.autoRotationLimit + */ + + /** + * Polar charts only. The label's pixel distance from the perimeter + * of the plot area. + * + * @type {Number} + * @default 15 + * @product highcharts + * @apioption xAxis.labels.distance + */ + + /** + * Enable or disable the axis labels. + * + * @sample {highcharts} highcharts/xaxis/labels-enabled/ + * X axis labels disabled + * @sample {highstock} stock/xaxis/labels-enabled/ + * X axis labels disabled + * @default {highcharts|highstock} true + * @default {highmaps} false + */ + enabled: true, + + /** + * A [format string](http://www.highcharts.com/docs/chart- + * concepts/labels-and-string-formatting) for the axis label. + * + * @type {String} + * @sample {highcharts|highstock} highcharts/yaxis/labels-format/ + * Add units to Y axis label + * @default {value} + * @since 3.0 + * @apioption xAxis.labels.format + */ + + /** + * Callback JavaScript function to format the label. The value + * is given by `this.value`. Additional properties for `this` are + * `axis`, `chart`, `isFirst` and `isLast`. The value of the default + * label formatter can be retrieved by calling + * `this.axis.defaultLabelFormatter.call(this)` within the function. + * + * Defaults to: + * + *
function() {
+			 *     return this.value;
+			 * }
+ * + * @type {Function} + * @sample {highcharts} + * highcharts/xaxis/labels-formatter-linked/ + * Linked category names + * @sample {highcharts} + * highcharts/xaxis/labels-formatter-extended/ + * Modified numeric labels + * @sample {highstock} + * stock/xaxis/labels-formatter/ + * Added units on Y axis + * @apioption xAxis.labels.formatter + */ + + /** + * How to handle overflowing labels on horizontal axis. Can be + * undefined, `false` or `"justify"`. By default it aligns inside + * the chart area. If "justify", labels will not render outside + * the plot area. If `false`, it will not be aligned at all. + * If there is room to move it, it will be aligned to the edge, + * else it will be removed. + * + * @deprecated + * @validvalue [null, "justify"] + * @type {String} + * @since 2.2.5 + * @apioption xAxis.labels.overflow + */ + + /** + * The pixel padding for axis labels, to ensure white space between + * them. + * + * @type {Number} + * @default 5 + * @product highcharts + * @apioption xAxis.labels.padding + */ + + /** + * Whether to reserve space for the labels. By default, space is + * reserved for the labels in these cases: + * + * * On all horizontal axes. + * * On vertical axes if `label.align` is `right` on a left-side + * axis or `left` on a right-side axis. + * * On vertical axes if `label.align` is `center`. + * + * This can be turned off when for example the labels are rendered + * inside the plot area instead of outside. + * + * @type {Boolean} + * @sample {highcharts} highcharts/xaxis/labels-reservespace/ + * No reserved space, labels inside plot + * @sample {highcharts} + * highcharts/xaxis/labels-reservespace-true/ + * Left-aligned labels on a vertical category axis + * @see [labels.align](#xAxis.labels.align) + * @default null + * @since 4.1.10 + * @product highcharts + * @apioption xAxis.labels.reserveSpace + */ + + /** + * Rotation of the labels in degrees. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/labels-rotation/ + * X axis labels rotated 90° + * @default 0 + * @apioption xAxis.labels.rotation + */ + // rotation: 0, + + /** + * Horizontal axes only. The number of lines to spread the labels + * over to make room or tighter labels. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/labels-staggerlines/ + * Show labels over two lines + * @sample {highstock} stock/xaxis/labels-staggerlines/ + * Show labels over two lines + * @default null + * @since 2.1 + * @apioption xAxis.labels.staggerLines + */ + + /** + * To show only every _n_'th label on the axis, set the step to _n_. + * Setting the step to 2 shows every other label. + * + * By default, the step is calculated automatically to avoid + * overlap. To prevent this, set it to 1\. This usually only + * happens on a category axis, and is often a sign that you have + * chosen the wrong axis type. + * + * Read more at + * [Axis docs](http://www.highcharts.com/docs/chart-concepts/axes) + * => What axis should I use? + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/labels-step/ + * Showing only every other axis label on a categorized + * x axis + * @sample {highcharts} highcharts/xaxis/labels-step-auto/ + * Auto steps on a category axis + * @default null + * @since 2.1 + * @apioption xAxis.labels.step + */ + // step: null, + + + + /** + * CSS styles for the label. Use `whiteSpace: 'nowrap'` to prevent + * wrapping of category labels. Use `textOverflow: 'none'` to + * prevent ellipsis (dots). + * + * In styled mode, the labels are styled with the + * `.highcharts-axis-labels` class. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/xaxis/labels-style/ + * Red X axis labels + */ + style: { + color: '#666666', + cursor: 'default', + fontSize: '11px' + }, + + + /** + * Whether to [use HTML](http://www.highcharts.com/docs/chart- + * concepts/labels-and-string-formatting#html) to render the labels. + * + * @type {Boolean} + * @default false + * @apioption xAxis.labels.useHTML + */ + + /** + * The x position offset of the label relative to the tick position + * on the axis. + * + * @sample {highcharts} highcharts/xaxis/labels-x/ + * Y axis labels placed on grid lines + */ + x: 0 + + /** + * The y position offset of the label relative to the tick position + * on the axis. The default makes it adapt to the font size on + * bottom axis. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/labels-x/ + * Y axis labels placed on grid lines + * @default null + * @apioption xAxis.labels.y + */ + + /** + * The Z index for the axis labels. + * + * @type {Number} + * @default 7 + * @apioption xAxis.labels.zIndex + */ + }, + + /** + * Index of another axis that this axis is linked to. When an axis is + * linked to a master axis, it will take the same extremes as + * the master, but as assigned by min or max or by setExtremes. + * It can be used to show additional info, or to ease reading the + * chart by duplicating the scales. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/linkedto/ + * Different string formats of the same date + * @sample {highcharts} highcharts/yaxis/linkedto/ + * Y values on both sides + * @default null + * @since 2.0.2 + * @product highcharts highstock + * @apioption xAxis.linkedTo + */ + + /** + * The maximum value of the axis. If `null`, the max value is + * automatically calculated. + * + * If the `endOnTick` option is true, the `max` value might + * be rounded up. + * + * If a [tickAmount](#yAxis.tickAmount) is set, the axis may be extended + * beyond the set max in order to reach the given number of ticks. The + * same may happen in a chart with multiple axes, determined by [chart. + * alignTicks](#chart), where a `tickAmount` is applied internally. + * + * @type {Number} + * @sample {highcharts} highcharts/yaxis/max-200/ + * Y axis max of 200 + * @sample {highcharts} highcharts/yaxis/max-logarithmic/ + * Y axis max on logarithmic axis + * @sample {highstock} stock/xaxis/min-max/ + * Fixed min and max on X axis + * @sample {highmaps} maps/axis/min-max/ + * Pre-zoomed to a specific area + * @apioption xAxis.max + */ + + /** + * Padding of the max value relative to the length of the axis. A + * padding of 0.05 will make a 100px axis 5px longer. This is useful + * when you don't want the highest data value to appear on the edge + * of the plot area. When the axis' `max` option is set or a max extreme + * is set using `axis.setExtremes()`, the maxPadding will be ignored. + * + * @sample {highcharts} highcharts/yaxis/maxpadding/ + * Max padding of 0.25 on y axis + * @sample {highstock} stock/xaxis/minpadding-maxpadding/ + * Greater min- and maxPadding + * @sample {highmaps} maps/chart/plotbackgroundcolor-gradient/ + * Add some padding + * @default {highcharts} 0.01 + * @default {highstock|highmaps} 0 + * @since 1.2.0 + */ + maxPadding: 0.01, + + /** + * Deprecated. Use `minRange` instead. + * + * @deprecated + * @type {Number} + * @product highcharts highstock + * @apioption xAxis.maxZoom + */ + + /** + * The minimum value of the axis. If `null` the min value is + * automatically calculated. + * + * If the `startOnTick` option is true (default), the `min` value might + * be rounded down. + * + * The automatically calculated minimum value is also affected by + * [floor](#yAxis.floor), [softMin](#yAxis.softMin), + * [minPadding](#yAxis.minPadding), [minRange](#yAxis.minRange) + * as well as [series.threshold](#plotOptions.series.threshold) + * and [series.softThreshold](#plotOptions.series.softThreshold). + * + * @type {Number} + * @sample {highcharts} highcharts/yaxis/min-startontick-false/ + * -50 with startOnTick to false + * @sample {highcharts} highcharts/yaxis/min-startontick-true/ + * -50 with startOnTick true by default + * @sample {highstock} stock/xaxis/min-max/ + * Set min and max on X axis + * @sample {highmaps} maps/axis/min-max/ + * Pre-zoomed to a specific area + * @apioption xAxis.min + */ + + /** + * The dash or dot style of the minor grid lines. For possible values, + * see [this demonstration](http://jsfiddle.net/gh/get/library/pure/ + * highcharts/highcharts/tree/master/samples/highcharts/plotoptions/ + * series-dashstyle-all/). + * + * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", + * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", + * "DashDot", "LongDashDot", "LongDashDotDot"] + * @type {String} + * @sample {highcharts} highcharts/yaxis/minorgridlinedashstyle/ + * Long dashes on minor grid lines + * @sample {highstock} stock/xaxis/minorgridlinedashstyle/ + * Long dashes on minor grid lines + * @default Solid + * @since 1.2 + * @apioption xAxis.minorGridLineDashStyle + */ + + /** + * Specific tick interval in axis units for the minor ticks. + * On a linear axis, if `"auto"`, the minor tick interval is + * calculated as a fifth of the tickInterval. If `null`, minor + * ticks are not shown. + * + * On logarithmic axes, the unit is the power of the value. For example, + * setting the minorTickInterval to 1 puts one tick on each of 0.1, + * 1, 10, 100 etc. Setting the minorTickInterval to 0.1 produces 9 + * ticks between 1 and 10, 10 and 100 etc. + * + * If user settings dictate minor ticks to become too dense, they don't + * make sense, and will be ignored to prevent performance problems. + * + * @type {Number|String} + * @sample {highcharts} highcharts/yaxis/minortickinterval-null/ + * Null by default + * @sample {highcharts} highcharts/yaxis/minortickinterval-5/ + * 5 units + * @sample {highcharts} highcharts/yaxis/minortickinterval-log-auto/ + * "auto" + * @sample {highcharts} highcharts/yaxis/minortickinterval-log/ + * 0.1 + * @sample {highstock} stock/demo/basic-line/ + * Null by default + * @sample {highstock} stock/xaxis/minortickinterval-auto/ + * "auto" + * @apioption xAxis.minorTickInterval + */ + + /** + * The pixel length of the minor tick marks. + * + * @sample {highcharts} highcharts/yaxis/minorticklength/ + * 10px on Y axis + * @sample {highstock} stock/xaxis/minorticks/ + * 10px on Y axis + */ + minorTickLength: 2, + + /** + * The position of the minor tick marks relative to the axis line. + * Can be one of `inside` and `outside`. + * + * @validvalue ["inside", "outside"] + * @sample {highcharts} highcharts/yaxis/minortickposition-outside/ + * Outside by default + * @sample {highcharts} highcharts/yaxis/minortickposition-inside/ + * Inside + * @sample {highstock} stock/xaxis/minorticks/ + * Inside + */ + minorTickPosition: 'outside', + + /** + * Enable or disable minor ticks. Unless + * [minorTickInterval](#xAxis.minorTickInterval) is set, the tick + * interval is calculated as a fifth of the `tickInterval`. + * + * On a logarithmic axis, minor ticks are laid out based on a best + * guess, attempting to enter approximately 5 minor ticks between + * each major tick. + * + * Prior to v6.0.0, ticks were unabled in auto layout by setting + * `minorTickInterval` to `"auto"`. + * + * @productdesc {highcharts} + * On axes using [categories](#xAxis.categories), minor ticks are not + * supported. + * + * @type {Boolean} + * @default false + * @since 6.0.0 + * @sample {highcharts} highcharts/yaxis/minorticks-true/ + * Enabled on linear Y axis + * @apioption xAxis.minorTicks + */ + + /** + * The pixel width of the minor tick mark. + * + * @type {Number} + * @sample {highcharts} highcharts/yaxis/minortickwidth/ + * 3px width + * @sample {highstock} stock/xaxis/minorticks/ + * 1px width + * @default 0 + * @apioption xAxis.minorTickWidth + */ + + /** + * Padding of the min value relative to the length of the axis. A + * padding of 0.05 will make a 100px axis 5px longer. This is useful + * when you don't want the lowest data value to appear on the edge + * of the plot area. When the axis' `min` option is set or a min extreme + * is set using `axis.setExtremes()`, the minPadding will be ignored. + * + * @sample {highcharts} highcharts/yaxis/minpadding/ + * Min padding of 0.2 + * @sample {highstock} stock/xaxis/minpadding-maxpadding/ + * Greater min- and maxPadding + * @sample {highmaps} maps/chart/plotbackgroundcolor-gradient/ + * Add some padding + * @default {highcharts} 0.01 + * @default {highstock|highmaps} 0 + * @since 1.2.0 + */ + minPadding: 0.01, + + /** + * The minimum range to display on this axis. The entire axis will not + * be allowed to span over a smaller interval than this. For example, + * for a datetime axis the main unit is milliseconds. If minRange is + * set to 3600000, you can't zoom in more than to one hour. + * + * The default minRange for the x axis is five times the smallest + * interval between any of the data points. + * + * On a logarithmic axis, the unit for the minimum range is the power. + * So a minRange of 1 means that the axis can be zoomed to 10-100, + * 100-1000, 1000-10000 etc. + * + * Note that the `minPadding`, `maxPadding`, `startOnTick` and + * `endOnTick` settings also affect how the extremes of the axis + * are computed. + * + * @type {Number} + * @sample {highcharts} highcharts/xaxis/minrange/ + * Minimum range of 5 + * @sample {highstock} stock/xaxis/minrange/ + * Max zoom of 6 months overrides user selections + * @sample {highmaps} maps/axis/minrange/ + * Minimum range of 1000 + * @apioption xAxis.minRange + */ + + /** + * The minimum tick interval allowed in axis values. For example on + * zooming in on an axis with daily data, this can be used to prevent + * the axis from showing hours. Defaults to the closest distance between + * two points on the axis. + * + * @type {Number} + * @since 2.3.0 + * @apioption xAxis.minTickInterval + */ + + /** + * The distance in pixels from the plot area to the axis line. + * A positive offset moves the axis with it's line, labels and ticks + * away from the plot area. This is typically used when two or more + * axes are displayed on the same side of the plot. With multiple + * axes the offset is dynamically adjusted to avoid collision, this + * can be overridden by setting offset explicitly. + * + * @type {Number} + * @sample {highcharts} highcharts/yaxis/offset/ + * Y axis offset of 70 + * @sample {highcharts} highcharts/yaxis/offset-centered/ + * Axes positioned in the center of the plot + * @sample {highstock} stock/xaxis/offset/ + * Y axis offset by 70 px + * @default 0 + * @apioption xAxis.offset + */ + + /** + * Whether to display the axis on the opposite side of the normal. The + * normal is on the left side for vertical axes and bottom for + * horizontal, so the opposite sides will be right and top respectively. + * This is typically used with dual or multiple axes. + * + * @type {Boolean} + * @sample {highcharts} highcharts/yaxis/opposite/ + * Secondary Y axis opposite + * @sample {highstock} stock/xaxis/opposite/ + * Y axis on left side + * @default false + * @apioption xAxis.opposite + */ + + /** + * Refers to the index in the [panes](#panes) array. Used for circular + * gauges and polar charts. When the option is not set then first pane + * will be used. + * + * @type {Number} + * @sample highcharts/demo/gauge-vu-meter + * Two gauges with different center + * @product highcharts + * @apioption xAxis.pane + */ + + /** + * Whether to reverse the axis so that the highest number is closest + * to the origin. If the chart is inverted, the x axis is reversed by + * default. + * + * @type {Boolean} + * @sample {highcharts} highcharts/yaxis/reversed/ + * Reversed Y axis + * @sample {highstock} stock/xaxis/reversed/ + * Reversed Y axis + * @default false + * @apioption xAxis.reversed + */ + // reversed: false, + + /** + * Whether to show the last tick label. Defaults to `true` on cartesian + * charts, and `false` on polar charts. + * + * @type {Boolean} + * @sample {highcharts} highcharts/xaxis/showlastlabel-true/ + * Set to true on X axis + * @sample {highstock} stock/xaxis/showfirstlabel/ + * Labels below plot lines on Y axis + * @default true + * @product highcharts highstock + * @apioption xAxis.showLastLabel + */ + + /** + * For datetime axes, this decides where to put the tick between weeks. + * 0 = Sunday, 1 = Monday. + * + * @sample {highcharts} highcharts/xaxis/startofweek-monday/ + * Monday by default + * @sample {highcharts} highcharts/xaxis/startofweek-sunday/ + * Sunday + * @sample {highstock} stock/xaxis/startofweek-1 + * Monday by default + * @sample {highstock} stock/xaxis/startofweek-0 + * Sunday + * @product highcharts highstock + */ + startOfWeek: 1, + + /** + * Whether to force the axis to start on a tick. Use this option with + * the `minPadding` option to control the axis start. + * + * @productdesc {highstock} + * In Highstock, `startOnTick` is always false when the navigator is + * enabled, to prevent jumpy scrolling. + * + * @sample {highcharts} highcharts/xaxis/startontick-false/ + * False by default + * @sample {highcharts} highcharts/xaxis/startontick-true/ + * True + * @sample {highstock} stock/xaxis/endontick/ + * False for Y axis + * @since 1.2.0 + */ + startOnTick: false, + + /** + * The pixel length of the main tick marks. + * + * @sample {highcharts} highcharts/xaxis/ticklength/ + * 20 px tick length on the X axis + * @sample {highstock} stock/xaxis/ticks/ + * Formatted ticks on X axis + */ + tickLength: 10, + + /** + * For categorized axes only. If `on` the tick mark is placed in the + * center of the category, if `between` the tick mark is placed between + * categories. The default is `between` if the `tickInterval` is 1, + * else `on`. + * + * @validvalue [null, "on", "between"] + * @sample {highcharts} highcharts/xaxis/tickmarkplacement-between/ + * "between" by default + * @sample {highcharts} highcharts/xaxis/tickmarkplacement-on/ + * "on" + * @product highcharts + */ + tickmarkPlacement: 'between', + + /** + * If tickInterval is `null` this option sets the approximate pixel + * interval of the tick marks. Not applicable to categorized axis. + * + * The tick interval is also influenced by the [minTickInterval](#xAxis. + * minTickInterval) option, that, by default prevents ticks from being + * denser than the data points. + * + * @see [tickInterval](#xAxis.tickInterval), + * [tickPositioner](#xAxis.tickPositioner), + * [tickPositions](#xAxis.tickPositions). + * @sample {highcharts} highcharts/xaxis/tickpixelinterval-50/ + * 50 px on X axis + * @sample {highstock} stock/xaxis/tickpixelinterval/ + * 200 px on X axis + */ + tickPixelInterval: 100, + + /** + * The position of the major tick marks relative to the axis line. + * Can be one of `inside` and `outside`. + * + * @validvalue ["inside", "outside"] + * @sample {highcharts} highcharts/xaxis/tickposition-outside/ + * "outside" by default + * @sample {highcharts} highcharts/xaxis/tickposition-inside/ + * "inside" + * @sample {highstock} stock/xaxis/ticks/ + * Formatted ticks on X axis + */ + tickPosition: 'outside', + + /** + * The axis title, showing next to the axis line. + * + * @productdesc {highmaps} + * In Highmaps, the axis is hidden by default, but adding an axis title + * is still possible. X axis and Y axis titles will appear at the bottom + * and left by default. + */ + title: { + + /** + * Alignment of the title relative to the axis values. Possible + * values are "low", "middle" or "high". + * + * @validvalue ["low", "middle", "high"] + * @sample {highcharts} highcharts/xaxis/title-align-low/ + * "low" + * @sample {highcharts} highcharts/xaxis/title-align-center/ + * "middle" by default + * @sample {highcharts} highcharts/xaxis/title-align-high/ + * "high" + * @sample {highcharts} highcharts/yaxis/title-offset/ + * Place the Y axis title on top of the axis + * @sample {highstock} stock/xaxis/title-align/ + * Aligned to "high" value + */ + align: 'middle', + + + + /** + * CSS styles for the title. If the title text is longer than the + * axis length, it will wrap to multiple lines by default. This can + * be customized by setting `textOverflow: 'ellipsis'`, by + * setting a specific `width` or by setting `wordSpace: 'nowrap'`. + * + * In styled mode, the stroke width is given in the + * `.highcharts-axis-title` class. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/xaxis/title-style/ + * Red + * @sample {highcharts} highcharts/css/axis/ + * Styled mode + * @default { "color": "#666666" } + */ + style: { + color: '#666666' + } + + }, + + /** + * The type of axis. Can be one of `linear`, `logarithmic`, `datetime` + * or `category`. In a datetime axis, the numbers are given in + * milliseconds, and tick marks are placed on appropriate values like + * full hours or days. In a category axis, the + * [point names](#series.line.data.name) of the chart's series are used + * for categories, if not a [categories](#xAxis.categories) array is + * defined. + * + * @validvalue ["linear", "logarithmic", "datetime", "category"] + * @sample {highcharts} highcharts/xaxis/type-linear/ + * Linear + * @sample {highcharts} highcharts/yaxis/type-log/ + * Logarithmic + * @sample {highcharts} highcharts/yaxis/type-log-minorgrid/ + * Logarithmic with minor grid lines + * @sample {highcharts} highcharts/xaxis/type-log-both/ + * Logarithmic on two axes + * @sample {highcharts} highcharts/yaxis/type-log-negative/ + * Logarithmic with extension to emulate negative values + * @product highcharts + */ + type: 'linear', + + + + /** + * Color of the minor, secondary grid lines. + * + * In styled mode, the stroke width is given in the + * `.highcharts-minor-grid-line` class. + * + * @type {Color} + * @sample {highcharts} highcharts/yaxis/minorgridlinecolor/ + * Bright grey lines from Y axis + * @sample {highcharts|highstock} highcharts/css/axis-grid/ + * Styled mode + * @sample {highstock} stock/xaxis/minorgridlinecolor/ + * Bright grey lines from Y axis + * @default #f2f2f2 + */ + minorGridLineColor: '#f2f2f2', + // minorGridLineDashStyle: null, + + /** + * Width of the minor, secondary grid lines. + * + * In styled mode, the stroke width is given in the + * `.highcharts-grid-line` class. + * + * @sample {highcharts} highcharts/yaxis/minorgridlinewidth/ + * 2px lines from Y axis + * @sample {highcharts|highstock} highcharts/css/axis-grid/ + * Styled mode + * @sample {highstock} stock/xaxis/minorgridlinewidth/ + * 2px lines from Y axis + */ + minorGridLineWidth: 1, + + /** + * Color for the minor tick marks. + * + * @type {Color} + * @sample {highcharts} highcharts/yaxis/minortickcolor/ + * Black tick marks on Y axis + * @sample {highstock} stock/xaxis/minorticks/ + * Black tick marks on Y axis + * @default #999999 + */ + minorTickColor: '#999999', + + /** + * The color of the line marking the axis itself. + * + * In styled mode, the line stroke is given in the + * `.highcharts-axis-line` or `.highcharts-xaxis-line` class. + * + * @productdesc {highmaps} + * In Highmaps, the axis line is hidden by default, because the axis is + * not visible by default. + * + * @type {Color} + * @sample {highcharts} highcharts/yaxis/linecolor/ + * A red line on Y axis + * @sample {highcharts|highstock} highcharts/css/axis/ + * Axes in styled mode + * @sample {highstock} stock/xaxis/linecolor/ + * A red line on X axis + * @default #ccd6eb + */ + lineColor: '#ccd6eb', + + /** + * The width of the line marking the axis itself. + * + * In styled mode, the stroke width is given in the + * `.highcharts-axis-line` or `.highcharts-xaxis-line` class. + * + * @sample {highcharts} highcharts/yaxis/linecolor/ + * A 1px line on Y axis + * @sample {highcharts|highstock} highcharts/css/axis/ + * Axes in styled mode + * @sample {highstock} stock/xaxis/linewidth/ + * A 2px line on X axis + * @default {highcharts|highstock} 1 + * @default {highmaps} 0 + */ + lineWidth: 1, + + /** + * Color of the grid lines extending the ticks across the plot area. + * + * In styled mode, the stroke is given in the `.highcharts-grid-line` + * class. + * + * @productdesc {highmaps} + * In Highmaps, the grid lines are hidden by default. + * + * @type {Color} + * @sample {highcharts} highcharts/yaxis/gridlinecolor/ + * Green lines + * @sample {highcharts|highstock} highcharts/css/axis-grid/ + * Styled mode + * @sample {highstock} stock/xaxis/gridlinecolor/ + * Green lines + * @default #e6e6e6 + */ + gridLineColor: '#e6e6e6', + // gridLineDashStyle: 'solid', + + + /** + * The width of the grid lines extending the ticks across the plot area. + * + * In styled mode, the stroke width is given in the + * `.highcharts-grid-line` class. + * + * @type {Number} + * @sample {highcharts} highcharts/yaxis/gridlinewidth/ + * 2px lines + * @sample {highcharts|highstock} highcharts/css/axis-grid/ + * Styled mode + * @sample {highstock} stock/xaxis/gridlinewidth/ + * 2px lines + * @default 0 + * @apioption xAxis.gridLineWidth + */ + // gridLineWidth: 0, + + /** + * Color for the main tick marks. + * + * In styled mode, the stroke is given in the `.highcharts-tick` + * class. + * + * @type {Color} + * @sample {highcharts} highcharts/xaxis/tickcolor/ + * Red ticks on X axis + * @sample {highcharts|highstock} highcharts/css/axis-grid/ + * Styled mode + * @sample {highstock} stock/xaxis/ticks/ + * Formatted ticks on X axis + * @default #ccd6eb + */ + tickColor: '#ccd6eb' + // tickWidth: 1 + + }, + + /** + * The Y axis or value axis. Normally this is the vertical axis, + * though if the chart is inverted this is the horizontal axis. + * In case of multiple axes, the yAxis node is an array of + * configuration objects. + * + * See [the Axis object](#Axis) for programmatic access to the axis. + * + * @extends xAxis + * @excluding ordinal,overscroll + * @optionparent yAxis + */ + defaultYAxisOptions: { + /** + * @productdesc {highstock} + * In Highstock, `endOnTick` is always false when the navigator is + * enabled, to prevent jumpy scrolling. + */ + endOnTick: true, + + /** + * @productdesc {highstock} + * In Highstock 1.x, the Y axis was placed on the left side by default. + * + * @sample {highcharts} highcharts/yaxis/opposite/ + * Secondary Y axis opposite + * @sample {highstock} stock/xaxis/opposite/ + * Y axis on left side + * @default {highstock} true + * @default {highcharts} false + * @product highstock highcharts + * @apioption yAxis.opposite + */ + + /** + * @see [tickInterval](#xAxis.tickInterval), + * [tickPositioner](#xAxis.tickPositioner), + * [tickPositions](#xAxis.tickPositions). + */ + tickPixelInterval: 72, + + showLastLabel: true, + + /** + * @extends xAxis.labels + */ + labels: { + /** + * What part of the string the given position is anchored to. Can + * be one of `"left"`, `"center"` or `"right"`. The exact position + * also depends on the `labels.x` setting. + * + * Angular gauges and solid gauges defaults to `center`. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/yaxis/labels-align-left/ + * Left + * @default {highcharts|highmaps} right + * @default {highstock} left + * @apioption yAxis.labels.align + */ + + /** + * The x position offset of the label relative to the tick position + * on the axis. Defaults to -15 for left axis, 15 for right axis. + * + * @sample {highcharts} highcharts/xaxis/labels-x/ + * Y axis labels placed on grid lines + */ + x: -8 + }, + + /** + * @productdesc {highmaps} + * In Highmaps, the axis line is hidden by default, because the axis is + * not visible by default. + * + * @apioption yAxis.lineColor + */ + + /** + * @sample {highcharts} highcharts/yaxis/min-startontick-false/ + * -50 with startOnTick to false + * @sample {highcharts} highcharts/yaxis/min-startontick-true/ + * -50 with startOnTick true by default + * @sample {highstock} stock/yaxis/min-max/ + * Fixed min and max on Y axis + * @sample {highmaps} maps/axis/min-max/ + * Pre-zoomed to a specific area + * @apioption yAxis.min + */ + + /** + * @sample {highcharts} highcharts/yaxis/max-200/ + * Y axis max of 200 + * @sample {highcharts} highcharts/yaxis/max-logarithmic/ + * Y axis max on logarithmic axis + * @sample {highstock} stock/yaxis/min-max/ + * Fixed min and max on Y axis + * @sample {highmaps} maps/axis/min-max/ + * Pre-zoomed to a specific area + * @apioption yAxis.max + */ + + /** + * Padding of the max value relative to the length of the axis. A + * padding of 0.05 will make a 100px axis 5px longer. This is useful + * when you don't want the highest data value to appear on the edge + * of the plot area. When the axis' `max` option is set or a max extreme + * is set using `axis.setExtremes()`, the maxPadding will be ignored. + * + * @sample {highcharts} highcharts/yaxis/maxpadding-02/ + * Max padding of 0.2 + * @sample {highstock} stock/xaxis/minpadding-maxpadding/ + * Greater min- and maxPadding + * @since 1.2.0 + * @product highcharts highstock + */ + maxPadding: 0.05, + + /** + * Padding of the min value relative to the length of the axis. A + * padding of 0.05 will make a 100px axis 5px longer. This is useful + * when you don't want the lowest data value to appear on the edge + * of the plot area. When the axis' `min` option is set or a max extreme + * is set using `axis.setExtremes()`, the maxPadding will be ignored. + * + * @sample {highcharts} highcharts/yaxis/minpadding/ + * Min padding of 0.2 + * @sample {highstock} stock/xaxis/minpadding-maxpadding/ + * Greater min- and maxPadding + * @since 1.2.0 + * @product highcharts highstock + */ + minPadding: 0.05, + + /** + * Whether to force the axis to start on a tick. Use this option with + * the `maxPadding` option to control the axis start. + * + * @sample {highcharts} highcharts/xaxis/startontick-false/ + * False by default + * @sample {highcharts} highcharts/xaxis/startontick-true/ + * True + * @sample {highstock} stock/xaxis/endontick/ + * False for Y axis + * @since 1.2.0 + * @product highcharts highstock + */ + startOnTick: true, + + /** + * @extends xAxis.title + */ + title: { + + /** + * The rotation of the text in degrees. 0 is horizontal, 270 is + * vertical reading from bottom to top. + * + * @sample {highcharts} highcharts/yaxis/title-offset/ + * Horizontal + */ + rotation: 270, + + /** + * The actual text of the axis title. Horizontal texts can contain + * HTML, but rotated texts are painted using vector techniques and + * must be clean text. The Y axis title is disabled by setting the + * `text` option to `null`. + * + * @sample {highcharts} highcharts/xaxis/title-text/ + * Custom HTML + * @default {highcharts} Values + * @default {highstock} null + * @product highcharts highstock + */ + text: 'Values' + }, + + /** + * The stack labels show the total value for each bar in a stacked + * column or bar chart. The label will be placed on top of positive + * columns and below negative columns. In case of an inverted column + * chart or a bar chart the label is placed to the right of positive + * bars and to the left of negative bars. + * + * @product highcharts + */ + stackLabels: { + + /** + * Allow the stack labels to overlap. + * + * @sample {highcharts} + * highcharts/yaxis/stacklabels-allowoverlap-false/ + * Default false + * @since 5.0.13 + * @product highcharts + */ + allowOverlap: false, + + /** + * Enable or disable the stack total labels. + * + * @sample {highcharts} highcharts/yaxis/stacklabels-enabled/ + * Enabled stack total labels + * @since 2.1.5 + * @product highcharts + */ + enabled: false, + + /** + * Callback JavaScript function to format the label. The value is + * given by `this.total`. + * + * @default function() { return this.total; } + * + * @type {Function} + * @sample {highcharts} highcharts/yaxis/stacklabels-formatter/ + * Added units to stack total value + * @since 2.1.5 + * @product highcharts + */ + formatter: function () { + return H.numberFormat(this.total, -1); + }, + + + /** + * CSS styles for the label. + * + * In styled mode, the styles are set in the + * `.highcharts-stack-label` class. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/yaxis/stacklabels-style/ + * Red stack total labels + * @since 2.1.5 + * @product highcharts + */ + style: { + fontSize: '11px', + fontWeight: 'bold', + color: '#000000', + textOutline: '1px contrast' + } + + }, + + gridLineWidth: 1, + lineWidth: 0 + // tickWidth: 0 + + }, + + /** + * These options extend the defaultOptions for left axes. + * + * @private + * @type {Object} + */ + defaultLeftAxisOptions: { + labels: { + x: -15 + }, + title: { + rotation: 270 + } + }, + + /** + * These options extend the defaultOptions for right axes. + * + * @private + * @type {Object} + */ + defaultRightAxisOptions: { + labels: { + x: 15 + }, + title: { + rotation: 90 + } + }, + + /** + * These options extend the defaultOptions for bottom axes. + * + * @private + * @type {Object} + */ + defaultBottomAxisOptions: { + labels: { + autoRotation: [-45], + x: 0 + // overflow: undefined, + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + /** + * These options extend the defaultOptions for top axes. + * + * @private + * @type {Object} + */ + defaultTopAxisOptions: { + labels: { + autoRotation: [-45], + x: 0 + // overflow: undefined + // staggerLines: null + }, + title: { + rotation: 0 + } + }, + + /** + * Overrideable function to initialize the axis. + * + * @see {@link Axis} + */ + init: function (chart, userOptions) { + + + var isXAxis = userOptions.isX, + axis = this; + + + /** + * The Chart that the axis belongs to. + * + * @name chart + * @memberOf Axis + * @type {Chart} + */ + axis.chart = chart; + + /** + * Whether the axis is horizontal. + * + * @name horiz + * @memberOf Axis + * @type {Boolean} + */ + axis.horiz = chart.inverted && !axis.isZAxis ? !isXAxis : isXAxis; + + // Flag, isXAxis + axis.isXAxis = isXAxis; + + /** + * The collection where the axis belongs, for example `xAxis`, `yAxis` + * or `colorAxis`. Corresponds to properties on Chart, for example + * {@link Chart.xAxis}. + * + * @name coll + * @memberOf Axis + * @type {String} + */ + axis.coll = axis.coll || (isXAxis ? 'xAxis' : 'yAxis'); + + + axis.opposite = userOptions.opposite; // needed in setOptions + + /** + * The side on which the axis is rendered. 0 is top, 1 is right, 2 is + * bottom and 3 is left. + * + * @name side + * @memberOf Axis + * @type {Number} + */ + axis.side = userOptions.side || (axis.horiz ? + (axis.opposite ? 0 : 2) : // top : bottom + (axis.opposite ? 1 : 3)); // right : left + + axis.setOptions(userOptions); + + + var options = this.options, + type = options.type, + isDatetimeAxis = type === 'datetime'; + + axis.labelFormatter = options.labels.formatter || + axis.defaultLabelFormatter; // can be overwritten by dynamic format + + + // Flag, stagger lines or not + axis.userOptions = userOptions; + + axis.minPixelPadding = 0; + + + /** + * Whether the axis is reversed. Based on the `axis.reversed`, + * option, but inverted charts have reversed xAxis by default. + * + * @name reversed + * @memberOf Axis + * @type {Boolean} + */ + axis.reversed = options.reversed; + axis.visible = options.visible !== false; + axis.zoomEnabled = options.zoomEnabled !== false; + + // Initial categories + axis.hasNames = type === 'category' || options.categories === true; + axis.categories = options.categories || axis.hasNames; + axis.names = axis.names || []; // Preserve on update (#3830) + + // Placeholder for plotlines and plotbands groups + axis.plotLinesAndBandsGroups = {}; + + // Shorthand types + axis.isLog = type === 'logarithmic'; + axis.isDatetimeAxis = isDatetimeAxis; + axis.positiveValuesOnly = axis.isLog && !axis.allowNegativeLog; + + // Flag, if axis is linked to another axis + axis.isLinked = defined(options.linkedTo); + + // Major ticks + axis.ticks = {}; + axis.labelEdge = []; + // Minor ticks + axis.minorTicks = {}; + + // List of plotLines/Bands + axis.plotLinesAndBands = []; + + // Alternate bands + axis.alternateBands = {}; + + // Axis metrics + axis.len = 0; + axis.minRange = axis.userMinRange = options.minRange || options.maxZoom; + axis.range = options.range; + axis.offset = options.offset || 0; + + + // Dictionary for stacks + axis.stacks = {}; + axis.oldStacks = {}; + axis.stacksTouched = 0; + + + /** + * The maximum value of the axis. In a logarithmic axis, this is the + * logarithm of the real value, and the real value can be obtained from + * {@link Axis#getExtremes}. + * + * @name max + * @memberOf Axis + * @type {Number} + */ + axis.max = null; + /** + * The minimum value of the axis. In a logarithmic axis, this is the + * logarithm of the real value, and the real value can be obtained from + * {@link Axis#getExtremes}. + * + * @name min + * @memberOf Axis + * @type {Number} + */ + axis.min = null; + + + /** + * The processed crosshair options. + * + * @name crosshair + * @memberOf Axis + * @type {AxisCrosshairOptions} + */ + axis.crosshair = pick( + options.crosshair, + splat(chart.options.tooltip.crosshairs)[isXAxis ? 0 : 1], + false + ); + + var events = axis.options.events; + + // Register. Don't add it again on Axis.update(). + if (inArray(axis, chart.axes) === -1) { // + if (isXAxis) { // #2713 + chart.axes.splice(chart.xAxis.length, 0, axis); + } else { + chart.axes.push(axis); + } + + chart[axis.coll].push(axis); + } + + /** + * All series associated to the axis. + * + * @name series + * @memberOf Axis + * @type {Array.} + */ + axis.series = axis.series || []; // populated by Series + + // Reversed axis + if ( + chart.inverted && + !axis.isZAxis && + isXAxis && + axis.reversed === undefined + ) { + axis.reversed = true; + } + + // register event listeners + objectEach(events, function (event, eventType) { + addEvent(axis, eventType, event); + }); + + // extend logarithmic axis + axis.lin2log = options.linearToLogConverter || axis.lin2log; + if (axis.isLog) { + axis.val2lin = axis.log2lin; + axis.lin2val = axis.lin2log; + } + }, + + /** + * Merge and set options. + * + * @private + */ + setOptions: function (userOptions) { + this.options = merge( + this.defaultOptions, + this.coll === 'yAxis' && this.defaultYAxisOptions, + [ + this.defaultTopAxisOptions, + this.defaultRightAxisOptions, + this.defaultBottomAxisOptions, + this.defaultLeftAxisOptions + ][this.side], + merge( + defaultOptions[this.coll], // if set in setOptions (#1053) + userOptions + ) + ); + }, + + /** + * The default label formatter. The context is a special config object for + * the label. In apps, use the {@link + * https://api.highcharts.com/highcharts/xAxis.labels.formatter| + * labels.formatter} instead except when a modification is needed. + * + * @private + */ + defaultLabelFormatter: function () { + var axis = this.axis, + value = this.value, + time = axis.chart.time, + categories = axis.categories, + dateTimeLabelFormat = this.dateTimeLabelFormat, + lang = defaultOptions.lang, + numericSymbols = lang.numericSymbols, + numSymMagnitude = lang.numericSymbolMagnitude || 1000, + i = numericSymbols && numericSymbols.length, + multi, + ret, + formatOption = axis.options.labels.format, + + // make sure the same symbol is added for all labels on a linear + // axis + numericSymbolDetector = axis.isLog ? + Math.abs(value) : + axis.tickInterval; + + if (formatOption) { + ret = format(formatOption, this, time); + + } else if (categories) { + ret = value; + + } else if (dateTimeLabelFormat) { // datetime axis + ret = time.dateFormat(dateTimeLabelFormat, value); + + } else if (i && numericSymbolDetector >= 1000) { + // Decide whether we should add a numeric symbol like k (thousands) + // or M (millions). If we are to enable this in tooltip or other + // places as well, we can move this logic to the numberFormatter and + // enable it by a parameter. + while (i-- && ret === undefined) { + multi = Math.pow(numSymMagnitude, i + 1); + if ( + // Only accept a numeric symbol when the distance is more + // than a full unit. So for example if the symbol is k, we + // don't accept numbers like 0.5k. + numericSymbolDetector >= multi && + // Accept one decimal before the symbol. Accepts 0.5k but + // not 0.25k. How does this work with the previous? + (value * 10) % multi === 0 && + numericSymbols[i] !== null && + value !== 0 + ) { // #5480 + ret = H.numberFormat(value / multi, -1) + numericSymbols[i]; + } + } + } + + if (ret === undefined) { + if (Math.abs(value) >= 10000) { // add thousands separators + ret = H.numberFormat(value, -1); + } else { // small numbers + ret = H.numberFormat(value, -1, undefined, ''); // #2466 + } + } + + return ret; + }, + + /** + * Get the minimum and maximum for the series of each axis. The function + * analyzes the axis series and updates `this.dataMin` and `this.dataMax`. + * + * @private + */ + getSeriesExtremes: function () { + var axis = this, + chart = axis.chart; + axis.hasVisibleSeries = false; + + // Reset properties in case we're redrawing (#3353) + axis.dataMin = axis.dataMax = axis.threshold = null; + axis.softThreshold = !axis.isXAxis; + + if (axis.buildStacks) { + axis.buildStacks(); + } + + // loop through this axis' series + each(axis.series, function (series) { + + if (series.visible || !chart.options.chart.ignoreHiddenSeries) { + + var seriesOptions = series.options, + xData, + threshold = seriesOptions.threshold, + seriesDataMin, + seriesDataMax; + + axis.hasVisibleSeries = true; + + // Validate threshold in logarithmic axes + if (axis.positiveValuesOnly && threshold <= 0) { + threshold = null; + } + + // Get dataMin and dataMax for X axes + if (axis.isXAxis) { + xData = series.xData; + if (xData.length) { + // If xData contains values which is not numbers, then + // filter them out. To prevent performance hit, we only + // do this after we have already found seriesDataMin + // because in most cases all data is valid. #5234. + seriesDataMin = arrayMin(xData); + seriesDataMax = arrayMax(xData); + + if ( + !isNumber(seriesDataMin) && + !(seriesDataMin instanceof Date) // #5010 + ) { + xData = grep(xData, isNumber); + // Do it again with valid data + seriesDataMin = arrayMin(xData); + seriesDataMax = arrayMax(xData); + } + + if (xData.length) { + axis.dataMin = Math.min( + pick(axis.dataMin, xData[0], seriesDataMin), + seriesDataMin + ); + axis.dataMax = Math.max( + pick(axis.dataMax, xData[0], seriesDataMax), + seriesDataMax + ); + } + } + + // Get dataMin and dataMax for Y axes, as well as handle + // stacking and processed data + } else { + + // Get this particular series extremes + series.getExtremes(); + seriesDataMax = series.dataMax; + seriesDataMin = series.dataMin; + + // Get the dataMin and dataMax so far. If percentage is + // used, the min and max are always 0 and 100. If + // seriesDataMin and seriesDataMax is null, then series + // doesn't have active y data, we continue with nulls + if (defined(seriesDataMin) && defined(seriesDataMax)) { + axis.dataMin = Math.min( + pick(axis.dataMin, seriesDataMin), + seriesDataMin + ); + axis.dataMax = Math.max( + pick(axis.dataMax, seriesDataMax), + seriesDataMax + ); + } + + // Adjust to threshold + if (defined(threshold)) { + axis.threshold = threshold; + } + // If any series has a hard threshold, it takes precedence + if ( + !seriesOptions.softThreshold || + axis.positiveValuesOnly + ) { + axis.softThreshold = false; + } + } + } + }); + }, + + /** + * Translate from axis value to pixel position on the chart, or back. Use + * the `toPixels` and `toValue` functions in applications. + * + * @private + */ + translate: function ( + val, + backwards, + cvsCoord, + old, + handleLog, + pointPlacement + ) { + var axis = this.linkedParent || this, // #1417 + sign = 1, + cvsOffset = 0, + localA = old ? axis.oldTransA : axis.transA, + localMin = old ? axis.oldMin : axis.min, + returnValue, + minPixelPadding = axis.minPixelPadding, + doPostTranslate = ( + axis.isOrdinal || + axis.isBroken || + (axis.isLog && handleLog) + ) && axis.lin2val; + + if (!localA) { + localA = axis.transA; + } + + // In vertical axes, the canvas coordinates start from 0 at the top like + // in SVG. + if (cvsCoord) { + sign *= -1; // canvas coordinates inverts the value + cvsOffset = axis.len; + } + + // Handle reversed axis + if (axis.reversed) { + sign *= -1; + cvsOffset -= sign * (axis.sector || axis.len); + } + + // From pixels to value + if (backwards) { // reverse translation + + val = val * sign + cvsOffset; + val -= minPixelPadding; + returnValue = val / localA + localMin; // from chart pixel to value + if (doPostTranslate) { // log and ordinal axes + returnValue = axis.lin2val(returnValue); + } + + // From value to pixels + } else { + if (doPostTranslate) { // log and ordinal axes + val = axis.val2lin(val); + } + returnValue = isNumber(localMin) ? + ( + sign * (val - localMin) * localA + + cvsOffset + + (sign * minPixelPadding) + + (isNumber(pointPlacement) ? localA * pointPlacement : 0) + ) : + undefined; + } + + return returnValue; + }, + + /** + * Translate a value in terms of axis units into pixels within the chart. + * + * @param {Number} value + * A value in terms of axis units. + * @param {Boolean} paneCoordinates + * Whether to return the pixel coordinate relative to the chart or + * just the axis/pane itself. + * @return {Number} Pixel position of the value on the chart or axis. + */ + toPixels: function (value, paneCoordinates) { + return this.translate(value, false, !this.horiz, null, true) + + (paneCoordinates ? 0 : this.pos); + }, + + /** + * Translate a pixel position along the axis to a value in terms of axis + * units. + * @param {Number} pixel + * The pixel value coordinate. + * @param {Boolean} paneCoordiantes + * Whether the input pixel is relative to the chart or just the + * axis/pane itself. + * @return {Number} The axis value. + */ + toValue: function (pixel, paneCoordinates) { + return this.translate( + pixel - (paneCoordinates ? 0 : this.pos), + true, + !this.horiz, + null, + true + ); + }, + + /** + * Create the path for a plot line that goes from the given value on + * this axis, across the plot to the opposite side. Also used internally for + * grid lines and crosshairs. + * + * @param {Number} value + * Axis value. + * @param {Number} [lineWidth=1] + * Used for calculation crisp line coordinates. + * @param {Boolean} [old=false] + * Use old coordinates (for resizing and rescaling). + * @param {Boolean} [force=false] + * If `false`, the function will return null when it falls outside + * the axis bounds. + * @param {Number} [translatedValue] + * If given, return the plot line path of a pixel position on the + * axis. + * + * @return {Array.} + * The SVG path definition for the plot line. + */ + getPlotLinePath: function (value, lineWidth, old, force, translatedValue) { + var axis = this, + chart = axis.chart, + axisLeft = axis.left, + axisTop = axis.top, + x1, + y1, + x2, + y2, + cHeight = (old && chart.oldChartHeight) || chart.chartHeight, + cWidth = (old && chart.oldChartWidth) || chart.chartWidth, + skip, + transB = axis.transB, + /** + * Check if x is between a and b. If not, either move to a/b + * or skip, depending on the force parameter. + */ + between = function (x, a, b) { + if (x < a || x > b) { + if (force) { + x = Math.min(Math.max(a, x), b); + } else { + skip = true; + } + } + return x; + }; + + translatedValue = pick( + translatedValue, + axis.translate(value, null, null, old) + ); + x1 = x2 = Math.round(translatedValue + transB); + y1 = y2 = Math.round(cHeight - translatedValue - transB); + if (!isNumber(translatedValue)) { // no min or max + skip = true; + force = false; // #7175, don't force it when path is invalid + } else if (axis.horiz) { + y1 = axisTop; + y2 = cHeight - axis.bottom; + x1 = x2 = between(x1, axisLeft, axisLeft + axis.width); + } else { + x1 = axisLeft; + x2 = cWidth - axis.right; + y1 = y2 = between(y1, axisTop, axisTop + axis.height); + } + return skip && !force ? + null : + chart.renderer.crispLine( + ['M', x1, y1, 'L', x2, y2], + lineWidth || 1 + ); + }, + + /** + * Internal function to et the tick positions of a linear axis to round + * values like whole tens or every five. + * + * @param {Number} tickInterval + * The normalized tick interval + * @param {Number} min + * Axis minimum. + * @param {Number} max + * Axis maximum. + * + * @return {Array.} + * An array of axis values where ticks should be placed. + */ + getLinearTickPositions: function (tickInterval, min, max) { + var pos, + lastPos, + roundedMin = + correctFloat(Math.floor(min / tickInterval) * tickInterval), + roundedMax = + correctFloat(Math.ceil(max / tickInterval) * tickInterval), + tickPositions = [], + precision; + + // When the precision is higher than what we filter out in + // correctFloat, skip it (#6183). + if (correctFloat(roundedMin + tickInterval) === roundedMin) { + precision = 20; + } + + // For single points, add a tick regardless of the relative position + // (#2662, #6274) + if (this.single) { + return [min]; + } + + // Populate the intermediate values + pos = roundedMin; + while (pos <= roundedMax) { + + // Place the tick on the rounded value + tickPositions.push(pos); + + // Always add the raw tickInterval, not the corrected one. + pos = correctFloat( + pos + tickInterval, + precision + ); + + // If the interval is not big enough in the current min - max range + // to actually increase the loop variable, we need to break out to + // prevent endless loop. Issue #619 + if (pos === lastPos) { + break; + } + + // Record the last value + lastPos = pos; + } + return tickPositions; + }, + + /** + * Resolve the new minorTicks/minorTickInterval options into the legacy + * loosely typed minorTickInterval option. + */ + getMinorTickInterval: function () { + var options = this.options; + + if (options.minorTicks === true) { + return pick(options.minorTickInterval, 'auto'); + } + if (options.minorTicks === false) { + return null; + } + return options.minorTickInterval; + }, + + /** + * Internal function to return the minor tick positions. For logarithmic + * axes, the same logic as for major ticks is reused. + * + * @return {Array.} + * An array of axis values where ticks should be placed. + */ + getMinorTickPositions: function () { + var axis = this, + options = axis.options, + tickPositions = axis.tickPositions, + minorTickInterval = axis.minorTickInterval, + minorTickPositions = [], + pos, + pointRangePadding = axis.pointRangePadding || 0, + min = axis.min - pointRangePadding, // #1498 + max = axis.max + pointRangePadding, // #1498 + range = max - min; + + // If minor ticks get too dense, they are hard to read, and may cause + // long running script. So we don't draw them. + if (range && range / minorTickInterval < axis.len / 3) { // #3875 + + if (axis.isLog) { + // For each interval in the major ticks, compute the minor ticks + // separately. + each(this.paddedTicks, function (pos, i, paddedTicks) { + if (i) { + minorTickPositions.push.apply( + minorTickPositions, + axis.getLogTickPositions( + minorTickInterval, + paddedTicks[i - 1], + paddedTicks[i], + true + ) + ); + } + }); + + } else if ( + axis.isDatetimeAxis && + this.getMinorTickInterval() === 'auto' + ) { // #1314 + minorTickPositions = minorTickPositions.concat( + axis.getTimeTicks( + axis.normalizeTimeTickInterval(minorTickInterval), + min, + max, + options.startOfWeek + ) + ); + } else { + for ( + pos = min + (tickPositions[0] - min) % minorTickInterval; + pos <= max; + pos += minorTickInterval + ) { + // Very, very, tight grid lines (#5771) + if (pos === minorTickPositions[0]) { + break; + } + minorTickPositions.push(pos); + } + } + } + + if (minorTickPositions.length !== 0) { + axis.trimTicks(minorTickPositions); // #3652 #3743 #1498 #6330 + } + return minorTickPositions; + }, + + /** + * Adjust the min and max for the minimum range. Keep in mind that the + * series data is not yet processed, so we don't have information on data + * cropping and grouping, or updated axis.pointRange or series.pointRange. + * The data can't be processed until we have finally established min and + * max. + * + * @private + */ + adjustForMinRange: function () { + var axis = this, + options = axis.options, + min = axis.min, + max = axis.max, + zoomOffset, + spaceAvailable, + closestDataRange, + i, + distance, + xData, + loopLength, + minArgs, + maxArgs, + minRange; + + // Set the automatic minimum range based on the closest point distance + if (axis.isXAxis && axis.minRange === undefined && !axis.isLog) { + + if (defined(options.min) || defined(options.max)) { + axis.minRange = null; // don't do this again + + } else { + + // Find the closest distance between raw data points, as opposed + // to closestPointRange that applies to processed points + // (cropped and grouped) + each(axis.series, function (series) { + xData = series.xData; + loopLength = series.xIncrement ? 1 : xData.length - 1; + for (i = loopLength; i > 0; i--) { + distance = xData[i] - xData[i - 1]; + if ( + closestDataRange === undefined || + distance < closestDataRange + ) { + closestDataRange = distance; + } + } + }); + axis.minRange = Math.min( + closestDataRange * 5, + axis.dataMax - axis.dataMin + ); + } + } + + // if minRange is exceeded, adjust + if (max - min < axis.minRange) { + + spaceAvailable = axis.dataMax - axis.dataMin >= axis.minRange; + minRange = axis.minRange; + zoomOffset = (minRange - max + min) / 2; + + // if min and max options have been set, don't go beyond it + minArgs = [min - zoomOffset, pick(options.min, min - zoomOffset)]; + // If space is available, stay within the data range + if (spaceAvailable) { + minArgs[2] = axis.isLog ? + axis.log2lin(axis.dataMin) : + axis.dataMin; + } + min = arrayMax(minArgs); + + maxArgs = [min + minRange, pick(options.max, min + minRange)]; + // If space is availabe, stay within the data range + if (spaceAvailable) { + maxArgs[2] = axis.isLog ? + axis.log2lin(axis.dataMax) : + axis.dataMax; + } + + max = arrayMin(maxArgs); + + // now if the max is adjusted, adjust the min back + if (max - min < minRange) { + minArgs[0] = max - minRange; + minArgs[1] = pick(options.min, max - minRange); + min = arrayMax(minArgs); + } + } + + // Record modified extremes + axis.min = min; + axis.max = max; + }, + + /** + * Find the closestPointRange across all series. + * + * @private + */ + getClosest: function () { + var ret; + + if (this.categories) { + ret = 1; + } else { + each(this.series, function (series) { + var seriesClosest = series.closestPointRange, + visible = series.visible || + !series.chart.options.chart.ignoreHiddenSeries; + + if ( + !series.noSharedTooltip && + defined(seriesClosest) && + visible + ) { + ret = defined(ret) ? + Math.min(ret, seriesClosest) : + seriesClosest; + } + }); + } + return ret; + }, + + /** + * When a point name is given and no x, search for the name in the existing + * categories, or if categories aren't provided, search names or create a + * new category (#2522). + * + * @private + * + * @param {Point} + * The point to inspect. + * + * @return {Number} + * The X value that the point is given. + */ + nameToX: function (point) { + var explicitCategories = isArray(this.categories), + names = explicitCategories ? this.categories : this.names, + nameX = point.options.x, + x; + + point.series.requireSorting = false; + + if (!defined(nameX)) { + nameX = this.options.uniqueNames === false ? + point.series.autoIncrement() : + inArray(point.name, names); + } + if (nameX === -1) { // The name is not found in currenct categories + if (!explicitCategories) { + x = names.length; + } + } else { + x = nameX; + } + + // Write the last point's name to the names array + if (x !== undefined) { + this.names[x] = point.name; + } + + return x; + }, + + /** + * When changes have been done to series data, update the axis.names. + * + * @private + */ + updateNames: function () { + var axis = this; + + if (this.names.length > 0) { + this.names.length = 0; + this.minRange = this.userMinRange; // Reset + each(this.series || [], function (series) { + + // Reset incrementer (#5928) + series.xIncrement = null; + + // When adding a series, points are not yet generated + if (!series.points || series.isDirtyData) { + series.processData(); + series.generatePoints(); + } + + each(series.points, function (point, i) { + var x; + if (point.options) { + x = axis.nameToX(point); + if (x !== undefined && x !== point.x) { + point.x = x; + series.xData[i] = x; + } + } + }); + }); + } + }, + + /** + * Update translation information. + * + * @private + */ + setAxisTranslation: function (saveOld) { + var axis = this, + range = axis.max - axis.min, + pointRange = axis.axisPointRange || 0, + closestPointRange, + minPointOffset = 0, + pointRangePadding = 0, + linkedParent = axis.linkedParent, + ordinalCorrection, + hasCategories = !!axis.categories, + transA = axis.transA, + isXAxis = axis.isXAxis; + + // Adjust translation for padding. Y axis with categories need to go + // through the same (#1784). + if (isXAxis || hasCategories || pointRange) { + + // Get the closest points + closestPointRange = axis.getClosest(); + + if (linkedParent) { + minPointOffset = linkedParent.minPointOffset; + pointRangePadding = linkedParent.pointRangePadding; + } else { + each(axis.series, function (series) { + var seriesPointRange = hasCategories ? + 1 : + ( + isXAxis ? + pick( + series.options.pointRange, + closestPointRange, + 0 + ) : + (axis.axisPointRange || 0) + ), // #2806 + pointPlacement = series.options.pointPlacement; + + pointRange = Math.max(pointRange, seriesPointRange); + + if (!axis.single) { + // minPointOffset is the value padding to the left of + // the axis in order to make room for points with a + // pointRange, typically columns. When the + // pointPlacement option is 'between' or 'on', this + // padding does not apply. + minPointOffset = Math.max( + minPointOffset, + isString(pointPlacement) ? 0 : seriesPointRange / 2 + ); + + // Determine the total padding needed to the length of + // the axis to make room for the pointRange. If the + // series' pointPlacement is 'on', no padding is added. + pointRangePadding = Math.max( + pointRangePadding, + pointPlacement === 'on' ? 0 : seriesPointRange + ); + } + }); + } + + // Record minPointOffset and pointRangePadding + ordinalCorrection = axis.ordinalSlope && closestPointRange ? + axis.ordinalSlope / closestPointRange : + 1; // #988, #1853 + axis.minPointOffset = minPointOffset = + minPointOffset * ordinalCorrection; + axis.pointRangePadding = + pointRangePadding = pointRangePadding * ordinalCorrection; + + // pointRange means the width reserved for each point, like in a + // column chart + axis.pointRange = Math.min(pointRange, range); + + // closestPointRange means the closest distance between points. In + // columns it is mostly equal to pointRange, but in lines pointRange + // is 0 while closestPointRange is some other value + if (isXAxis) { + axis.closestPointRange = closestPointRange; + } + } + + // Secondary values + if (saveOld) { + axis.oldTransA = transA; + } + axis.translationSlope = axis.transA = transA = + axis.options.staticScale || + axis.len / ((range + pointRangePadding) || 1); + + // Translation addend + axis.transB = axis.horiz ? axis.left : axis.bottom; + axis.minPixelPadding = transA * minPointOffset; + }, + + minFromRange: function () { + return this.max - this.range; + }, + + /** + * Set the tick positions to round values and optionally extend the extremes + * to the nearest tick. + * + * @private + */ + setTickInterval: function (secondPass) { + var axis = this, + chart = axis.chart, + options = axis.options, + isLog = axis.isLog, + log2lin = axis.log2lin, + isDatetimeAxis = axis.isDatetimeAxis, + isXAxis = axis.isXAxis, + isLinked = axis.isLinked, + maxPadding = options.maxPadding, + minPadding = options.minPadding, + length, + linkedParentExtremes, + tickIntervalOption = options.tickInterval, + minTickInterval, + tickPixelIntervalOption = options.tickPixelInterval, + categories = axis.categories, + threshold = axis.threshold, + softThreshold = axis.softThreshold, + thresholdMin, + thresholdMax, + hardMin, + hardMax; + + if (!isDatetimeAxis && !categories && !isLinked) { + this.getTickAmount(); + } + + // Min or max set either by zooming/setExtremes or initial options + hardMin = pick(axis.userMin, options.min); + hardMax = pick(axis.userMax, options.max); + + // Linked axis gets the extremes from the parent axis + if (isLinked) { + axis.linkedParent = chart[axis.coll][options.linkedTo]; + linkedParentExtremes = axis.linkedParent.getExtremes(); + axis.min = pick( + linkedParentExtremes.min, + linkedParentExtremes.dataMin + ); + axis.max = pick( + linkedParentExtremes.max, + linkedParentExtremes.dataMax + ); + if (options.type !== axis.linkedParent.options.type) { + H.error(11, 1); // Can't link axes of different type + } + + // Initial min and max from the extreme data values + } else { + + // Adjust to hard threshold + if (!softThreshold && defined(threshold)) { + if (axis.dataMin >= threshold) { + thresholdMin = threshold; + minPadding = 0; + } else if (axis.dataMax <= threshold) { + thresholdMax = threshold; + maxPadding = 0; + } + } + + axis.min = pick(hardMin, thresholdMin, axis.dataMin); + axis.max = pick(hardMax, thresholdMax, axis.dataMax); + + } + + if (isLog) { + if ( + axis.positiveValuesOnly && + !secondPass && + Math.min(axis.min, pick(axis.dataMin, axis.min)) <= 0 + ) { // #978 + H.error(10, 1); // Can't plot negative values on log axis + } + // The correctFloat cures #934, float errors on full tens. But it + // was too aggressive for #4360 because of conversion back to lin, + // therefore use precision 15. + axis.min = correctFloat(log2lin(axis.min), 15); + axis.max = correctFloat(log2lin(axis.max), 15); + } + + // handle zoomed range + if (axis.range && defined(axis.max)) { + axis.userMin = axis.min = hardMin = + Math.max(axis.dataMin, axis.minFromRange()); // #618, #6773 + axis.userMax = hardMax = axis.max; + + axis.range = null; // don't use it when running setExtremes + } + + // Hook for Highstock Scroller. Consider combining with beforePadding. + fireEvent(axis, 'foundExtremes'); + + // Hook for adjusting this.min and this.max. Used by bubble series. + if (axis.beforePadding) { + axis.beforePadding(); + } + + // adjust min and max for the minimum range + axis.adjustForMinRange(); + + // Pad the values to get clear of the chart's edges. To avoid + // tickInterval taking the padding into account, we do this after + // computing tick interval (#1337). + if ( + !categories && + !axis.axisPointRange && + !axis.usePercentage && + !isLinked && + defined(axis.min) && + defined(axis.max) + ) { + length = axis.max - axis.min; + if (length) { + if (!defined(hardMin) && minPadding) { + axis.min -= length * minPadding; + } + if (!defined(hardMax) && maxPadding) { + axis.max += length * maxPadding; + } + } + } + + // Handle options for floor, ceiling, softMin and softMax (#6359) + if (isNumber(options.softMin) && !isNumber(axis.userMin)) { + axis.min = Math.min(axis.min, options.softMin); + } + if (isNumber(options.softMax) && !isNumber(axis.userMax)) { + axis.max = Math.max(axis.max, options.softMax); + } + if (isNumber(options.floor)) { + axis.min = Math.max(axis.min, options.floor); + } + if (isNumber(options.ceiling)) { + axis.max = Math.min(axis.max, options.ceiling); + } + + + // When the threshold is soft, adjust the extreme value only if the data + // extreme and the padded extreme land on either side of the threshold. + // For example, a series of [0, 1, 2, 3] would make the yAxis add a tick + // for -1 because of the default minPadding and startOnTick options. + // This is prevented by the softThreshold option. + if (softThreshold && defined(axis.dataMin)) { + threshold = threshold || 0; + if ( + !defined(hardMin) && + axis.min < threshold && + axis.dataMin >= threshold + ) { + axis.min = threshold; + + } else if ( + !defined(hardMax) && + axis.max > threshold && + axis.dataMax <= threshold + ) { + axis.max = threshold; + } + } + + + // get tickInterval + if ( + axis.min === axis.max || + axis.min === undefined || + axis.max === undefined + ) { + axis.tickInterval = 1; + + } else if ( + isLinked && + !tickIntervalOption && + tickPixelIntervalOption === + axis.linkedParent.options.tickPixelInterval + ) { + axis.tickInterval = tickIntervalOption = + axis.linkedParent.tickInterval; + + } else { + axis.tickInterval = pick( + tickIntervalOption, + this.tickAmount ? + ((axis.max - axis.min) / Math.max(this.tickAmount - 1, 1)) : + undefined, + // For categoried axis, 1 is default, for linear axis use + // tickPix + categories ? + 1 : + // don't let it be more than the data range + (axis.max - axis.min) * tickPixelIntervalOption / + Math.max(axis.len, tickPixelIntervalOption) + ); + } + + /** + * Now we're finished detecting min and max, crop and group series data. + * This is in turn needed in order to find tick positions in + * ordinal axes. + */ + if (isXAxis && !secondPass) { + each(axis.series, function (series) { + series.processData( + axis.min !== axis.oldMin || axis.max !== axis.oldMax + ); + }); + } + + // set the translation factor used in translate function + axis.setAxisTranslation(true); + + // hook for ordinal axes and radial axes + if (axis.beforeSetTickPositions) { + axis.beforeSetTickPositions(); + } + + // hook for extensions, used in Highstock ordinal axes + if (axis.postProcessTickInterval) { + axis.tickInterval = axis.postProcessTickInterval(axis.tickInterval); + } + + // In column-like charts, don't cramp in more ticks than there are + // points (#1943, #4184) + if (axis.pointRange && !tickIntervalOption) { + axis.tickInterval = Math.max(axis.pointRange, axis.tickInterval); + } + + // Before normalizing the tick interval, handle minimum tick interval. + // This applies only if tickInterval is not defined. + minTickInterval = pick( + options.minTickInterval, + axis.isDatetimeAxis && axis.closestPointRange + ); + if (!tickIntervalOption && axis.tickInterval < minTickInterval) { + axis.tickInterval = minTickInterval; + } + + // for linear axes, get magnitude and normalize the interval + if (!isDatetimeAxis && !isLog && !tickIntervalOption) { + axis.tickInterval = normalizeTickInterval( + axis.tickInterval, + null, + getMagnitude(axis.tickInterval), + // If the tick interval is between 0.5 and 5 and the axis max is + // in the order of thousands, chances are we are dealing with + // years. Don't allow decimals. #3363. + pick( + options.allowDecimals, + !( + axis.tickInterval > 0.5 && + axis.tickInterval < 5 && + axis.max > 1000 && + axis.max < 9999 + ) + ), + !!this.tickAmount + ); + } + + // Prevent ticks from getting so close that we can't draw the labels + if (!this.tickAmount) { + axis.tickInterval = axis.unsquish(); + } + + this.setTickPositions(); + }, + + /** + * Now we have computed the normalized tickInterval, get the tick positions + */ + setTickPositions: function () { + + var options = this.options, + tickPositions, + tickPositionsOption = options.tickPositions, + minorTickIntervalOption = this.getMinorTickInterval(), + tickPositioner = options.tickPositioner, + startOnTick = options.startOnTick, + endOnTick = options.endOnTick; + + // Set the tickmarkOffset + this.tickmarkOffset = ( + this.categories && + options.tickmarkPlacement === 'between' && + this.tickInterval === 1 + ) ? 0.5 : 0; // #3202 + + + // get minorTickInterval + this.minorTickInterval = + minorTickIntervalOption === 'auto' && + this.tickInterval ? + this.tickInterval / 5 : + minorTickIntervalOption; + + // When there is only one point, or all points have the same value on + // this axis, then min and max are equal and tickPositions.length is 0 + // or 1. In this case, add some padding in order to center the point, + // but leave it with one tick. #1337. + this.single = + this.min === this.max && + defined(this.min) && + !this.tickAmount && + ( + // Data is on integer (#6563) + parseInt(this.min, 10) === this.min || + + // Between integers and decimals are not allowed (#6274) + options.allowDecimals !== false + ); + + // Find the tick positions. Work on a copy (#1565) + this.tickPositions = tickPositions = + tickPositionsOption && tickPositionsOption.slice(); + if (!tickPositions) { + + if (this.isDatetimeAxis) { + tickPositions = this.getTimeTicks( + this.normalizeTimeTickInterval( + this.tickInterval, + options.units + ), + this.min, + this.max, + options.startOfWeek, + this.ordinalPositions, + this.closestPointRange, + true + ); + } else if (this.isLog) { + tickPositions = this.getLogTickPositions( + this.tickInterval, + this.min, + this.max + ); + } else { + tickPositions = this.getLinearTickPositions( + this.tickInterval, + this.min, + this.max + ); + } + + // Too dense ticks, keep only the first and last (#4477) + if (tickPositions.length > this.len) { + tickPositions = [tickPositions[0], tickPositions.pop()]; + // Reduce doubled value (#7339) + if (tickPositions[0] === tickPositions[1]) { + tickPositions.length = 1; + } + } + + this.tickPositions = tickPositions; + + // Run the tick positioner callback, that allows modifying auto tick + // positions. + if (tickPositioner) { + tickPositioner = tickPositioner.apply( + this, + [this.min, this.max] + ); + if (tickPositioner) { + this.tickPositions = tickPositions = tickPositioner; + } + } + + } + + // Reset min/max or remove extremes based on start/end on tick + this.paddedTicks = tickPositions.slice(0); // Used for logarithmic minor + this.trimTicks(tickPositions, startOnTick, endOnTick); + if (!this.isLinked) { + + // Substract half a unit (#2619, #2846, #2515, #3390), + // but not in case of multiple ticks (#6897) + if (this.single && tickPositions.length < 2) { + this.min -= 0.5; + this.max += 0.5; + } + if (!tickPositionsOption && !tickPositioner) { + this.adjustTickAmount(); + } + } + }, + + /** + * Handle startOnTick and endOnTick by either adapting to padding min/max or + * rounded min/max. Also handle single data points. + * + * @private + */ + trimTicks: function (tickPositions, startOnTick, endOnTick) { + var roundedMin = tickPositions[0], + roundedMax = tickPositions[tickPositions.length - 1], + minPointOffset = this.minPointOffset || 0; + + if (!this.isLinked) { + if (startOnTick && roundedMin !== -Infinity) { // #6502 + this.min = roundedMin; + } else { + while (this.min - minPointOffset > tickPositions[0]) { + tickPositions.shift(); + } + } + + if (endOnTick) { + this.max = roundedMax; + } else { + while (this.max + minPointOffset < + tickPositions[tickPositions.length - 1]) { + tickPositions.pop(); + } + } + + // If no tick are left, set one tick in the middle (#3195) + if ( + tickPositions.length === 0 && + defined(roundedMin) && + !this.options.tickPositions + ) { + tickPositions.push((roundedMax + roundedMin) / 2); + } + } + }, + + /** + * Check if there are multiple axes in the same pane. + * + * @private + * @return {Boolean} + * True if there are other axes. + */ + alignToOthers: function () { + var others = {}, // Whether there is another axis to pair with this one + hasOther, + options = this.options; + + if ( + // Only if alignTicks is true + this.chart.options.chart.alignTicks !== false && + options.alignTicks !== false && + + // Don't try to align ticks on a log axis, they are not evenly + // spaced (#6021) + !this.isLog + ) { + each(this.chart[this.coll], function (axis) { + var otherOptions = axis.options, + horiz = axis.horiz, + key = [ + horiz ? otherOptions.left : otherOptions.top, + otherOptions.width, + otherOptions.height, + otherOptions.pane + ].join(','); + + + if (axis.series.length) { // #4442 + if (others[key]) { + hasOther = true; // #4201 + } else { + others[key] = 1; + } + } + }); + } + return hasOther; + }, + + /** + * Find the max ticks of either the x and y axis collection, and record it + * in `this.tickAmount`. + * + * @private + */ + getTickAmount: function () { + var options = this.options, + tickAmount = options.tickAmount, + tickPixelInterval = options.tickPixelInterval; + + if ( + !defined(options.tickInterval) && + this.len < tickPixelInterval && + !this.isRadial && + !this.isLog && + options.startOnTick && + options.endOnTick + ) { + tickAmount = 2; + } + + if (!tickAmount && this.alignToOthers()) { + // Add 1 because 4 tick intervals require 5 ticks (including first + // and last) + tickAmount = Math.ceil(this.len / tickPixelInterval) + 1; + } + + // For tick amounts of 2 and 3, compute five ticks and remove the + // intermediate ones. This prevents the axis from adding ticks that are + // too far away from the data extremes. + if (tickAmount < 4) { + this.finalTickAmt = tickAmount; + tickAmount = 5; + } + + this.tickAmount = tickAmount; + }, + + /** + * When using multiple axes, adjust the number of ticks to match the highest + * number of ticks in that group. + * + * @private + */ + adjustTickAmount: function () { + var tickInterval = this.tickInterval, + tickPositions = this.tickPositions, + tickAmount = this.tickAmount, + finalTickAmt = this.finalTickAmt, + currentTickAmount = tickPositions && tickPositions.length, + threshold = pick(this.threshold, this.softThreshold ? 0 : null), + i, + len; + + if (this.hasData()) { + if (currentTickAmount < tickAmount) { + while (tickPositions.length < tickAmount) { + + // Extend evenly for both sides unless we're on the + // threshold (#3965) + if ( + tickPositions.length % 2 || + this.min === threshold + ) { + // to the end + tickPositions.push(correctFloat( + tickPositions[tickPositions.length - 1] + + tickInterval + )); + } else { + // to the start + tickPositions.unshift(correctFloat( + tickPositions[0] - tickInterval + )); + } + } + this.transA *= (currentTickAmount - 1) / (tickAmount - 1); + this.min = tickPositions[0]; + this.max = tickPositions[tickPositions.length - 1]; + + // We have too many ticks, run second pass to try to reduce ticks + } else if (currentTickAmount > tickAmount) { + this.tickInterval *= 2; + this.setTickPositions(); + } + + // The finalTickAmt property is set in getTickAmount + if (defined(finalTickAmt)) { + i = len = tickPositions.length; + while (i--) { + if ( + // Remove every other tick + (finalTickAmt === 3 && i % 2 === 1) || + // Remove all but first and last + (finalTickAmt <= 2 && i > 0 && i < len - 1) + ) { + tickPositions.splice(i, 1); + } + } + this.finalTickAmt = undefined; + } + } + }, + + /** + * Set the scale based on data min and max, user set min and max or options. + * + * @private + */ + setScale: function () { + var axis = this, + isDirtyData, + isDirtyAxisLength; + + axis.oldMin = axis.min; + axis.oldMax = axis.max; + axis.oldAxisLength = axis.len; + + // set the new axisLength + axis.setAxisSize(); + isDirtyAxisLength = axis.len !== axis.oldAxisLength; + + // is there new data? + each(axis.series, function (series) { + if ( + series.isDirtyData || + series.isDirty || + // When x axis is dirty, we need new data extremes for y as well + series.xAxis.isDirty + ) { + isDirtyData = true; + } + }); + + // do we really need to go through all this? + if ( + isDirtyAxisLength || + isDirtyData || + axis.isLinked || + axis.forceRedraw || + axis.userMin !== axis.oldUserMin || + axis.userMax !== axis.oldUserMax || + axis.alignToOthers() + ) { + + if (axis.resetStacks) { + axis.resetStacks(); + } + + axis.forceRedraw = false; + + // get data extremes if needed + axis.getSeriesExtremes(); + + // get fixed positions based on tickInterval + axis.setTickInterval(); + + // record old values to decide whether a rescale is necessary later + // on (#540) + axis.oldUserMin = axis.userMin; + axis.oldUserMax = axis.userMax; + + // Mark as dirty if it is not already set to dirty and extremes have + // changed. #595. + if (!axis.isDirty) { + axis.isDirty = + isDirtyAxisLength || + axis.min !== axis.oldMin || + axis.max !== axis.oldMax; + } + } else if (axis.cleanStacks) { + axis.cleanStacks(); + } + }, + + /** + * Set the minimum and maximum of the axes after render time. If the + * `startOnTick` and `endOnTick` options are true, the minimum and maximum + * values are rounded off to the nearest tick. To prevent this, these + * options can be set to false before calling setExtremes. Also, setExtremes + * will not allow a range lower than the `minRange` option, which by default + * is the range of five points. + * + * @param {Number} [newMin] + * The new minimum value. + * @param {Number} [newMax] + * The new maximum value. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart or wait for an explicit call to + * {@link Highcharts.Chart#redraw} + * @param {AnimationOptions} [animation=true] + * Enable or modify animations. + * @param {Object} [eventArguments] + * Arguments to be accessed in event handler. + * + * @sample highcharts/members/axis-setextremes/ + * Set extremes from a button + * @sample highcharts/members/axis-setextremes-datetime/ + * Set extremes on a datetime axis + * @sample highcharts/members/axis-setextremes-off-ticks/ + * Set extremes off ticks + * @sample stock/members/axis-setextremes/ + * Set extremes in Highstock + * @sample maps/members/axis-setextremes/ + * Set extremes in Highmaps + */ + setExtremes: function (newMin, newMax, redraw, animation, eventArguments) { + var axis = this, + chart = axis.chart; + + redraw = pick(redraw, true); // defaults to true + + each(axis.series, function (serie) { + delete serie.kdTree; + }); + + // Extend the arguments with min and max + eventArguments = extend(eventArguments, { + min: newMin, + max: newMax + }); + + // Fire the event + fireEvent(axis, 'setExtremes', eventArguments, function () { + + axis.userMin = newMin; + axis.userMax = newMax; + axis.eventArgs = eventArguments; + + if (redraw) { + chart.redraw(animation); + } + }); + }, + + /** + * Overridable method for zooming chart. Pulled out in a separate method to + * allow overriding in stock charts. + * + * @private + */ + zoom: function (newMin, newMax) { + var dataMin = this.dataMin, + dataMax = this.dataMax, + options = this.options, + min = Math.min(dataMin, pick(options.min, dataMin)), + max = Math.max(dataMax, pick(options.max, dataMax)); + + if (newMin !== this.min || newMax !== this.max) { // #5790 + + // Prevent pinch zooming out of range. Check for defined is for + // #1946. #1734. + if (!this.allowZoomOutside) { + // #6014, sometimes newMax will be smaller than min (or newMin + // will be larger than max). + if (defined(dataMin)) { + if (newMin < min) { + newMin = min; + } + if (newMin > max) { + newMin = max; + } + } + if (defined(dataMax)) { + if (newMax < min) { + newMax = min; + } + if (newMax > max) { + newMax = max; + } + } + } + + // In full view, displaying the reset zoom button is not required + this.displayBtn = newMin !== undefined || newMax !== undefined; + + // Do it + this.setExtremes( + newMin, + newMax, + false, + undefined, + { trigger: 'zoom' } + ); + } + + return true; + }, + + /** + * Update the axis metrics. + * + * @private + */ + setAxisSize: function () { + var chart = this.chart, + options = this.options, + // [top, right, bottom, left] + offsets = options.offsets || [0, 0, 0, 0], + horiz = this.horiz, + + // Check for percentage based input values. Rounding fixes problems + // with column overflow and plot line filtering (#4898, #4899) + width = this.width = Math.round(H.relativeLength( + pick( + options.width, + chart.plotWidth - offsets[3] + offsets[1] + ), + chart.plotWidth + )), + height = this.height = Math.round(H.relativeLength( + pick( + options.height, + chart.plotHeight - offsets[0] + offsets[2] + ), + chart.plotHeight + )), + top = this.top = Math.round(H.relativeLength( + pick(options.top, chart.plotTop + offsets[0]), + chart.plotHeight, + chart.plotTop + )), + left = this.left = Math.round(H.relativeLength( + pick(options.left, chart.plotLeft + offsets[3]), + chart.plotWidth, + chart.plotLeft + )); + + // Expose basic values to use in Series object and navigator + this.bottom = chart.chartHeight - height - top; + this.right = chart.chartWidth - width - left; + + // Direction agnostic properties + this.len = Math.max(horiz ? width : height, 0); // Math.max fixes #905 + this.pos = horiz ? left : top; // distance from SVG origin + }, + + /** + * The returned object literal from the {@link Highcharts.Axis#getExtremes} + * function. + * + * @typedef {Object} Extremes + * @property {Number} dataMax + * The maximum value of the axis' associated series. + * @property {Number} dataMin + * The minimum value of the axis' associated series. + * @property {Number} max + * The maximum axis value, either automatic or set manually. If + * the `max` option is not set, `maxPadding` is 0 and `endOnTick` + * is false, this value will be the same as `dataMax`. + * @property {Number} min + * The minimum axis value, either automatic or set manually. If + * the `min` option is not set, `minPadding` is 0 and + * `startOnTick` is false, this value will be the same + * as `dataMin`. + */ + /** + * Get the current extremes for the axis. + * + * @returns {Extremes} + * An object containing extremes information. + * + * @sample highcharts/members/axis-getextremes/ + * Report extremes by click on a button + * @sample maps/members/axis-getextremes/ + * Get extremes in Highmaps + */ + getExtremes: function () { + var axis = this, + isLog = axis.isLog, + lin2log = axis.lin2log; + + return { + min: isLog ? correctFloat(lin2log(axis.min)) : axis.min, + max: isLog ? correctFloat(lin2log(axis.max)) : axis.max, + dataMin: axis.dataMin, + dataMax: axis.dataMax, + userMin: axis.userMin, + userMax: axis.userMax + }; + }, + + /** + * Get the zero plane either based on zero or on the min or max value. + * Used in bar and area plots. + * + * @param {Number} threshold + * The threshold in axis values. + * + * @return {Number} + * The translated threshold position in terms of pixels, and + * corrected to stay within the axis bounds. + */ + getThreshold: function (threshold) { + var axis = this, + isLog = axis.isLog, + lin2log = axis.lin2log, + realMin = isLog ? lin2log(axis.min) : axis.min, + realMax = isLog ? lin2log(axis.max) : axis.max; + + if (threshold === null) { + threshold = realMin; + } else if (realMin > threshold) { + threshold = realMin; + } else if (realMax < threshold) { + threshold = realMax; + } + + return axis.translate(threshold, 0, 1, 0, 1); + }, + + /** + * Compute auto alignment for the axis label based on which side the axis is + * on and the given rotation for the label. + * + * @param {Number} rotation + * The rotation in degrees as set by either the `rotation` or + * `autoRotation` options. + * @private + */ + autoLabelAlign: function (rotation) { + var ret, + angle = (pick(rotation, 0) - (this.side * 90) + 720) % 360; + + if (angle > 15 && angle < 165) { + ret = 'right'; + } else if (angle > 195 && angle < 345) { + ret = 'left'; + } else { + ret = 'center'; + } + return ret; + }, + + /** + * Get the tick length and width for the axis based on axis options. + * + * @private + * + * @param {String} prefix + * 'tick' or 'minorTick' + * @return {Array.} + * An array of tickLength and tickWidth + */ + tickSize: function (prefix) { + var options = this.options, + tickLength = options[prefix + 'Length'], + tickWidth = pick( + options[prefix + 'Width'], + prefix === 'tick' && this.isXAxis ? 1 : 0 // X axis default 1 + ); + + if (tickWidth && tickLength) { + // Negate the length + if (options[prefix + 'Position'] === 'inside') { + tickLength = -tickLength; + } + return [tickLength, tickWidth]; + } + + }, + + /** + * Return the size of the labels. + * + * @private + */ + labelMetrics: function () { + var index = this.tickPositions && this.tickPositions[0] || 0; + return this.chart.renderer.fontMetrics( + this.options.labels.style && this.options.labels.style.fontSize, + this.ticks[index] && this.ticks[index].label + ); + }, + + /** + * Prevent the ticks from getting so close we can't draw the labels. On a + * horizontal axis, this is handled by rotating the labels, removing ticks + * and adding ellipsis. On a vertical axis remove ticks and add ellipsis. + * + * @private + */ + unsquish: function () { + var labelOptions = this.options.labels, + horiz = this.horiz, + tickInterval = this.tickInterval, + newTickInterval = tickInterval, + slotSize = this.len / ( + ((this.categories ? 1 : 0) + this.max - this.min) / tickInterval + ), + rotation, + rotationOption = labelOptions.rotation, + labelMetrics = this.labelMetrics(), + step, + bestScore = Number.MAX_VALUE, + autoRotation, + // Return the multiple of tickInterval that is needed to avoid + // collision + getStep = function (spaceNeeded) { + var step = spaceNeeded / (slotSize || 1); + step = step > 1 ? Math.ceil(step) : 1; + return step * tickInterval; + }; + + if (horiz) { + autoRotation = !labelOptions.staggerLines && + !labelOptions.step && + ( // #3971 + defined(rotationOption) ? + [rotationOption] : + slotSize < pick(labelOptions.autoRotationLimit, 80) && + labelOptions.autoRotation + ); + + if (autoRotation) { + + // Loop over the given autoRotation options, and determine + // which gives the best score. The best score is that with + // the lowest number of steps and a rotation closest + // to horizontal. + each(autoRotation, function (rot) { + var score; + + if ( + rot === rotationOption || + (rot && rot >= -90 && rot <= 90) + ) { // #3891 + + step = getStep( + Math.abs(labelMetrics.h / Math.sin(deg2rad * rot)) + ); + + score = step + Math.abs(rot / 360); + + if (score < bestScore) { + bestScore = score; + rotation = rot; + newTickInterval = step; + } + } + }); + } + + } else if (!labelOptions.step) { // #4411 + newTickInterval = getStep(labelMetrics.h); + } + + this.autoRotation = autoRotation; + this.labelRotation = pick(rotation, rotationOption); + + return newTickInterval; + }, + + /** + * Get the general slot width for labels/categories on this axis. This may + * change between the pre-render (from Axis.getOffset) and the final tick + * rendering and placement. + * + * @private + * @return {Number} + * The pixel width allocated to each axis label. + */ + getSlotWidth: function () { + // #5086, #1580, #1931 + var chart = this.chart, + horiz = this.horiz, + labelOptions = this.options.labels, + slotCount = Math.max( + this.tickPositions.length - (this.categories ? 0 : 1), + 1 + ), + marginLeft = chart.margin[3]; + + return ( + horiz && + (labelOptions.step || 0) < 2 && + !labelOptions.rotation && // #4415 + ((this.staggerLines || 1) * this.len) / slotCount + ) || ( + !horiz && ( + // #7028 + ( + labelOptions.style && + parseInt(labelOptions.style.width, 10) + ) || + ( + marginLeft && + (marginLeft - chart.spacing[3]) + ) || + chart.chartWidth * 0.33 + ) + ); + + }, + + /** + * Render the axis labels and determine whether ellipsis or rotation need + * to be applied. + * + * @private + */ + renderUnsquish: function () { + var chart = this.chart, + renderer = chart.renderer, + tickPositions = this.tickPositions, + ticks = this.ticks, + labelOptions = this.options.labels, + horiz = this.horiz, + slotWidth = this.getSlotWidth(), + innerWidth = Math.max( + 1, + Math.round(slotWidth - 2 * (labelOptions.padding || 5)) + ), + attr = {}, + labelMetrics = this.labelMetrics(), + textOverflowOption = labelOptions.style && + labelOptions.style.textOverflow, + css, + maxLabelLength = 0, + label, + i, + pos; + + // Set rotation option unless it is "auto", like in gauges + if (!isString(labelOptions.rotation)) { + attr.rotation = labelOptions.rotation || 0; // #4443 + } + + // Get the longest label length + each(tickPositions, function (tick) { + tick = ticks[tick]; + if (tick && tick.labelLength > maxLabelLength) { + maxLabelLength = tick.labelLength; + } + }); + this.maxLabelLength = maxLabelLength; + + + // Handle auto rotation on horizontal axis + if (this.autoRotation) { + + // Apply rotation only if the label is too wide for the slot, and + // the label is wider than its height. + if ( + maxLabelLength > innerWidth && + maxLabelLength > labelMetrics.h + ) { + attr.rotation = this.labelRotation; + } else { + this.labelRotation = 0; + } + + // Handle word-wrap or ellipsis on vertical axis + } else if (slotWidth) { + // For word-wrap or ellipsis + css = { width: innerWidth + 'px' }; + + if (!textOverflowOption) { + css.textOverflow = 'clip'; + + // On vertical axis, only allow word wrap if there is room + // for more lines. + i = tickPositions.length; + while (!horiz && i--) { + pos = tickPositions[i]; + label = ticks[pos].label; + if (label) { + // Reset ellipsis in order to get the correct + // bounding box (#4070) + if ( + label.styles && + label.styles.textOverflow === 'ellipsis' + ) { + label.css({ textOverflow: 'clip' }); + + // Set the correct width in order to read + // the bounding box height (#4678, #5034) + } else if (ticks[pos].labelLength > slotWidth) { + label.css({ width: slotWidth + 'px' }); + } + + if ( + label.getBBox().height > ( + this.len / tickPositions.length - + (labelMetrics.h - labelMetrics.f) + ) + ) { + label.specCss = { textOverflow: 'ellipsis' }; + } + } + } + } + } + + + // Add ellipsis if the label length is significantly longer than ideal + if (attr.rotation) { + css = { + width: ( + maxLabelLength > chart.chartHeight * 0.5 ? + chart.chartHeight * 0.33 : + chart.chartHeight + ) + 'px' + }; + if (!textOverflowOption) { + css.textOverflow = 'ellipsis'; + } + } + + // Set the explicit or automatic label alignment + this.labelAlign = labelOptions.align || + this.autoLabelAlign(this.labelRotation); + if (this.labelAlign) { + attr.align = this.labelAlign; + } + + // Apply general and specific CSS + each(tickPositions, function (pos) { + var tick = ticks[pos], + label = tick && tick.label; + if (label) { + // This needs to go before the CSS in old IE (#4502) + label.attr(attr); + + if (css) { + label.css(merge(css, label.specCss)); + } + delete label.specCss; + tick.rotation = attr.rotation; + } + }); + + // Note: Why is this not part of getLabelPosition? + this.tickRotCorr = renderer.rotCorr( + labelMetrics.b, + this.labelRotation || 0, + this.side !== 0 + ); + }, + + /** + * Return true if the axis has associated data. + * + * @return {Boolean} + * True if the axis has associated visible series and those series + * have either valid data points or explicit `min` and `max` + * settings. + */ + hasData: function () { + return ( + this.hasVisibleSeries || + ( + defined(this.min) && + defined(this.max) && + this.tickPositions && + this.tickPositions.length > 0 + ) + ); + }, + + /** + * Adds the title defined in axis.options.title. + * @param {Boolean} display - whether or not to display the title + */ + addTitle: function (display) { + var axis = this, + renderer = axis.chart.renderer, + horiz = axis.horiz, + opposite = axis.opposite, + options = axis.options, + axisTitleOptions = options.title, + textAlign; + + if (!axis.axisTitle) { + textAlign = axisTitleOptions.textAlign; + if (!textAlign) { + textAlign = (horiz ? { + low: 'left', + middle: 'center', + high: 'right' + } : { + low: opposite ? 'right' : 'left', + middle: 'center', + high: opposite ? 'left' : 'right' + })[axisTitleOptions.align]; + } + axis.axisTitle = renderer.text( + axisTitleOptions.text, + 0, + 0, + axisTitleOptions.useHTML + ) + .attr({ + zIndex: 7, + rotation: axisTitleOptions.rotation || 0, + align: textAlign + }) + .addClass('highcharts-axis-title') + + .css(axisTitleOptions.style) + + .add(axis.axisGroup); + axis.axisTitle.isNew = true; + } + + // Max width defaults to the length of the axis + + if (!axisTitleOptions.style.width && !axis.isRadial) { + + axis.axisTitle.css({ + width: axis.len + }); + + } + + + + // hide or show the title depending on whether showEmpty is set + axis.axisTitle[display ? 'show' : 'hide'](true); + }, + + /** + * Generates a tick for initial positioning. + * + * @private + * @param {number} pos + * The tick position in axis values. + * @param {number} i + * The index of the tick in {@link Axis.tickPositions}. + */ + generateTick: function (pos) { + var ticks = this.ticks; + + if (!ticks[pos]) { + ticks[pos] = new Tick(this, pos); + } else { + ticks[pos].addLabel(); // update labels depending on tick interval + } + }, + + /** + * Render the tick labels to a preliminary position to get their sizes. + * + * @private + */ + getOffset: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + tickPositions = axis.tickPositions, + ticks = axis.ticks, + horiz = axis.horiz, + side = axis.side, + invertedSide = chart.inverted && + !axis.isZAxis ? [1, 0, 3, 2][side] : side, + hasData, + showAxis, + titleOffset = 0, + titleOffsetOption, + titleMargin = 0, + axisTitleOptions = options.title, + labelOptions = options.labels, + labelOffset = 0, // reset + labelOffsetPadded, + axisOffset = chart.axisOffset, + clipOffset = chart.clipOffset, + clip, + directionFactor = [-1, 1, 1, -1][side], + className = options.className, + axisParent = axis.axisParent, // Used in color axis + lineHeightCorrection, + tickSize = this.tickSize('tick'); + + // For reuse in Axis.render + hasData = axis.hasData(); + axis.showAxis = showAxis = hasData || pick(options.showEmpty, true); + + // Set/reset staggerLines + axis.staggerLines = axis.horiz && labelOptions.staggerLines; + + // Create the axisGroup and gridGroup elements on first iteration + if (!axis.axisGroup) { + axis.gridGroup = renderer.g('grid') + .attr({ zIndex: options.gridZIndex || 1 }) + .addClass( + 'highcharts-' + this.coll.toLowerCase() + '-grid ' + + (className || '') + ) + .add(axisParent); + axis.axisGroup = renderer.g('axis') + .attr({ zIndex: options.zIndex || 2 }) + .addClass( + 'highcharts-' + this.coll.toLowerCase() + ' ' + + (className || '') + ) + .add(axisParent); + axis.labelGroup = renderer.g('axis-labels') + .attr({ zIndex: labelOptions.zIndex || 7 }) + .addClass( + 'highcharts-' + axis.coll.toLowerCase() + '-labels ' + + (className || '') + ) + .add(axisParent); + } + + if (hasData || axis.isLinked) { + + // Generate ticks + each(tickPositions, function (pos, i) { + // i is not used here, but may be used in overrides + axis.generateTick(pos, i); + }); + + axis.renderUnsquish(); + + + // Left side must be align: right and right side must + // have align: left for labels + axis.reserveSpaceDefault = ( + side === 0 || + side === 2 || + { 1: 'left', 3: 'right' }[side] === axis.labelAlign + ); + if (pick( + labelOptions.reserveSpace, + axis.labelAlign === 'center' ? true : null, + axis.reserveSpaceDefault) + ) { + each(tickPositions, function (pos) { + // get the highest offset + labelOffset = Math.max( + ticks[pos].getLabelSize(), + labelOffset + ); + }); + } + + if (axis.staggerLines) { + labelOffset *= axis.staggerLines; + } + axis.labelOffset = labelOffset * (axis.opposite ? -1 : 1); + + } else { // doesn't have data + objectEach(ticks, function (tick, n) { + tick.destroy(); + delete ticks[n]; + }); + } + + if ( + axisTitleOptions && + axisTitleOptions.text && + axisTitleOptions.enabled !== false + ) { + axis.addTitle(showAxis); + + if (showAxis && axisTitleOptions.reserveSpace !== false) { + axis.titleOffset = titleOffset = + axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; + titleOffsetOption = axisTitleOptions.offset; + titleMargin = defined(titleOffsetOption) ? + 0 : + pick(axisTitleOptions.margin, horiz ? 5 : 10); + } + } + + // Render the axis line + axis.renderLine(); + + // handle automatic or user set offset + axis.offset = directionFactor * pick(options.offset, axisOffset[side]); + + axis.tickRotCorr = axis.tickRotCorr || { x: 0, y: 0 }; // polar + if (side === 0) { + lineHeightCorrection = -axis.labelMetrics().h; + } else if (side === 2) { + lineHeightCorrection = axis.tickRotCorr.y; + } else { + lineHeightCorrection = 0; + } + + // Find the padded label offset + labelOffsetPadded = Math.abs(labelOffset) + titleMargin; + if (labelOffset) { + labelOffsetPadded -= lineHeightCorrection; + labelOffsetPadded += directionFactor * ( + horiz ? + pick( + labelOptions.y, + axis.tickRotCorr.y + directionFactor * 8 + ) : + labelOptions.x + ); + } + + axis.axisTitleMargin = pick(titleOffsetOption, labelOffsetPadded); + + axisOffset[side] = Math.max( + axisOffset[side], + axis.axisTitleMargin + titleOffset + directionFactor * axis.offset, + labelOffsetPadded, // #3027 + hasData && tickPositions.length && tickSize ? + tickSize[0] + directionFactor * axis.offset : + 0 // #4866 + ); + + // Decide the clipping needed to keep the graph inside + // the plot area and axis lines + clip = options.offset ? + 0 : + Math.floor(axis.axisLine.strokeWidth() / 2) * 2; // #4308, #4371 + clipOffset[invertedSide] = Math.max(clipOffset[invertedSide], clip); + }, + + /** + * Internal function to get the path for the axis line. Extended for polar + * charts. + * + * @param {Number} lineWidth + * The line width in pixels. + * @return {Array} + * The SVG path definition in array form. + */ + getLinePath: function (lineWidth) { + var chart = this.chart, + opposite = this.opposite, + offset = this.offset, + horiz = this.horiz, + lineLeft = this.left + (opposite ? this.width : 0) + offset, + lineTop = chart.chartHeight - this.bottom - + (opposite ? this.height : 0) + offset; + + if (opposite) { + lineWidth *= -1; // crispify the other way - #1480, #1687 + } + + return chart.renderer + .crispLine([ + 'M', + horiz ? + this.left : + lineLeft, + horiz ? + lineTop : + this.top, + 'L', + horiz ? + chart.chartWidth - this.right : + lineLeft, + horiz ? + lineTop : + chart.chartHeight - this.bottom + ], lineWidth); + }, + + /** + * Render the axis line. Called internally when rendering and redrawing the + * axis. + */ + renderLine: function () { + if (!this.axisLine) { + this.axisLine = this.chart.renderer.path() + .addClass('highcharts-axis-line') + .add(this.axisGroup); + + + this.axisLine.attr({ + stroke: this.options.lineColor, + 'stroke-width': this.options.lineWidth, + zIndex: 7 + }); + + } + }, + + /** + * Position the axis title. + * + * @private + * + * @return {Object} + * X and Y positions for the title. + */ + getTitlePosition: function () { + // compute anchor points for each of the title align options + var horiz = this.horiz, + axisLeft = this.left, + axisTop = this.top, + axisLength = this.len, + axisTitleOptions = this.options.title, + margin = horiz ? axisLeft : axisTop, + opposite = this.opposite, + offset = this.offset, + xOption = axisTitleOptions.x || 0, + yOption = axisTitleOptions.y || 0, + axisTitle = this.axisTitle, + fontMetrics = this.chart.renderer.fontMetrics( + axisTitleOptions.style && axisTitleOptions.style.fontSize, + axisTitle + ), + // The part of a multiline text that is below the baseline of the + // first line. Subtract 1 to preserve pixel-perfectness from the + // old behaviour (v5.0.12), where only one line was allowed. + textHeightOvershoot = Math.max( + axisTitle.getBBox(null, 0).height - fontMetrics.h - 1, + 0 + ), + + // the position in the length direction of the axis + alongAxis = { + low: margin + (horiz ? 0 : axisLength), + middle: margin + axisLength / 2, + high: margin + (horiz ? axisLength : 0) + }[axisTitleOptions.align], + + // the position in the perpendicular direction of the axis + offAxis = (horiz ? axisTop + this.height : axisLeft) + + (horiz ? 1 : -1) * // horizontal axis reverses the margin + (opposite ? -1 : 1) * // so does opposite axes + this.axisTitleMargin + + [ + -textHeightOvershoot, // top + textHeightOvershoot, // right + fontMetrics.f, // bottom + -textHeightOvershoot // left + ][this.side]; + + + return { + x: horiz ? + alongAxis + xOption : + offAxis + (opposite ? this.width : 0) + offset + xOption, + y: horiz ? + offAxis + yOption - (opposite ? this.height : 0) + offset : + alongAxis + yOption + }; + }, + + /** + * Render a minor tick into the given position. If a minor tick already + * exists in this position, move it. + * + * @param {number} pos + * The position in axis values. + */ + renderMinorTick: function (pos) { + var slideInTicks = this.chart.hasRendered && isNumber(this.oldMin), + minorTicks = this.minorTicks; + + if (!minorTicks[pos]) { + minorTicks[pos] = new Tick(this, pos, 'minor'); + } + + // Render new ticks in old position + if (slideInTicks && minorTicks[pos].isNew) { + minorTicks[pos].render(null, true); + } + + minorTicks[pos].render(null, false, 1); + }, + + /** + * Render a major tick into the given position. If a tick already exists + * in this position, move it. + * + * @param {number} pos + * The position in axis values. + * @param {number} i + * The tick index. + */ + renderTick: function (pos, i) { + var isLinked = this.isLinked, + ticks = this.ticks, + slideInTicks = this.chart.hasRendered && isNumber(this.oldMin); + + // Linked axes need an extra check to find out if + if (!isLinked || (pos >= this.min && pos <= this.max)) { + + if (!ticks[pos]) { + ticks[pos] = new Tick(this, pos); + } + + // render new ticks in old position + if (slideInTicks && ticks[pos].isNew) { + ticks[pos].render(i, true, 0.1); + } + + ticks[pos].render(i); + } + }, + + /** + * Render the axis. + * + * @private + */ + render: function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + options = axis.options, + isLog = axis.isLog, + lin2log = axis.lin2log, + isLinked = axis.isLinked, + tickPositions = axis.tickPositions, + axisTitle = axis.axisTitle, + ticks = axis.ticks, + minorTicks = axis.minorTicks, + alternateBands = axis.alternateBands, + stackLabelOptions = options.stackLabels, + alternateGridColor = options.alternateGridColor, + tickmarkOffset = axis.tickmarkOffset, + axisLine = axis.axisLine, + showAxis = axis.showAxis, + animation = animObject(renderer.globalAnimation), + from, + to; + + // Reset + axis.labelEdge.length = 0; + axis.overlap = false; + + // Mark all elements inActive before we go over and mark the active ones + each([ticks, minorTicks, alternateBands], function (coll) { + objectEach(coll, function (tick) { + tick.isActive = false; + }); + }); + + // If the series has data draw the ticks. Else only the line and title + if (axis.hasData() || isLinked) { + + // minor ticks + if (axis.minorTickInterval && !axis.categories) { + each(axis.getMinorTickPositions(), function (pos) { + axis.renderMinorTick(pos); + }); + } + + // Major ticks. Pull out the first item and render it last so that + // we can get the position of the neighbour label. #808. + if (tickPositions.length) { // #1300 + each(tickPositions, function (pos, i) { + axis.renderTick(pos, i); + }); + // In a categorized axis, the tick marks are displayed + // between labels. So we need to add a tick mark and + // grid line at the left edge of the X axis. + if (tickmarkOffset && (axis.min === 0 || axis.single)) { + if (!ticks[-1]) { + ticks[-1] = new Tick(axis, -1, null, true); + } + ticks[-1].render(-1); + } + + } + + // alternate grid color + if (alternateGridColor) { + each(tickPositions, function (pos, i) { + to = tickPositions[i + 1] !== undefined ? + tickPositions[i + 1] + tickmarkOffset : + axis.max - tickmarkOffset; + + if ( + i % 2 === 0 && + pos < axis.max && + to <= axis.max + ( + chart.polar ? + -tickmarkOffset : + tickmarkOffset + ) + ) { // #2248, #4660 + if (!alternateBands[pos]) { + alternateBands[pos] = new H.PlotLineOrBand(axis); + } + from = pos + tickmarkOffset; // #949 + alternateBands[pos].options = { + from: isLog ? lin2log(from) : from, + to: isLog ? lin2log(to) : to, + color: alternateGridColor + }; + alternateBands[pos].render(); + alternateBands[pos].isActive = true; + } + }); + } + + // custom plot lines and bands + if (!axis._addedPlotLB) { // only first time + each( + (options.plotLines || []).concat(options.plotBands || []), + function (plotLineOptions) { + axis.addPlotBandOrLine(plotLineOptions); + } + ); + axis._addedPlotLB = true; + } + + } // end if hasData + + // Remove inactive ticks + each([ticks, minorTicks, alternateBands], function (coll) { + var i, + forDestruction = [], + delay = animation.duration, + destroyInactiveItems = function () { + i = forDestruction.length; + while (i--) { + // When resizing rapidly, the same items + // may be destroyed in different timeouts, + // or the may be reactivated + if ( + coll[forDestruction[i]] && + !coll[forDestruction[i]].isActive + ) { + coll[forDestruction[i]].destroy(); + delete coll[forDestruction[i]]; + } + } + + }; + + objectEach(coll, function (tick, pos) { + if (!tick.isActive) { + // Render to zero opacity + tick.render(pos, false, 0); + tick.isActive = false; + forDestruction.push(pos); + } + }); + + // When the objects are finished fading out, destroy them + syncTimeout( + destroyInactiveItems, + coll === alternateBands || + !chart.hasRendered || + !delay ? + 0 : + delay + ); + }); + + // Set the axis line path + if (axisLine) { + axisLine[axisLine.isPlaced ? 'animate' : 'attr']({ + d: this.getLinePath(axisLine.strokeWidth()) + }); + axisLine.isPlaced = true; + + // Show or hide the line depending on options.showEmpty + axisLine[showAxis ? 'show' : 'hide'](true); + } + + if (axisTitle && showAxis) { + var titleXy = axis.getTitlePosition(); + if (isNumber(titleXy.y)) { + axisTitle[axisTitle.isNew ? 'attr' : 'animate'](titleXy); + axisTitle.isNew = false; + } else { + axisTitle.attr('y', -9999); + axisTitle.isNew = true; + } + } + + // Stacked totals: + if (stackLabelOptions && stackLabelOptions.enabled) { + axis.renderStackTotals(); + } + // End stacked totals + + axis.isDirty = false; + }, + + /** + * Redraw the axis to reflect changes in the data or axis extremes. Called + * internally from {@link Chart#redraw}. + * + * @private + */ + redraw: function () { + + if (this.visible) { + // render the axis + this.render(); + + // move plot lines and bands + each(this.plotLinesAndBands, function (plotLine) { + plotLine.render(); + }); + } + + // mark associated series as dirty and ready for redraw + each(this.series, function (series) { + series.isDirty = true; + }); + + }, + + // Properties to survive after destroy, needed for Axis.update (#4317, + // #5773, #5881). + keepProps: ['extKey', 'hcEvents', 'names', 'series', 'userMax', 'userMin'], + + /** + * Destroys an Axis instance. See {@link Axis#remove} for the API endpoint + * to fully remove the axis. + * + * @private + * @param {Boolean} keepEvents + * Whether to preserve events, used internally in Axis.update. + */ + destroy: function (keepEvents) { + var axis = this, + stacks = axis.stacks, + plotLinesAndBands = axis.plotLinesAndBands, + plotGroup, + i; + + // Remove the events + if (!keepEvents) { + removeEvent(axis); + } + + // Destroy each stack total + objectEach(stacks, function (stack, stackKey) { + destroyObjectProperties(stack); + + stacks[stackKey] = null; + }); + + // Destroy collections + each( + [axis.ticks, axis.minorTicks, axis.alternateBands], + function (coll) { + destroyObjectProperties(coll); + } + ); + if (plotLinesAndBands) { + i = plotLinesAndBands.length; + while (i--) { // #1975 + plotLinesAndBands[i].destroy(); + } + } + + // Destroy local variables + each( + ['stackTotalGroup', 'axisLine', 'axisTitle', 'axisGroup', + 'gridGroup', 'labelGroup', 'cross'], + function (prop) { + if (axis[prop]) { + axis[prop] = axis[prop].destroy(); + } + } + ); + + // Destroy each generated group for plotlines and plotbands + for (plotGroup in axis.plotLinesAndBandsGroups) { + axis.plotLinesAndBandsGroups[plotGroup] = + axis.plotLinesAndBandsGroups[plotGroup].destroy(); + } + + // Delete all properties and fall back to the prototype. + objectEach(axis, function (val, key) { + if (inArray(key, axis.keepProps) === -1) { + delete axis[key]; + } + }); + }, + + /** + * Internal function to draw a crosshair. + * + * @param {PointerEvent} [e] + * The event arguments from the modified pointer event, extended + * with `chartX` and `chartY` + * @param {Point} [point] + * The Point object if the crosshair snaps to points. + */ + drawCrosshair: function (e, point) { + + var path, + options = this.crosshair, + snap = pick(options.snap, true), + pos, + categorized, + graphic = this.cross; + + // Use last available event when updating non-snapped crosshairs without + // mouse interaction (#5287) + if (!e) { + e = this.cross && this.cross.e; + } + + if ( + // Disabled in options + !this.crosshair || + // Snap + ((defined(point) || !snap) === false) + ) { + this.hideCrosshair(); + } else { + + // Get the path + if (!snap) { + pos = e && + ( + this.horiz ? + e.chartX - this.pos : + this.len - e.chartY + this.pos + ); + } else if (defined(point)) { + // #3834 + pos = this.isXAxis ? point.plotX : this.len - point.plotY; + } + + if (defined(pos)) { + path = this.getPlotLinePath( + // First argument, value, only used on radial + point && (this.isXAxis ? + point.x : + pick(point.stackY, point.y) + ), + null, + null, + null, + pos // Translated position + ) || null; // #3189 + } + + if (!defined(path)) { + this.hideCrosshair(); + return; + } + + categorized = this.categories && !this.isRadial; + + // Draw the cross + if (!graphic) { + this.cross = graphic = this.chart.renderer + .path() + .addClass( + 'highcharts-crosshair highcharts-crosshair-' + + (categorized ? 'category ' : 'thin ') + + options.className + ) + .attr({ + zIndex: pick(options.zIndex, 2) + }) + .add(); + + + // Presentational attributes + graphic.attr({ + 'stroke': options.color || + ( + categorized ? + color('#ccd6eb') + .setOpacity(0.25).get() : + '#cccccc' + ), + 'stroke-width': pick(options.width, 1) + }).css({ + 'pointer-events': 'none' + }); + if (options.dashStyle) { + graphic.attr({ + dashstyle: options.dashStyle + }); + } + + + } + + graphic.show().attr({ + d: path + }); + + if (categorized && !options.width) { + graphic.attr({ + 'stroke-width': this.transA + }); + } + this.cross.e = e; + } + }, + + /** + * Hide the crosshair if visible. + */ + hideCrosshair: function () { + if (this.cross) { + this.cross.hide(); + } + } +}); // end Axis + +H.Axis = Axis; +return Axis; +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var Axis = H.Axis, + defined = H.defined, + each = H.each, + extend = H.extend, + getMagnitude = H.getMagnitude, + normalizeTickInterval = H.normalizeTickInterval, + pick = H.pick, + timeUnits = H.timeUnits; +/** + * Set the tick positions to a time unit that makes sense, for example + * on the first of each month or on every Monday. Return an array + * with the time positions. Used in datetime axes as well as for grouping + * data on a datetime axis. + * + * @param {Object} normalizedInterval The interval in axis values (ms) and the count + * @param {Number} min The minimum in axis values + * @param {Number} max The maximum in axis values + * @param {Number} startOfWeek + */ +Axis.prototype.getTimeTicks = function (normalizedInterval, min, max, startOfWeek) { + var time = this.chart.time, + Date = time.Date, + tickPositions = [], + i, + higherRanks = {}, + minYear, // used in months and years as a basis for Date.UTC() + // When crossing DST, use the max. Resolves #6278. + minDate = new Date( + min - Math.max( + time.getTimezoneOffset(min), + time.getTimezoneOffset(max) + ) + ), + interval = normalizedInterval.unitRange, + count = normalizedInterval.count, + baseOffset, // #6797 + variableDayLength; + + if (defined(min)) { // #1300 + minDate[time.setMilliseconds](interval >= timeUnits.second ? 0 : // #3935 + count * Math.floor(minDate.getMilliseconds() / count)); // #3652, #3654 + + if (interval >= timeUnits.second) { // second + minDate[time.setSeconds](interval >= timeUnits.minute ? 0 : // #3935 + count * Math.floor(minDate.getSeconds() / count)); + } + + if (interval >= timeUnits.minute) { // minute + minDate[time.setMinutes](interval >= timeUnits.hour ? 0 : + count * Math.floor(minDate[time.getMinutes]() / count)); + } + + if (interval >= timeUnits.hour) { // hour + minDate[time.setHours](interval >= timeUnits.day ? 0 : + count * Math.floor(minDate[time.getHours]() / count)); + } + + if (interval >= timeUnits.day) { // day + minDate[time.setDate](interval >= timeUnits.month ? 1 : + count * Math.floor(minDate[time.getDate]() / count)); + } + + if (interval >= timeUnits.month) { // month + minDate[time.setMonth](interval >= timeUnits.year ? 0 : + count * Math.floor(minDate[time.getMonth]() / count)); + minYear = minDate[time.getFullYear](); + } + + if (interval >= timeUnits.year) { // year + minYear -= minYear % count; + minDate[time.setFullYear](minYear); + } + + // week is a special case that runs outside the hierarchy + if (interval === timeUnits.week) { + // get start of current week, independent of count + minDate[time.setDate](minDate[time.getDate]() - minDate[time.getDay]() + + pick(startOfWeek, 1)); + } + + + // Get basics for variable time spans + minYear = minDate[time.getFullYear](); + var minMonth = minDate[time.getMonth](), + minDateDate = minDate[time.getDate](), + minHours = minDate[time.getHours](); + + // Redefine min to the floored/rounded minimum time (#7432) + min = minDate.getTime(); + + // Handle local timezone offset + if (time.variableTimezone) { + + // Detect whether we need to take the DST crossover into + // consideration. If we're crossing over DST, the day length may be + // 23h or 25h and we need to compute the exact clock time for each + // tick instead of just adding hours. This comes at a cost, so first + // we find out if it is needed (#4951). + variableDayLength = ( + // Long range, assume we're crossing over. + max - min > 4 * timeUnits.month || + // Short range, check if min and max are in different time + // zones. + time.getTimezoneOffset(min) !== time.getTimezoneOffset(max) + ); + } + + + // Adjust minDate to the offset date + baseOffset = time.getTimezoneOffset(minDate); + if (baseOffset) { + minDate = new Date(min + baseOffset); + } + + + // Iterate and add tick positions at appropriate values + var t = minDate.getTime(); + i = 1; + while (t < max) { + tickPositions.push(t); + + // if the interval is years, use Date.UTC to increase years + if (interval === timeUnits.year) { + t = time.makeTime(minYear + i * count, 0); + + // if the interval is months, use Date.UTC to increase months + } else if (interval === timeUnits.month) { + t = time.makeTime(minYear, minMonth + i * count); + + // if we're using global time, the interval is not fixed as it jumps + // one hour at the DST crossover + } else if ( + variableDayLength && + (interval === timeUnits.day || interval === timeUnits.week) + ) { + t = time.makeTime(minYear, minMonth, minDateDate + + i * count * (interval === timeUnits.day ? 1 : 7)); + + } else if (variableDayLength && interval === timeUnits.hour) { + // corrected by the start date time zone offset (baseOffset) + // to hide duplicated label (#6797) + t = time.makeTime(minYear, minMonth, minDateDate, minHours + + i * count, 0, 0, baseOffset) - baseOffset; + + // else, the interval is fixed and we use simple addition + } else { + t += interval * count; + } + + i++; + } + + // push the last time + tickPositions.push(t); + + + // Handle higher ranks. Mark new days if the time is on midnight + // (#950, #1649, #1760, #3349). Use a reasonable dropout threshold to + // prevent looping over dense data grouping (#6156). + if (interval <= timeUnits.hour && tickPositions.length < 10000) { + each(tickPositions, function (t) { + if ( + // Speed optimization, no need to run dateFormat unless + // we're on a full or half hour + t % 1800000 === 0 && + // Check for local or global midnight + time.dateFormat('%H%M%S%L', t) === '000000000' + ) { + higherRanks[t] = 'day'; + } + }); + } + } + + + // record information on the chosen unit - for dynamic label formatter + tickPositions.info = extend(normalizedInterval, { + higherRanks: higherRanks, + totalRange: interval * count + }); + + return tickPositions; +}; + +/** + * Get a normalized tick interval for dates. Returns a configuration object with + * unit range (interval), count and name. Used to prepare data for getTimeTicks. + * Previously this logic was part of getTimeTicks, but as getTimeTicks now runs + * of segments in stock charts, the normalizing logic was extracted in order to + * prevent it for running over again for each segment having the same interval. + * #662, #697. + */ +Axis.prototype.normalizeTimeTickInterval = function (tickInterval, unitsOption) { + var units = unitsOption || [[ + 'millisecond', // unit name + [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples + ], [ + 'second', + [1, 2, 5, 10, 15, 30] + ], [ + 'minute', + [1, 2, 5, 10, 15, 30] + ], [ + 'hour', + [1, 2, 3, 4, 6, 8, 12] + ], [ + 'day', + [1, 2] + ], [ + 'week', + [1, 2] + ], [ + 'month', + [1, 2, 3, 4, 6] + ], [ + 'year', + null + ]], + unit = units[units.length - 1], // default unit is years + interval = timeUnits[unit[0]], + multiples = unit[1], + count, + i; + + // loop through the units to find the one that best fits the tickInterval + for (i = 0; i < units.length; i++) { + unit = units[i]; + interval = timeUnits[unit[0]]; + multiples = unit[1]; + + + if (units[i + 1]) { + // lessThan is in the middle between the highest multiple and the next unit. + var lessThan = (interval * multiples[multiples.length - 1] + + timeUnits[units[i + 1][0]]) / 2; + + // break and keep the current unit + if (tickInterval <= lessThan) { + break; + } + } + } + + // prevent 2.5 years intervals, though 25, 250 etc. are allowed + if (interval === timeUnits.year && tickInterval < 5 * interval) { + multiples = [1, 2, 5]; + } + + // get the count + count = normalizeTickInterval( + tickInterval / interval, + multiples, + unit[0] === 'year' ? Math.max(getMagnitude(tickInterval / interval), 1) : 1 // #1913, #2360 + ); + + return { + unitRange: interval, + count: count, + unitName: unit[0] + }; +}; + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var Axis = H.Axis, + getMagnitude = H.getMagnitude, + map = H.map, + normalizeTickInterval = H.normalizeTickInterval, + pick = H.pick; +/** + * Methods defined on the Axis prototype + */ + +/** + * Set the tick positions of a logarithmic axis + */ +Axis.prototype.getLogTickPositions = function (interval, min, max, minor) { + var axis = this, + options = axis.options, + axisLength = axis.len, + lin2log = axis.lin2log, + log2lin = axis.log2lin, + // Since we use this method for both major and minor ticks, + // use a local variable and return the result + positions = []; + + // Reset + if (!minor) { + axis._minorAutoInterval = null; + } + + // First case: All ticks fall on whole logarithms: 1, 10, 100 etc. + if (interval >= 0.5) { + interval = Math.round(interval); + positions = axis.getLinearTickPositions(interval, min, max); + + // Second case: We need intermediary ticks. For example + // 1, 2, 4, 6, 8, 10, 20, 40 etc. + } else if (interval >= 0.08) { + var roundedMin = Math.floor(min), + intermediate, + i, + j, + len, + pos, + lastPos, + break2; + + if (interval > 0.3) { + intermediate = [1, 2, 4]; + } else if (interval > 0.15) { // 0.2 equals five minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 4, 6, 8]; + } else { // 0.1 equals ten minor ticks per 1, 10, 100 etc + intermediate = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + } + + for (i = roundedMin; i < max + 1 && !break2; i++) { + len = intermediate.length; + for (j = 0; j < len && !break2; j++) { + pos = log2lin(lin2log(i) * intermediate[j]); + if (pos > min && (!minor || lastPos <= max) && lastPos !== undefined) { // #1670, lastPos is #3113 + positions.push(lastPos); + } + + if (lastPos > max) { + break2 = true; + } + lastPos = pos; + } + } + + // Third case: We are so deep in between whole logarithmic values that + // we might as well handle the tick positions like a linear axis. For + // example 1.01, 1.02, 1.03, 1.04. + } else { + var realMin = lin2log(min), + realMax = lin2log(max), + tickIntervalOption = minor ? + this.getMinorTickInterval() : + options.tickInterval, + filteredTickIntervalOption = tickIntervalOption === 'auto' ? null : tickIntervalOption, + tickPixelIntervalOption = options.tickPixelInterval / (minor ? 5 : 1), + totalPixelLength = minor ? axisLength / axis.tickPositions.length : axisLength; + + interval = pick( + filteredTickIntervalOption, + axis._minorAutoInterval, + (realMax - realMin) * tickPixelIntervalOption / (totalPixelLength || 1) + ); + + interval = normalizeTickInterval( + interval, + null, + getMagnitude(interval) + ); + + positions = map(axis.getLinearTickPositions( + interval, + realMin, + realMax + ), log2lin); + + if (!minor) { + axis._minorAutoInterval = interval / 5; + } + } + + // Set the axis-level tickInterval variable + if (!minor) { + axis.tickInterval = interval; + } + return positions; +}; + +Axis.prototype.log2lin = function (num) { + return Math.log(num) / Math.LN10; +}; + +Axis.prototype.lin2log = function (num) { + return Math.pow(10, num); +}; + +}(Highcharts)); +(function (H, Axis) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var arrayMax = H.arrayMax, + arrayMin = H.arrayMin, + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + each = H.each, + erase = H.erase, + merge = H.merge, + pick = H.pick; +/* + * The object wrapper for plot lines and plot bands + * @param {Object} options + */ +H.PlotLineOrBand = function (axis, options) { + this.axis = axis; + + if (options) { + this.options = options; + this.id = options.id; + } +}; + +H.PlotLineOrBand.prototype = { + + /** + * Render the plot line or plot band. If it is already existing, + * move it. + */ + render: function () { + var plotLine = this, + axis = plotLine.axis, + horiz = axis.horiz, + options = plotLine.options, + optionsLabel = options.label, + label = plotLine.label, + to = options.to, + from = options.from, + value = options.value, + isBand = defined(from) && defined(to), + isLine = defined(value), + svgElem = plotLine.svgElem, + isNew = !svgElem, + path = [], + color = options.color, + zIndex = pick(options.zIndex, 0), + events = options.events, + attribs = { + 'class': 'highcharts-plot-' + (isBand ? 'band ' : 'line ') + + (options.className || '') + }, + groupAttribs = {}, + renderer = axis.chart.renderer, + groupName = isBand ? 'bands' : 'lines', + group, + log2lin = axis.log2lin; + + // logarithmic conversion + if (axis.isLog) { + from = log2lin(from); + to = log2lin(to); + value = log2lin(value); + } + + + // Set the presentational attributes + if (isLine) { + attribs = { + stroke: color, + 'stroke-width': options.width + }; + if (options.dashStyle) { + attribs.dashstyle = options.dashStyle; + } + + } else if (isBand) { // plot band + if (color) { + attribs.fill = color; + } + if (options.borderWidth) { + attribs.stroke = options.borderColor; + attribs['stroke-width'] = options.borderWidth; + } + } + + + // Grouping and zIndex + groupAttribs.zIndex = zIndex; + groupName += '-' + zIndex; + + group = axis.plotLinesAndBandsGroups[groupName]; + if (!group) { + axis.plotLinesAndBandsGroups[groupName] = group = + renderer.g('plot-' + groupName) + .attr(groupAttribs).add(); + } + + // Create the path + if (isNew) { + plotLine.svgElem = svgElem = + renderer + .path() + .attr(attribs).add(group); + } + + + // Set the path or return + if (isLine) { + path = axis.getPlotLinePath(value, svgElem.strokeWidth()); + } else if (isBand) { // plot band + path = axis.getPlotBandPath(from, to, options); + } else { + return; + } + + + // common for lines and bands + if (isNew && path && path.length) { + svgElem.attr({ d: path }); + + // events + if (events) { + H.objectEach(events, function (event, eventType) { + svgElem.on(eventType, function (e) { + events[eventType].apply(plotLine, [e]); + }); + }); + } + } else if (svgElem) { + if (path) { + svgElem.show(); + svgElem.animate({ d: path }); + } else { + svgElem.hide(); + if (label) { + plotLine.label = label = label.destroy(); + } + } + } + + // the plot band/line label + if ( + optionsLabel && + defined(optionsLabel.text) && + path && + path.length && + axis.width > 0 && + axis.height > 0 && + !path.flat + ) { + // apply defaults + optionsLabel = merge({ + align: horiz && isBand && 'center', + x: horiz ? !isBand && 4 : 10, + verticalAlign: !horiz && isBand && 'middle', + y: horiz ? isBand ? 16 : 10 : isBand ? 6 : -4, + rotation: horiz && !isBand && 90 + }, optionsLabel); + + this.renderLabel(optionsLabel, path, isBand, zIndex); + + } else if (label) { // move out of sight + label.hide(); + } + + // chainable + return plotLine; + }, + + /** + * Render and align label for plot line or band. + */ + renderLabel: function (optionsLabel, path, isBand, zIndex) { + var plotLine = this, + label = plotLine.label, + renderer = plotLine.axis.chart.renderer, + attribs, + xBounds, + yBounds, + x, + y; + + // add the SVG element + if (!label) { + attribs = { + align: optionsLabel.textAlign || optionsLabel.align, + rotation: optionsLabel.rotation, + 'class': 'highcharts-plot-' + (isBand ? 'band' : 'line') + + '-label ' + (optionsLabel.className || '') + }; + + attribs.zIndex = zIndex; + + plotLine.label = label = renderer.text( + optionsLabel.text, + 0, + 0, + optionsLabel.useHTML + ) + .attr(attribs) + .add(); + + + label.css(optionsLabel.style); + + } + + // get the bounding box and align the label + // #3000 changed to better handle choice between plotband or plotline + xBounds = path.xBounds || + [path[1], path[4], (isBand ? path[6] : path[1])]; + yBounds = path.yBounds || + [path[2], path[5], (isBand ? path[7] : path[2])]; + + x = arrayMin(xBounds); + y = arrayMin(yBounds); + + label.align(optionsLabel, false, { + x: x, + y: y, + width: arrayMax(xBounds) - x, + height: arrayMax(yBounds) - y + }); + label.show(); + }, + + /** + * Remove the plot line or band + */ + destroy: function () { + // remove it from the lookup + erase(this.axis.plotLinesAndBands, this); + + delete this.axis; + destroyObjectProperties(this); + } +}; + +/** + * Object with members for extending the Axis prototype + * @todo Extend directly instead of adding object to Highcharts first + */ + +H.extend(Axis.prototype, /** @lends Highcharts.Axis.prototype */ { + + /** + * Internal function to create the SVG path definition for a plot band. + * + * @param {Number} from + * The axis value to start from. + * @param {Number} to + * The axis value to end on. + * + * @return {Array.} + * The SVG path definition in array form. + */ + getPlotBandPath: function (from, to) { + var toPath = this.getPlotLinePath(to, null, null, true), + path = this.getPlotLinePath(from, null, null, true), + result = [], + i, + // #4964 check if chart is inverted or plotband is on yAxis + horiz = this.horiz, + plus = 1, + flat, + outside = + (from < this.min && to < this.min) || + (from > this.max && to > this.max); + + if (path && toPath) { + + // Flat paths don't need labels (#3836) + if (outside) { + flat = path.toString() === toPath.toString(); + plus = 0; + } + + // Go over each subpath - for panes in Highstock + for (i = 0; i < path.length; i += 6) { + + // Add 1 pixel when coordinates are the same + if (horiz && toPath[i + 1] === path[i + 1]) { + toPath[i + 1] += plus; + toPath[i + 4] += plus; + } else if (!horiz && toPath[i + 2] === path[i + 2]) { + toPath[i + 2] += plus; + toPath[i + 5] += plus; + } + + result.push( + 'M', + path[i + 1], + path[i + 2], + 'L', + path[i + 4], + path[i + 5], + toPath[i + 4], + toPath[i + 5], + toPath[i + 1], + toPath[i + 2], + 'z' + ); + result.flat = flat; + } + + } else { // outside the axis area + path = null; + } + + return result; + }, + + /** + * Add a plot band after render time. + * + * @param {AxisPlotBandsOptions} options + * A configuration object for the plot band, as defined in {@link + * https://api.highcharts.com/highcharts/xAxis.plotBands| + * xAxis.plotBands}. + * @return {Object} + * The added plot band. + * @sample highcharts/members/axis-addplotband/ + * Toggle the plot band from a button + */ + addPlotBand: function (options) { + return this.addPlotBandOrLine(options, 'plotBands'); + }, + + /** + * Add a plot line after render time. + * + * @param {AxisPlotLinesOptions} options + * A configuration object for the plot line, as defined in {@link + * https://api.highcharts.com/highcharts/xAxis.plotLines| + * xAxis.plotLines}. + * @return {Object} + * The added plot line. + * @sample highcharts/members/axis-addplotline/ + * Toggle the plot line from a button + */ + addPlotLine: function (options) { + return this.addPlotBandOrLine(options, 'plotLines'); + }, + + /** + * Add a plot band or plot line after render time. Called from addPlotBand + * and addPlotLine internally. + * + * @private + * @param options {AxisPlotLinesOptions|AxisPlotBandsOptions} + * The plotBand or plotLine configuration object. + */ + addPlotBandOrLine: function (options, coll) { + var obj = new H.PlotLineOrBand(this, options).render(), + userOptions = this.userOptions; + + if (obj) { // #2189 + // Add it to the user options for exporting and Axis.update + if (coll) { + userOptions[coll] = userOptions[coll] || []; + userOptions[coll].push(options); + } + this.plotLinesAndBands.push(obj); + } + + return obj; + }, + + /** + * Remove a plot band or plot line from the chart by id. Called internally + * from `removePlotBand` and `removePlotLine`. + * + * @private + * @param {String} id + */ + removePlotBandOrLine: function (id) { + var plotLinesAndBands = this.plotLinesAndBands, + options = this.options, + userOptions = this.userOptions, + i = plotLinesAndBands.length; + while (i--) { + if (plotLinesAndBands[i].id === id) { + plotLinesAndBands[i].destroy(); + } + } + each([ + options.plotLines || [], + userOptions.plotLines || [], + options.plotBands || [], + userOptions.plotBands || [] + ], function (arr) { + i = arr.length; + while (i--) { + if (arr[i].id === id) { + erase(arr, arr[i]); + } + } + }); + }, + + /** + * Remove a plot band by its id. + * + * @param {String} id + * The plot band's `id` as given in the original configuration + * object or in the `addPlotBand` option. + * @sample highcharts/members/axis-removeplotband/ + * Remove plot band by id + * @sample highcharts/members/axis-addplotband/ + * Toggle the plot band from a button + */ + removePlotBand: function (id) { + this.removePlotBandOrLine(id); + }, + + /** + * Remove a plot line by its id. + * @param {String} id + * The plot line's `id` as given in the original configuration + * object or in the `addPlotLine` option. + * @sample highcharts/xaxis/plotlines-id/ + * Remove plot line by id + * @sample highcharts/members/axis-addplotline/ + * Toggle the plot line from a button + */ + removePlotLine: function (id) { + this.removePlotBandOrLine(id); + } +}); + +}(Highcharts, Axis)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var each = H.each, + extend = H.extend, + format = H.format, + isNumber = H.isNumber, + map = H.map, + merge = H.merge, + pick = H.pick, + splat = H.splat, + syncTimeout = H.syncTimeout, + timeUnits = H.timeUnits; +/** + * The tooltip object + * @param {Object} chart The chart instance + * @param {Object} options Tooltip options + */ +H.Tooltip = function () { + this.init.apply(this, arguments); +}; + +H.Tooltip.prototype = { + + init: function (chart, options) { + + // Save the chart and options + this.chart = chart; + this.options = options; + + // List of crosshairs + this.crosshairs = []; + + // Current values of x and y when animating + this.now = { x: 0, y: 0 }; + + // The tooltip is initially hidden + this.isHidden = true; + + + + // Public property for getting the shared state. + this.split = options.split && !chart.inverted; + this.shared = options.shared || this.split; + + }, + + /** + * Destroy the single tooltips in a split tooltip. + * If the tooltip is active then it is not destroyed, unless forced to. + * @param {boolean} force Force destroy all tooltips. + * @return {undefined} + */ + cleanSplit: function (force) { + each(this.chart.series, function (series) { + var tt = series && series.tt; + if (tt) { + if (!tt.isActive || force) { + series.tt = tt.destroy(); + } else { + tt.isActive = false; + } + } + }); + }, + + + + + /** + * Create the Tooltip label element if it doesn't exist, then return the + * label. + */ + getLabel: function () { + + var renderer = this.chart.renderer, + options = this.options; + + if (!this.label) { + // Create the label + if (this.split) { + this.label = renderer.g('tooltip'); + } else { + this.label = renderer.label( + '', + 0, + 0, + options.shape || 'callout', + null, + null, + options.useHTML, + null, + 'tooltip' + ) + .attr({ + padding: options.padding, + r: options.borderRadius + }); + + + this.label + .attr({ + 'fill': options.backgroundColor, + 'stroke-width': options.borderWidth + }) + // #2301, #2657 + .css(options.style) + .shadow(options.shadow); + + } + + + + this.label + .attr({ + zIndex: 8 + }) + .add(); + } + return this.label; + }, + + update: function (options) { + this.destroy(); + // Update user options (#6218) + merge(true, this.chart.options.tooltip.userOptions, options); + this.init(this.chart, merge(true, this.options, options)); + }, + + /** + * Destroy the tooltip and its elements. + */ + destroy: function () { + // Destroy and clear local variables + if (this.label) { + this.label = this.label.destroy(); + } + if (this.split && this.tt) { + this.cleanSplit(this.chart, true); + this.tt = this.tt.destroy(); + } + clearTimeout(this.hideTimer); + clearTimeout(this.tooltipTimeout); + }, + + /** + * Provide a soft movement for the tooltip + * + * @param {Number} x + * @param {Number} y + * @private + */ + move: function (x, y, anchorX, anchorY) { + var tooltip = this, + now = tooltip.now, + animate = tooltip.options.animation !== false && + !tooltip.isHidden && + // When we get close to the target position, abort animation and + // land on the right place (#3056) + (Math.abs(x - now.x) > 1 || Math.abs(y - now.y) > 1), + skipAnchor = tooltip.followPointer || tooltip.len > 1; + + // Get intermediate values for animation + extend(now, { + x: animate ? (2 * now.x + x) / 3 : x, + y: animate ? (now.y + y) / 2 : y, + anchorX: skipAnchor ? + undefined : + animate ? (2 * now.anchorX + anchorX) / 3 : anchorX, + anchorY: skipAnchor ? + undefined : + animate ? (now.anchorY + anchorY) / 2 : anchorY + }); + + // Move to the intermediate value + tooltip.getLabel().attr(now); + + + // Run on next tick of the mouse tracker + if (animate) { + + // Never allow two timeouts + clearTimeout(this.tooltipTimeout); + + // Set the fixed interval ticking for the smooth tooltip + this.tooltipTimeout = setTimeout(function () { + // The interval function may still be running during destroy, + // so check that the chart is really there before calling. + if (tooltip) { + tooltip.move(x, y, anchorX, anchorY); + } + }, 32); + + } + }, + + /** + * Hide the tooltip + */ + hide: function (delay) { + var tooltip = this; + // disallow duplicate timers (#1728, #1766) + clearTimeout(this.hideTimer); + delay = pick(delay, this.options.hideDelay, 500); + if (!this.isHidden) { + this.hideTimer = syncTimeout(function () { + tooltip.getLabel()[delay ? 'fadeOut' : 'hide'](); + tooltip.isHidden = true; + }, delay); + } + }, + + /** + * Extendable method to get the anchor position of the tooltip + * from a point or set of points + */ + getAnchor: function (points, mouseEvent) { + var ret, + chart = this.chart, + inverted = chart.inverted, + plotTop = chart.plotTop, + plotLeft = chart.plotLeft, + plotX = 0, + plotY = 0, + yAxis, + xAxis; + + points = splat(points); + + // Pie uses a special tooltipPos + ret = points[0].tooltipPos; + + // When tooltip follows mouse, relate the position to the mouse + if (this.followPointer && mouseEvent) { + if (mouseEvent.chartX === undefined) { + mouseEvent = chart.pointer.normalize(mouseEvent); + } + ret = [ + mouseEvent.chartX - chart.plotLeft, + mouseEvent.chartY - plotTop + ]; + } + // When shared, use the average position + if (!ret) { + each(points, function (point) { + yAxis = point.series.yAxis; + xAxis = point.series.xAxis; + plotX += point.plotX + + (!inverted && xAxis ? xAxis.left - plotLeft : 0); + plotY += + ( + point.plotLow ? + (point.plotLow + point.plotHigh) / 2 : + point.plotY + ) + + (!inverted && yAxis ? yAxis.top - plotTop : 0); // #1151 + }); + + plotX /= points.length; + plotY /= points.length; + + ret = [ + inverted ? chart.plotWidth - plotY : plotX, + this.shared && !inverted && points.length > 1 && mouseEvent ? + // place shared tooltip next to the mouse (#424) + mouseEvent.chartY - plotTop : + inverted ? chart.plotHeight - plotX : plotY + ]; + } + + return map(ret, Math.round); + }, + + /** + * Place the tooltip in a chart without spilling over + * and not covering the point it self. + */ + getPosition: function (boxWidth, boxHeight, point) { + + var chart = this.chart, + distance = this.distance, + ret = {}, + // Don't use h if chart isn't inverted (#7242) + h = (chart.inverted && point.h) || 0, // #4117 + swapped, + first = ['y', chart.chartHeight, boxHeight, + point.plotY + chart.plotTop, chart.plotTop, + chart.plotTop + chart.plotHeight], + second = ['x', chart.chartWidth, boxWidth, + point.plotX + chart.plotLeft, chart.plotLeft, + chart.plotLeft + chart.plotWidth], + // The far side is right or bottom + preferFarSide = !this.followPointer && pick( + point.ttBelow, + !chart.inverted === !!point.negative + ), // #4984 + + /** + * Handle the preferred dimension. When the preferred dimension is + * tooltip on top or bottom of the point, it will look for space + * there. + */ + firstDimension = function ( + dim, + outerSize, + innerSize, + point, + min, + max + ) { + var roomLeft = innerSize < point - distance, + roomRight = point + distance + innerSize < outerSize, + alignedLeft = point - distance - innerSize, + alignedRight = point + distance; + + if (preferFarSide && roomRight) { + ret[dim] = alignedRight; + } else if (!preferFarSide && roomLeft) { + ret[dim] = alignedLeft; + } else if (roomLeft) { + ret[dim] = Math.min( + max - innerSize, + alignedLeft - h < 0 ? alignedLeft : alignedLeft - h + ); + } else if (roomRight) { + ret[dim] = Math.max( + min, + alignedRight + h + innerSize > outerSize ? + alignedRight : + alignedRight + h + ); + } else { + return false; + } + }, + /** + * Handle the secondary dimension. If the preferred dimension is + * tooltip on top or bottom of the point, the second dimension is to + * align the tooltip above the point, trying to align center but + * allowing left or right align within the chart box. + */ + secondDimension = function (dim, outerSize, innerSize, point) { + var retVal; + + // Too close to the edge, return false and swap dimensions + if (point < distance || point > outerSize - distance) { + retVal = false; + // Align left/top + } else if (point < innerSize / 2) { + ret[dim] = 1; + // Align right/bottom + } else if (point > outerSize - innerSize / 2) { + ret[dim] = outerSize - innerSize - 2; + // Align center + } else { + ret[dim] = point - innerSize / 2; + } + return retVal; + }, + /** + * Swap the dimensions + */ + swap = function (count) { + var temp = first; + first = second; + second = temp; + swapped = count; + }, + run = function () { + if (firstDimension.apply(0, first) !== false) { + if ( + secondDimension.apply(0, second) === false && + !swapped + ) { + swap(true); + run(); + } + } else if (!swapped) { + swap(true); + run(); + } else { + ret.x = ret.y = 0; + } + }; + + // Under these conditions, prefer the tooltip on the side of the point + if (chart.inverted || this.len > 1) { + swap(); + } + run(); + + return ret; + + }, + + /** + * In case no user defined formatter is given, this will be used. Note that + * the context here is an object holding point, series, x, y etc. + * + * @returns {String|Array} + */ + defaultFormatter: function (tooltip) { + var items = this.points || splat(this), + s; + + // Build the header + s = [tooltip.tooltipFooterHeaderFormatter(items[0])]; + + // build the values + s = s.concat(tooltip.bodyFormatter(items)); + + // footer + s.push(tooltip.tooltipFooterHeaderFormatter(items[0], true)); + + return s; + }, + + /** + * Refresh the tooltip's text and position. + * @param {Object|Array} pointOrPoints Rither a point or an array of points + */ + refresh: function (pointOrPoints, mouseEvent) { + var tooltip = this, + label, + options = tooltip.options, + x, + y, + point = pointOrPoints, + anchor, + textConfig = {}, + text, + pointConfig = [], + formatter = options.formatter || tooltip.defaultFormatter, + shared = tooltip.shared, + currentSeries; + + if (!options.enabled) { + return; + } + + clearTimeout(this.hideTimer); + + // get the reference point coordinates (pie charts use tooltipPos) + tooltip.followPointer = splat(point)[0].series.tooltipOptions + .followPointer; + anchor = tooltip.getAnchor(point, mouseEvent); + x = anchor[0]; + y = anchor[1]; + + // shared tooltip, array is sent over + if (shared && !(point.series && point.series.noSharedTooltip)) { + each(point, function (item) { + item.setState('hover'); + + pointConfig.push(item.getLabelConfig()); + }); + + textConfig = { + x: point[0].category, + y: point[0].y + }; + textConfig.points = pointConfig; + point = point[0]; + + // single point tooltip + } else { + textConfig = point.getLabelConfig(); + } + this.len = pointConfig.length; // #6128 + text = formatter.call(textConfig, tooltip); + + // register the current series + currentSeries = point.series; + this.distance = pick(currentSeries.tooltipOptions.distance, 16); + + // update the inner HTML + if (text === false) { + this.hide(); + } else { + + label = tooltip.getLabel(); + + // show it + if (tooltip.isHidden) { + label.attr({ + opacity: 1 + }).show(); + } + + // update text + if (tooltip.split) { + this.renderSplit(text, splat(pointOrPoints)); + } else { + + // Prevent the tooltip from flowing over the chart box (#6659) + + if (!options.style.width) { + + label.css({ + width: this.chart.spacingBox.width + }); + + } + + + label.attr({ + text: text && text.join ? text.join('') : text + }); + + // Set the stroke color of the box to reflect the point + label.removeClass(/highcharts-color-[\d]+/g) + .addClass( + 'highcharts-color-' + + pick(point.colorIndex, currentSeries.colorIndex) + ); + + + label.attr({ + stroke: ( + options.borderColor || + point.color || + currentSeries.color || + '#666666' + ) + }); + + + tooltip.updatePosition({ + plotX: x, + plotY: y, + negative: point.negative, + ttBelow: point.ttBelow, + h: anchor[2] || 0 + }); + } + + this.isHidden = false; + } + }, + + /** + * Render the split tooltip. Loops over each point's text and adds + * a label next to the point, then uses the distribute function to + * find best non-overlapping positions. + */ + renderSplit: function (labels, points) { + var tooltip = this, + boxes = [], + chart = this.chart, + ren = chart.renderer, + rightAligned = true, + options = this.options, + headerHeight = 0, + tooltipLabel = this.getLabel(); + + // Graceful degradation for legacy formatters + if (H.isString(labels)) { + labels = [false, labels]; + } + // Create the individual labels for header and points, ignore footer + each(labels.slice(0, points.length + 1), function (str, i) { + if (str !== false) { + var point = points[i - 1] || + // Item 0 is the header. Instead of this, we could also + // use the crosshair label + { isHeader: true, plotX: points[0].plotX }, + owner = point.series || tooltip, + tt = owner.tt, + series = point.series || {}, + colorClass = 'highcharts-color-' + pick( + point.colorIndex, + series.colorIndex, + 'none' + ), + target, + x, + bBox, + boxWidth; + + // Store the tooltip referance on the series + if (!tt) { + owner.tt = tt = ren.label( + null, + null, + null, + 'callout', + null, + null, + options.useHTML + ) + .addClass('highcharts-tooltip-box ' + colorClass) + .attr({ + 'padding': options.padding, + 'r': options.borderRadius, + + 'fill': options.backgroundColor, + 'stroke': ( + options.borderColor || + point.color || + series.color || + '#333333' + ), + 'stroke-width': options.borderWidth + + }) + .add(tooltipLabel); + } + + tt.isActive = true; + tt.attr({ + text: str + }); + + tt.css(options.style) + .shadow(options.shadow); + + + // Get X position now, so we can move all to the other side in + // case of overflow + bBox = tt.getBBox(); + boxWidth = bBox.width + tt.strokeWidth(); + if (point.isHeader) { + headerHeight = bBox.height; + x = Math.max( + 0, // No left overflow + Math.min( + point.plotX + chart.plotLeft - boxWidth / 2, + // No right overflow (#5794) + chart.chartWidth - boxWidth + ) + ); + } else { + x = point.plotX + chart.plotLeft - + pick(options.distance, 16) - boxWidth; + } + + + // If overflow left, we don't use this x in the next loop + if (x < 0) { + rightAligned = false; + } + + // Prepare for distribution + target = (point.series && point.series.yAxis && + point.series.yAxis.pos) + (point.plotY || 0); + target -= chart.plotTop; + boxes.push({ + target: point.isHeader ? + chart.plotHeight + headerHeight : + target, + rank: point.isHeader ? 1 : 0, + size: owner.tt.getBBox().height + 1, + point: point, + x: x, + tt: tt + }); + } + }); + + // Clean previous run (for missing points) + this.cleanSplit(); + + // Distribute and put in place + H.distribute(boxes, chart.plotHeight + headerHeight); + each(boxes, function (box) { + var point = box.point, + series = point.series; + + // Put the label in place + box.tt.attr({ + visibility: box.pos === undefined ? 'hidden' : 'inherit', + x: (rightAligned || point.isHeader ? + box.x : + point.plotX + chart.plotLeft + pick(options.distance, 16)), + y: box.pos + chart.plotTop, + anchorX: point.isHeader ? + point.plotX + chart.plotLeft : + point.plotX + series.xAxis.pos, + anchorY: point.isHeader ? + box.pos + chart.plotTop - 15 : + point.plotY + series.yAxis.pos + }); + }); + }, + + /** + * Find the new position and perform the move + */ + updatePosition: function (point) { + var chart = this.chart, + label = this.getLabel(), + pos = (this.options.positioner || this.getPosition).call( + this, + label.width, + label.height, + point + ); + + // do the move + this.move( + Math.round(pos.x), + Math.round(pos.y || 0), // can be undefined (#3977) + point.plotX + chart.plotLeft, + point.plotY + chart.plotTop + ); + }, + + /** + * Get the optimal date format for a point, based on a range. + * @param {number} range - The time range + * @param {number|Date} date - The date of the point in question + * @param {number} startOfWeek - An integer representing the first day of + * the week, where 0 is Sunday + * @param {Object} dateTimeLabelFormats - A map of time units to formats + * @return {string} - the optimal date format for a point + */ + getDateFormat: function (range, date, startOfWeek, dateTimeLabelFormats) { + var time = this.chart.time, + dateStr = time.dateFormat('%m-%d %H:%M:%S.%L', date), + format, + n, + blank = '01-01 00:00:00.000', + strpos = { + millisecond: 15, + second: 12, + minute: 9, + hour: 6, + day: 3 + }, + lastN = 'millisecond'; // for sub-millisecond data, #4223 + for (n in timeUnits) { + + // If the range is exactly one week and we're looking at a + // Sunday/Monday, go for the week format + if ( + range === timeUnits.week && + +time.dateFormat('%w', date) === startOfWeek && + dateStr.substr(6) === blank.substr(6) + ) { + n = 'week'; + break; + } + + // The first format that is too great for the range + if (timeUnits[n] > range) { + n = lastN; + break; + } + + // If the point is placed every day at 23:59, we need to show + // the minutes as well. #2637. + if ( + strpos[n] && + dateStr.substr(strpos[n]) !== blank.substr(strpos[n]) + ) { + break; + } + + // Weeks are outside the hierarchy, only apply them on + // Mondays/Sundays like in the first condition + if (n !== 'week') { + lastN = n; + } + } + + if (n) { + format = dateTimeLabelFormats[n]; + } + + return format; + }, + + /** + * Get the best X date format based on the closest point range on the axis. + */ + getXDateFormat: function (point, options, xAxis) { + var xDateFormat, + dateTimeLabelFormats = options.dateTimeLabelFormats, + closestPointRange = xAxis && xAxis.closestPointRange; + + if (closestPointRange) { + xDateFormat = this.getDateFormat( + closestPointRange, + point.x, + xAxis.options.startOfWeek, + dateTimeLabelFormats + ); + } else { + xDateFormat = dateTimeLabelFormats.day; + } + + return xDateFormat || dateTimeLabelFormats.year; // #2546, 2581 + }, + + /** + * Format the footer/header of the tooltip + * #3397: abstraction to enable formatting of footer and header + */ + tooltipFooterHeaderFormatter: function (labelConfig, isFooter) { + var footOrHead = isFooter ? 'footer' : 'header', + series = labelConfig.series, + tooltipOptions = series.tooltipOptions, + xDateFormat = tooltipOptions.xDateFormat, + xAxis = series.xAxis, + isDateTime = ( + xAxis && + xAxis.options.type === 'datetime' && + isNumber(labelConfig.key) + ), + formatString = tooltipOptions[footOrHead + 'Format']; + + // Guess the best date format based on the closest point distance (#568, + // #3418) + if (isDateTime && !xDateFormat) { + xDateFormat = this.getXDateFormat( + labelConfig, + tooltipOptions, + xAxis + ); + } + + // Insert the footer date format if any + if (isDateTime && xDateFormat) { + each( + (labelConfig.point && labelConfig.point.tooltipDateKeys) || + ['key'], + function (key) { + formatString = formatString.replace( + '{point.' + key + '}', + '{point.' + key + ':' + xDateFormat + '}' + ); + } + ); + } + + return format(formatString, { + point: labelConfig, + series: series + }, this.chart.time); + }, + + /** + * Build the body (lines) of the tooltip by iterating over the items and + * returning one entry for each item, abstracting this functionality allows + * to easily overwrite and extend it. + */ + bodyFormatter: function (items) { + return map(items, function (item) { + var tooltipOptions = item.series.tooltipOptions; + return ( + tooltipOptions[ + (item.point.formatPrefix || 'point') + 'Formatter' + ] || + item.point.tooltipFormatter + ).call( + item.point, + tooltipOptions[(item.point.formatPrefix || 'point') + 'Format'] + ); + }); + } + +}; + +}(Highcharts)); +(function (Highcharts) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var H = Highcharts, + addEvent = H.addEvent, + attr = H.attr, + charts = H.charts, + color = H.color, + css = H.css, + defined = H.defined, + each = H.each, + extend = H.extend, + find = H.find, + fireEvent = H.fireEvent, + isObject = H.isObject, + offset = H.offset, + pick = H.pick, + splat = H.splat, + Tooltip = H.Tooltip; + +/** + * The mouse and touch tracker object. Each {@link Chart} item has one + * assosiated Pointer item that can be accessed from the {@link Chart.pointer} + * property. + * + * @class + * @param {Chart} chart + * The Chart instance. + * @param {Options} options + * The root options object. The pointer uses options from the chart and + * tooltip structures. + */ +Highcharts.Pointer = function (chart, options) { + this.init(chart, options); +}; + +Highcharts.Pointer.prototype = { + /** + * Initialize the Pointer. + * + * @private + */ + init: function (chart, options) { + + // Store references + this.options = options; + this.chart = chart; + + // Do we need to handle click on a touch device? + this.runChartClick = options.chart.events && !!options.chart.events.click; + + this.pinchDown = []; + this.lastValidTouch = {}; + + if (Tooltip) { + chart.tooltip = new Tooltip(chart, options.tooltip); + this.followTouchMove = pick(options.tooltip.followTouchMove, true); + } + + this.setDOMEvents(); + }, + + /** + * Resolve the zoomType option, this is reset on all touch start and mouse + * down events. + * + * @private + */ + zoomOption: function (e) { + var chart = this.chart, + options = chart.options.chart, + zoomType = options.zoomType || '', + inverted = chart.inverted, + zoomX, + zoomY; + + // Look for the pinchType option + if (/touch/.test(e.type)) { + zoomType = pick(options.pinchType, zoomType); + } + + this.zoomX = zoomX = /x/.test(zoomType); + this.zoomY = zoomY = /y/.test(zoomType); + this.zoomHor = (zoomX && !inverted) || (zoomY && inverted); + this.zoomVert = (zoomY && !inverted) || (zoomX && inverted); + this.hasZoom = zoomX || zoomY; + }, + + /** + * @typedef {Object} PointerEvent + * A native browser mouse or touch event, extended with position + * information relative to the {@link Chart.container}. + * @property {Number} chartX + * The X coordinate of the pointer interaction relative to the + * chart. + * @property {Number} chartY + * The Y coordinate of the pointer interaction relative to the + * chart. + * + */ + /** + * Takes a browser event object and extends it with custom Highcharts + * properties `chartX` and `chartY` in order to work on the internal + * coordinate system. + * + * @param {Object} e + * The event object in standard browsers. + * + * @return {PointerEvent} + * A browser event with extended properties `chartX` and `chartY`. + */ + normalize: function (e, chartPosition) { + var ePos; + + // iOS (#2757) + ePos = e.touches ? (e.touches.length ? e.touches.item(0) : e.changedTouches[0]) : e; + + // Get mouse position + if (!chartPosition) { + this.chartPosition = chartPosition = offset(this.chart.container); + } + + return extend(e, { + chartX: Math.round(ePos.pageX - chartPosition.left), + chartY: Math.round(ePos.pageY - chartPosition.top) + }); + }, + + /** + * Get the click position in terms of axis values. + * + * @param {PointerEvent} e + * A pointer event, extended with `chartX` and `chartY` + * properties. + */ + getCoordinates: function (e) { + var coordinates = { + xAxis: [], + yAxis: [] + }; + + each(this.chart.axes, function (axis) { + coordinates[axis.isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: axis.toValue(e[axis.horiz ? 'chartX' : 'chartY']) + }); + }); + return coordinates; + }, + /** + * Finds the closest point to a set of coordinates, using the k-d-tree + * algorithm. + * + * @param {Array.} series + * All the series to search in. + * @param {boolean} shared + * Whether it is a shared tooltip or not. + * @param {object} coordinates + * Chart coordinates of the pointer. + * @param {number} coordinates.chartX + * @param {number} coordinates.chartY + * + * @return {Point|undefined} The point closest to given coordinates. + */ + findNearestKDPoint: function (series, shared, coordinates) { + var closest, + sort = function (p1, p2) { + var isCloserX = p1.distX - p2.distX, + isCloser = p1.dist - p2.dist, + isAbove = + (p2.series.group && p2.series.group.zIndex) - + (p1.series.group && p1.series.group.zIndex), + result; + + // We have two points which are not in the same place on xAxis + // and shared tooltip: + if (isCloserX !== 0 && shared) { // #5721 + result = isCloserX; + // Points are not exactly in the same place on x/yAxis: + } else if (isCloser !== 0) { + result = isCloser; + // The same xAxis and yAxis position, sort by z-index: + } else if (isAbove !== 0) { + result = isAbove; + // The same zIndex, sort by array index: + } else { + result = p1.series.index > p2.series.index ? -1 : 1; + } + return result; + }; + each(series, function (s) { + var noSharedTooltip = s.noSharedTooltip && shared, + compareX = ( + !noSharedTooltip && + s.options.findNearestPointBy.indexOf('y') < 0 + ), + point = s.searchPoint( + coordinates, + compareX + ); + if ( + // Check that we actually found a point on the series. + isObject(point, true) && + // Use the new point if it is closer. + (!isObject(closest, true) || (sort(closest, point) > 0)) + ) { + closest = point; + } + }); + return closest; + }, + getPointFromEvent: function (e) { + var target = e.target, + point; + + while (target && !point) { + point = target.point; + target = target.parentNode; + } + return point; + }, + + getChartCoordinatesFromPoint: function (point, inverted) { + var series = point.series, + xAxis = series.xAxis, + yAxis = series.yAxis, + plotX = pick(point.clientX, point.plotX); + + if (xAxis && yAxis) { + return inverted ? { + chartX: xAxis.len + xAxis.pos - plotX, + chartY: yAxis.len + yAxis.pos - point.plotY + } : { + chartX: plotX + xAxis.pos, + chartY: point.plotY + yAxis.pos + }; + } + }, + + /** + * Calculates what is the current hovered point/points and series. + * + * @private + * + * @param {undefined|Point} existingHoverPoint + * The point currrently beeing hovered. + * @param {undefined|Series} existingHoverSeries + * The series currently beeing hovered. + * @param {Array.} series + * All the series in the chart. + * @param {boolean} isDirectTouch + * Is the pointer directly hovering the point. + * @param {boolean} shared + * Whether it is a shared tooltip or not. + * @param {object} coordinates + * Chart coordinates of the pointer. + * @param {number} coordinates.chartX + * @param {number} coordinates.chartY + * + * @return {object} + * Object containing resulting hover data. + */ + getHoverData: function ( + existingHoverPoint, + existingHoverSeries, + series, + isDirectTouch, + shared, + coordinates, + params + ) { + var hoverPoint, + hoverPoints = [], + hoverSeries = existingHoverSeries, + isBoosting = params && params.isBoosting, + useExisting = !!(isDirectTouch && existingHoverPoint), + notSticky = hoverSeries && !hoverSeries.stickyTracking, + filter = function (s) { + return ( + s.visible && + !(!shared && s.directTouch) && // #3821 + pick(s.options.enableMouseTracking, true) + ); + }, + // Which series to look in for the hover point + searchSeries = notSticky ? + // Only search on hovered series if it has stickyTracking false + [hoverSeries] : + // Filter what series to look in. + H.grep(series, function (s) { + return filter(s) && s.stickyTracking; + }); + + // Use existing hovered point or find the one closest to coordinates. + hoverPoint = useExisting ? + existingHoverPoint : + this.findNearestKDPoint(searchSeries, shared, coordinates); + + // Assign hover series + hoverSeries = hoverPoint && hoverPoint.series; + + // If we have a hoverPoint, assign hoverPoints. + if (hoverPoint) { + // When tooltip is shared, it displays more than one point + if (shared && !hoverSeries.noSharedTooltip) { + searchSeries = H.grep(series, function (s) { + return filter(s) && !s.noSharedTooltip; + }); + + // Get all points with the same x value as the hoverPoint + each(searchSeries, function (s) { + var point = find(s.points, function (p) { + return p.x === hoverPoint.x && !p.isNull; + }); + if (isObject(point)) { + /* + * Boost returns a minimal point. Convert it to a usable + * point for tooltip and states. + */ + if (isBoosting) { + point = s.getPoint(point); + } + hoverPoints.push(point); + } + }); + } else { + hoverPoints.push(hoverPoint); + } + } + return { + hoverPoint: hoverPoint, + hoverSeries: hoverSeries, + hoverPoints: hoverPoints + }; + }, + /** + * With line type charts with a single tracker, get the point closest to the + * mouse. Run Point.onMouseOver and display tooltip for the point or points. + * + * @private + */ + runPointActions: function (e, p) { + var pointer = this, + chart = pointer.chart, + series = chart.series, + tooltip = chart.tooltip && chart.tooltip.options.enabled ? + chart.tooltip : + undefined, + shared = tooltip ? tooltip.shared : false, + hoverPoint = p || chart.hoverPoint, + hoverSeries = hoverPoint && hoverPoint.series || chart.hoverSeries, + // onMouseOver or already hovering a series with directTouch + isDirectTouch = !!p || ( + (hoverSeries && hoverSeries.directTouch) && + pointer.isDirectTouch + ), + hoverData = this.getHoverData( + hoverPoint, + hoverSeries, + series, + isDirectTouch, + shared, + e, + { isBoosting: chart.isBoosting } + ), + useSharedTooltip, + followPointer, + anchor, + points; + + // Update variables from hoverData. + hoverPoint = hoverData.hoverPoint; + points = hoverData.hoverPoints; + hoverSeries = hoverData.hoverSeries; + followPointer = hoverSeries && hoverSeries.tooltipOptions.followPointer; + useSharedTooltip = shared && hoverSeries && !hoverSeries.noSharedTooltip; + + // Refresh tooltip for kdpoint if new hover point or tooltip was hidden + // #3926, #4200 + if ( + hoverPoint && + // !(hoverSeries && hoverSeries.directTouch) && + (hoverPoint !== chart.hoverPoint || (tooltip && tooltip.isHidden)) + ) { + each(chart.hoverPoints || [], function (p) { + if (H.inArray(p, points) === -1) { + p.setState(); + } + }); + // Do mouseover on all points (#3919, #3985, #4410, #5622) + each(points || [], function (p) { + p.setState('hover'); + }); + // set normal state to previous series + if (chart.hoverSeries !== hoverSeries) { + hoverSeries.onMouseOver(); + } + + // If tracking is on series in stead of on each point, + // fire mouseOver on hover point. // #4448 + if (chart.hoverPoint) { + chart.hoverPoint.firePointEvent('mouseOut'); + } + + // Hover point may have been destroyed in the event handlers (#7127) + if (!hoverPoint.series) { + return; + } + + hoverPoint.firePointEvent('mouseOver'); + chart.hoverPoints = points; + chart.hoverPoint = hoverPoint; + // Draw tooltip if necessary + if (tooltip) { + tooltip.refresh(useSharedTooltip ? points : hoverPoint, e); + } + // Update positions (regardless of kdpoint or hoverPoint) + } else if (followPointer && tooltip && !tooltip.isHidden) { + anchor = tooltip.getAnchor([{}], e); + tooltip.updatePosition({ plotX: anchor[0], plotY: anchor[1] }); + } + + // Start the event listener to pick up the tooltip and crosshairs + if (!pointer.unDocMouseMove) { + pointer.unDocMouseMove = addEvent( + chart.container.ownerDocument, + 'mousemove', + function (e) { + var chart = charts[H.hoverChartIndex]; + if (chart) { + chart.pointer.onDocumentMouseMove(e); + } + } + ); + } + + // Issues related to crosshair #4927, #5269 #5066, #5658 + each(chart.axes, function drawAxisCrosshair(axis) { + var snap = pick(axis.crosshair.snap, true), + point = !snap ? + undefined : + H.find(points, function (p) { + return p.series[axis.coll] === axis; + }); + + // Axis has snapping crosshairs, and one of the hover points belongs + // to axis. Always call drawCrosshair when it is not snap. + if (point || !snap) { + axis.drawCrosshair(e, point); + // Axis has snapping crosshairs, but no hover point belongs to axis + } else { + axis.hideCrosshair(); + } + }); + }, + + /** + * Reset the tracking by hiding the tooltip, the hover series state and the + * hover point + * + * @param allowMove {Boolean} + * Instead of destroying the tooltip altogether, allow moving it if + * possible. + */ + reset: function (allowMove, delay) { + var pointer = this, + chart = pointer.chart, + hoverSeries = chart.hoverSeries, + hoverPoint = chart.hoverPoint, + hoverPoints = chart.hoverPoints, + tooltip = chart.tooltip, + tooltipPoints = tooltip && tooltip.shared ? hoverPoints : hoverPoint; + + // Check if the points have moved outside the plot area (#1003, #4736, #5101) + if (allowMove && tooltipPoints) { + each(splat(tooltipPoints), function (point) { + if (point.series.isCartesian && point.plotX === undefined) { + allowMove = false; + } + }); + } + + // Just move the tooltip, #349 + if (allowMove) { + if (tooltip && tooltipPoints) { + tooltip.refresh(tooltipPoints); + if (hoverPoint) { // #2500 + hoverPoint.setState(hoverPoint.state, true); + each(chart.axes, function (axis) { + if (axis.crosshair) { + axis.drawCrosshair(null, hoverPoint); + } + }); + } + } + + // Full reset + } else { + + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + if (hoverSeries) { + hoverSeries.onMouseOut(); + } + + if (tooltip) { + tooltip.hide(delay); + } + + if (pointer.unDocMouseMove) { + pointer.unDocMouseMove = pointer.unDocMouseMove(); + } + + // Remove crosshairs + each(chart.axes, function (axis) { + axis.hideCrosshair(); + }); + + pointer.hoverX = chart.hoverPoints = chart.hoverPoint = null; + } + }, + + /** + * Scale series groups to a certain scale and translation. + * + * @private + */ + scaleGroups: function (attribs, clip) { + + var chart = this.chart, + seriesAttribs; + + // Scale each series + each(chart.series, function (series) { + seriesAttribs = attribs || series.getPlotBox(); // #1701 + if (series.xAxis && series.xAxis.zoomEnabled && series.group) { + series.group.attr(seriesAttribs); + if (series.markerGroup) { + series.markerGroup.attr(seriesAttribs); + series.markerGroup.clip(clip ? chart.clipRect : null); + } + if (series.dataLabelsGroup) { + series.dataLabelsGroup.attr(seriesAttribs); + } + } + }); + + // Clip + chart.clipRect.attr(clip || chart.clipBox); + }, + + /** + * Start a drag operation. + * + * @private + */ + dragStart: function (e) { + var chart = this.chart; + + // Record the start position + chart.mouseIsDown = e.type; + chart.cancelClick = false; + chart.mouseDownX = this.mouseDownX = e.chartX; + chart.mouseDownY = this.mouseDownY = e.chartY; + }, + + /** + * Perform a drag operation in response to a mousemove event while the mouse + * is down. + * + * @private + */ + drag: function (e) { + + var chart = this.chart, + chartOptions = chart.options.chart, + chartX = e.chartX, + chartY = e.chartY, + zoomHor = this.zoomHor, + zoomVert = this.zoomVert, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + clickedInside, + size, + selectionMarker = this.selectionMarker, + mouseDownX = this.mouseDownX, + mouseDownY = this.mouseDownY, + panKey = chartOptions.panKey && e[chartOptions.panKey + 'Key']; + + // If the device supports both touch and mouse (like IE11), and we are touch-dragging + // inside the plot area, don't handle the mouse event. #4339. + if (selectionMarker && selectionMarker.touch) { + return; + } + + // If the mouse is outside the plot area, adjust to cooordinates + // inside to prevent the selection marker from going outside + if (chartX < plotLeft) { + chartX = plotLeft; + } else if (chartX > plotLeft + plotWidth) { + chartX = plotLeft + plotWidth; + } + + if (chartY < plotTop) { + chartY = plotTop; + } else if (chartY > plotTop + plotHeight) { + chartY = plotTop + plotHeight; + } + + // determine if the mouse has moved more than 10px + this.hasDragged = Math.sqrt( + Math.pow(mouseDownX - chartX, 2) + + Math.pow(mouseDownY - chartY, 2) + ); + + if (this.hasDragged > 10) { + clickedInside = chart.isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop); + + // make a selection + if (chart.hasCartesianSeries && (this.zoomX || this.zoomY) && clickedInside && !panKey) { + if (!selectionMarker) { + this.selectionMarker = selectionMarker = chart.renderer.rect( + plotLeft, + plotTop, + zoomHor ? 1 : plotWidth, + zoomVert ? 1 : plotHeight, + 0 + ) + .attr({ + + fill: chartOptions.selectionMarkerFill || color('#335cad').setOpacity(0.25).get(), + + 'class': 'highcharts-selection-marker', + 'zIndex': 7 + }) + .add(); + } + } + + // adjust the width of the selection marker + if (selectionMarker && zoomHor) { + size = chartX - mouseDownX; + selectionMarker.attr({ + width: Math.abs(size), + x: (size > 0 ? 0 : size) + mouseDownX + }); + } + // adjust the height of the selection marker + if (selectionMarker && zoomVert) { + size = chartY - mouseDownY; + selectionMarker.attr({ + height: Math.abs(size), + y: (size > 0 ? 0 : size) + mouseDownY + }); + } + + // panning + if (clickedInside && !selectionMarker && chartOptions.panning) { + chart.pan(e, chartOptions.panning); + } + } + }, + + /** + * On mouse up or touch end across the entire document, drop the selection. + * + * @private + */ + drop: function (e) { + var pointer = this, + chart = this.chart, + hasPinched = this.hasPinched; + + if (this.selectionMarker) { + var selectionData = { + originalEvent: e, // #4890 + xAxis: [], + yAxis: [] + }, + selectionBox = this.selectionMarker, + selectionLeft = selectionBox.attr ? selectionBox.attr('x') : selectionBox.x, + selectionTop = selectionBox.attr ? selectionBox.attr('y') : selectionBox.y, + selectionWidth = selectionBox.attr ? selectionBox.attr('width') : selectionBox.width, + selectionHeight = selectionBox.attr ? selectionBox.attr('height') : selectionBox.height, + runZoom; + + // a selection has been made + if (this.hasDragged || hasPinched) { + + // record each axis' min and max + each(chart.axes, function (axis) { + if (axis.zoomEnabled && defined(axis.min) && (hasPinched || pointer[{ xAxis: 'zoomX', yAxis: 'zoomY' }[axis.coll]])) { // #859, #3569 + var horiz = axis.horiz, + minPixelPadding = e.type === 'touchend' ? axis.minPixelPadding : 0, // #1207, #3075 + selectionMin = axis.toValue((horiz ? selectionLeft : selectionTop) + minPixelPadding), + selectionMax = axis.toValue((horiz ? selectionLeft + selectionWidth : selectionTop + selectionHeight) - minPixelPadding); + + selectionData[axis.coll].push({ + axis: axis, + min: Math.min(selectionMin, selectionMax), // for reversed axes + max: Math.max(selectionMin, selectionMax) + }); + runZoom = true; + } + }); + if (runZoom) { + fireEvent(chart, 'selection', selectionData, function (args) { + chart.zoom(extend(args, hasPinched ? { animation: false } : null)); + }); + } + + } + this.selectionMarker = this.selectionMarker.destroy(); + + // Reset scaling preview + if (hasPinched) { + this.scaleGroups(); + } + } + + // Reset all + if (chart) { // it may be destroyed on mouse up - #877 + css(chart.container, { cursor: chart._cursor }); + chart.cancelClick = this.hasDragged > 10; // #370 + chart.mouseIsDown = this.hasDragged = this.hasPinched = false; + this.pinchDown = []; + } + }, + + onContainerMouseDown: function (e) { + + if (e.button !== 2) { + + e = this.normalize(e); + + this.zoomOption(e); + + // issue #295, dragging not always working in Firefox + if (e.preventDefault) { + e.preventDefault(); + } + + this.dragStart(e); + } + }, + + + + onDocumentMouseUp: function (e) { + if (charts[H.hoverChartIndex]) { + charts[H.hoverChartIndex].pointer.drop(e); + } + }, + + /** + * Special handler for mouse move that will hide the tooltip when the mouse + * leaves the plotarea. Issue #149 workaround. The mouseleave event does not + * always fire. + * + * @private + */ + onDocumentMouseMove: function (e) { + var chart = this.chart, + chartPosition = this.chartPosition; + + e = this.normalize(e, chartPosition); + + // If we're outside, hide the tooltip + if (chartPosition && !this.inClass(e.target, 'highcharts-tracker') && + !chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) { + this.reset(); + } + }, + + /** + * When mouse leaves the container, hide the tooltip. + * + * @private + */ + onContainerMouseLeave: function (e) { + var chart = charts[H.hoverChartIndex]; + if (chart && (e.relatedTarget || e.toElement)) { // #4886, MS Touch end fires mouseleave but with no related target + chart.pointer.reset(); + chart.pointer.chartPosition = null; // also reset the chart position, used in #149 fix + } + }, + + // The mousemove, touchmove and touchstart event handler + onContainerMouseMove: function (e) { + + var chart = this.chart; + + if (!defined(H.hoverChartIndex) || !charts[H.hoverChartIndex] || !charts[H.hoverChartIndex].mouseIsDown) { + H.hoverChartIndex = chart.index; + } + + e = this.normalize(e); + e.returnValue = false; // #2251, #3224 + + if (chart.mouseIsDown === 'mousedown') { + this.drag(e); + } + + // Show the tooltip and run mouse over events (#977) + if ((this.inClass(e.target, 'highcharts-tracker') || + chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop)) && !chart.openMenu) { + this.runPointActions(e); + } + }, + + /** + * Utility to detect whether an element has, or has a parent with, a specific + * class name. Used on detection of tracker objects and on deciding whether + * hovering the tooltip should cause the active series to mouse out. + * + * @param {SVGDOMElement|HTMLDOMElement} element + * The element to investigate. + * @param {String} className + * The class name to look for. + * + * @return {Boolean} + * True if either the element or one of its parents has the given + * class name. + */ + inClass: function (element, className) { + var elemClassName; + while (element) { + elemClassName = attr(element, 'class'); + if (elemClassName) { + if (elemClassName.indexOf(className) !== -1) { + return true; + } + if (elemClassName.indexOf('highcharts-container') !== -1) { + return false; + } + } + element = element.parentNode; + } + }, + + onTrackerMouseOut: function (e) { + var series = this.chart.hoverSeries, + relatedTarget = e.relatedTarget || e.toElement; + + this.isDirectTouch = false; + + if ( + series && + relatedTarget && + !series.stickyTracking && + !this.inClass(relatedTarget, 'highcharts-tooltip') && + ( + !this.inClass( + relatedTarget, + 'highcharts-series-' + series.index + ) || // #2499, #4465 + !this.inClass(relatedTarget, 'highcharts-tracker') // #5553 + ) + ) { + series.onMouseOut(); + } + }, + + onContainerClick: function (e) { + var chart = this.chart, + hoverPoint = chart.hoverPoint, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop; + + e = this.normalize(e); + + if (!chart.cancelClick) { + + // On tracker click, fire the series and point events. #783, #1583 + if (hoverPoint && this.inClass(e.target, 'highcharts-tracker')) { + + // the series click event + fireEvent(hoverPoint.series, 'click', extend(e, { + point: hoverPoint + })); + + // the point click event + if (chart.hoverPoint) { // it may be destroyed (#1844) + hoverPoint.firePointEvent('click', e); + } + + // When clicking outside a tracker, fire a chart event + } else { + extend(e, this.getCoordinates(e)); + + // fire a click event in the chart + if (chart.isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { + fireEvent(chart, 'click', e); + } + } + + + } + }, + + /** + * Set the JS DOM events on the container and document. This method should contain + * a one-to-one assignment between methods and their handlers. Any advanced logic should + * be moved to the handler reflecting the event's name. + * + * @private + */ + setDOMEvents: function () { + + var pointer = this, + container = pointer.chart.container, + ownerDoc = container.ownerDocument; + + container.onmousedown = function (e) { + pointer.onContainerMouseDown(e); + }; + container.onmousemove = function (e) { + pointer.onContainerMouseMove(e); + }; + container.onclick = function (e) { + pointer.onContainerClick(e); + }; + this.unbindContainerMouseLeave = addEvent( + container, + 'mouseleave', + pointer.onContainerMouseLeave + ); + if (!H.unbindDocumentMouseUp) { + H.unbindDocumentMouseUp = addEvent( + ownerDoc, + 'mouseup', + pointer.onDocumentMouseUp + ); + } + if (H.hasTouch) { + container.ontouchstart = function (e) { + pointer.onContainerTouchStart(e); + }; + container.ontouchmove = function (e) { + pointer.onContainerTouchMove(e); + }; + if (!H.unbindDocumentTouchEnd) { + H.unbindDocumentTouchEnd = addEvent( + ownerDoc, + 'touchend', + pointer.onDocumentTouchEnd + ); + } + } + + }, + + /** + * Destroys the Pointer object and disconnects DOM events. + */ + destroy: function () { + var pointer = this; + + if (pointer.unDocMouseMove) { + pointer.unDocMouseMove(); + } + + this.unbindContainerMouseLeave(); + + if (!H.chartCount) { + if (H.unbindDocumentMouseUp) { + H.unbindDocumentMouseUp = H.unbindDocumentMouseUp(); + } + if (H.unbindDocumentTouchEnd) { + H.unbindDocumentTouchEnd = H.unbindDocumentTouchEnd(); + } + } + + // memory and CPU leak + clearInterval(pointer.tooltipTimeout); + + H.objectEach(pointer, function (val, prop) { + pointer[prop] = null; + }); + } +}; + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var charts = H.charts, + each = H.each, + extend = H.extend, + map = H.map, + noop = H.noop, + pick = H.pick, + Pointer = H.Pointer; + +/* Support for touch devices */ +extend(Pointer.prototype, /** @lends Pointer.prototype */ { + + /** + * Run translation operations + */ + pinchTranslate: function ( + pinchDown, + touches, + transform, + selectionMarker, + clip, + lastValidTouch + ) { + if (this.zoomHor) { + this.pinchTranslateDirection( + true, + pinchDown, + touches, + transform, + selectionMarker, + clip, + lastValidTouch + ); + } + if (this.zoomVert) { + this.pinchTranslateDirection( + false, + pinchDown, + touches, + transform, + selectionMarker, + clip, + lastValidTouch + ); + } + }, + + /** + * Run translation operations for each direction (horizontal and vertical) + * independently + */ + pinchTranslateDirection: function (horiz, pinchDown, touches, transform, + selectionMarker, clip, lastValidTouch, forcedScale) { + var chart = this.chart, + xy = horiz ? 'x' : 'y', + XY = horiz ? 'X' : 'Y', + sChartXY = 'chart' + XY, + wh = horiz ? 'width' : 'height', + plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')], + selectionWH, + selectionXY, + clipXY, + scale = forcedScale || 1, + inverted = chart.inverted, + bounds = chart.bounds[horiz ? 'h' : 'v'], + singleTouch = pinchDown.length === 1, + touch0Start = pinchDown[0][sChartXY], + touch0Now = touches[0][sChartXY], + touch1Start = !singleTouch && pinchDown[1][sChartXY], + touch1Now = !singleTouch && touches[1][sChartXY], + outOfBounds, + transformScale, + scaleKey, + setScale = function () { + // Don't zoom if fingers are too close on this axis + if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) { + scale = forcedScale || + Math.abs(touch0Now - touch1Now) / + Math.abs(touch0Start - touch1Start); + } + + clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start; + selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / + scale; + }; + + // Set the scale, first pass + setScale(); + + // The clip position (x or y) is altered if out of bounds, the selection + // position is not + selectionXY = clipXY; + + // Out of bounds + if (selectionXY < bounds.min) { + selectionXY = bounds.min; + outOfBounds = true; + } else if (selectionXY + selectionWH > bounds.max) { + selectionXY = bounds.max - selectionWH; + outOfBounds = true; + } + + // Is the chart dragged off its bounds, determined by dataMin and + // dataMax? + if (outOfBounds) { + + // Modify the touchNow position in order to create an elastic drag + // movement. This indicates to the user that the chart is responsive + // but can't be dragged further. + touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]); + if (!singleTouch) { + touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]); + } + + // Set the scale, second pass to adapt to the modified touchNow + // positions + setScale(); + + } else { + lastValidTouch[xy] = [touch0Now, touch1Now]; + } + + // Set geometry for clipping, selection and transformation + if (!inverted) { + clip[xy] = clipXY - plotLeftTop; + clip[wh] = selectionWH; + } + scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY; + transformScale = inverted ? 1 / scale : scale; + + selectionMarker[wh] = selectionWH; + selectionMarker[xy] = selectionXY; + transform[scaleKey] = scale; + transform['translate' + XY] = (transformScale * plotLeftTop) + + (touch0Now - (transformScale * touch0Start)); + }, + + /** + * Handle touch events with two touches + */ + pinch: function (e) { + + var self = this, + chart = self.chart, + pinchDown = self.pinchDown, + touches = e.touches, + touchesLength = touches.length, + lastValidTouch = self.lastValidTouch, + hasZoom = self.hasZoom, + selectionMarker = self.selectionMarker, + transform = {}, + fireClickEvent = touchesLength === 1 && + ((self.inClass(e.target, 'highcharts-tracker') && + chart.runTrackerClick) || self.runChartClick), + clip = {}; + + // Don't initiate panning until the user has pinched. This prevents us + // from blocking page scrolling as users scroll down a long page + // (#4210). + if (touchesLength > 1) { + self.initiated = true; + } + + // On touch devices, only proceed to trigger click if a handler is + // defined + if (hasZoom && self.initiated && !fireClickEvent) { + e.preventDefault(); + } + + // Normalize each touch + map(touches, function (e) { + return self.normalize(e); + }); + + // Register the touch start position + if (e.type === 'touchstart') { + each(touches, function (e, i) { + pinchDown[i] = { chartX: e.chartX, chartY: e.chartY }; + }); + lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] && + pinchDown[1].chartX]; + lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] && + pinchDown[1].chartY]; + + // Identify the data bounds in pixels + each(chart.axes, function (axis) { + if (axis.zoomEnabled) { + var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], + minPixelPadding = axis.minPixelPadding, + min = axis.toPixels( + pick(axis.options.min, axis.dataMin) + ), + max = axis.toPixels( + pick(axis.options.max, axis.dataMax) + ), + absMin = Math.min(min, max), + absMax = Math.max(min, max); + + // Store the bounds for use in the touchmove handler + bounds.min = Math.min(axis.pos, absMin - minPixelPadding); + bounds.max = Math.max( + axis.pos + axis.len, + absMax + minPixelPadding + ); + } + }); + self.res = true; // reset on next move + + // Optionally move the tooltip on touchmove + } else if (self.followTouchMove && touchesLength === 1) { + this.runPointActions(self.normalize(e)); + + // Event type is touchmove, handle panning and pinching + } else if (pinchDown.length) { // can be 0 when releasing, if touchend + // fires first + + + // Set the marker + if (!selectionMarker) { + self.selectionMarker = selectionMarker = extend({ + destroy: noop, + touch: true + }, chart.plotBox); + } + + self.pinchTranslate( + pinchDown, + touches, + transform, + selectionMarker, + clip, + lastValidTouch + ); + + self.hasPinched = hasZoom; + + // Scale and translate the groups to provide visual feedback during + // pinching + self.scaleGroups(transform, clip); + + if (self.res) { + self.res = false; + this.reset(false, 0); + } + } + }, + + /** + * General touch handler shared by touchstart and touchmove. + */ + touch: function (e, start) { + var chart = this.chart, + hasMoved, + pinchDown, + isInside; + + if (chart.index !== H.hoverChartIndex) { + this.onContainerMouseLeave({ relatedTarget: true }); + } + H.hoverChartIndex = chart.index; + + if (e.touches.length === 1) { + + e = this.normalize(e); + + isInside = chart.isInsidePlot( + e.chartX - chart.plotLeft, + e.chartY - chart.plotTop + ); + if (isInside && !chart.openMenu) { + + // Run mouse events and display tooltip etc + if (start) { + this.runPointActions(e); + } + + // Android fires touchmove events after the touchstart even if + // the finger hasn't moved, or moved only a pixel or two. In iOS + // however, the touchmove doesn't fire unless the finger moves + // more than ~4px. So we emulate this behaviour in Android by + // checking how much it moved, and cancelling on small + // distances. #3450. + if (e.type === 'touchmove') { + pinchDown = this.pinchDown; + hasMoved = pinchDown[0] ? Math.sqrt( // #5266 + Math.pow(pinchDown[0].chartX - e.chartX, 2) + + Math.pow(pinchDown[0].chartY - e.chartY, 2) + ) >= 4 : false; + } + + if (pick(hasMoved, true)) { + this.pinch(e); + } + + } else if (start) { + // Hide the tooltip on touching outside the plot area (#1203) + this.reset(); + } + + } else if (e.touches.length === 2) { + this.pinch(e); + } + }, + + onContainerTouchStart: function (e) { + this.zoomOption(e); + this.touch(e, true); + }, + + onContainerTouchMove: function (e) { + this.touch(e); + }, + + onDocumentTouchEnd: function (e) { + if (charts[H.hoverChartIndex]) { + charts[H.hoverChartIndex].pointer.drop(e); + } + } + +}); + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var addEvent = H.addEvent, + charts = H.charts, + css = H.css, + doc = H.doc, + extend = H.extend, + hasTouch = H.hasTouch, + noop = H.noop, + Pointer = H.Pointer, + removeEvent = H.removeEvent, + win = H.win, + wrap = H.wrap; + +if (!hasTouch && (win.PointerEvent || win.MSPointerEvent)) { + + // The touches object keeps track of the points being touched at all times + var touches = {}, + hasPointerEvent = !!win.PointerEvent, + getWebkitTouches = function () { + var fake = []; + fake.item = function (i) { + return this[i]; + }; + H.objectEach(touches, function (touch) { + fake.push({ + pageX: touch.pageX, + pageY: touch.pageY, + target: touch.target + }); + }); + return fake; + }, + translateMSPointer = function (e, method, wktype, func) { + var p; + if ((e.pointerType === 'touch' || e.pointerType === e.MSPOINTER_TYPE_TOUCH) && charts[H.hoverChartIndex]) { + func(e); + p = charts[H.hoverChartIndex].pointer; + p[method]({ + type: wktype, + target: e.currentTarget, + preventDefault: noop, + touches: getWebkitTouches() + }); + } + }; + + /** + * Extend the Pointer prototype with methods for each event handler and more + */ + extend(Pointer.prototype, /** @lends Pointer.prototype */ { + onContainerPointerDown: function (e) { + translateMSPointer(e, 'onContainerTouchStart', 'touchstart', function (e) { + touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY, target: e.currentTarget }; + }); + }, + onContainerPointerMove: function (e) { + translateMSPointer(e, 'onContainerTouchMove', 'touchmove', function (e) { + touches[e.pointerId] = { pageX: e.pageX, pageY: e.pageY }; + if (!touches[e.pointerId].target) { + touches[e.pointerId].target = e.currentTarget; + } + }); + }, + onDocumentPointerUp: function (e) { + translateMSPointer(e, 'onDocumentTouchEnd', 'touchend', function (e) { + delete touches[e.pointerId]; + }); + }, + + /** + * Add or remove the MS Pointer specific events + */ + batchMSEvents: function (fn) { + fn(this.chart.container, hasPointerEvent ? 'pointerdown' : 'MSPointerDown', this.onContainerPointerDown); + fn(this.chart.container, hasPointerEvent ? 'pointermove' : 'MSPointerMove', this.onContainerPointerMove); + fn(doc, hasPointerEvent ? 'pointerup' : 'MSPointerUp', this.onDocumentPointerUp); + } + }); + + // Disable default IE actions for pinch and such on chart element + wrap(Pointer.prototype, 'init', function (proceed, chart, options) { + proceed.call(this, chart, options); + if (this.hasZoom) { // #4014 + css(chart.container, { + '-ms-touch-action': 'none', + 'touch-action': 'none' + }); + } + }); + + // Add IE specific touch events to chart + wrap(Pointer.prototype, 'setDOMEvents', function (proceed) { + proceed.apply(this); + if (this.hasZoom || this.followTouchMove) { + this.batchMSEvents(addEvent); + } + }); + // Destroy MS events also + wrap(Pointer.prototype, 'destroy', function (proceed) { + this.batchMSEvents(removeEvent); + proceed.call(this); + }); +} + +}(Highcharts)); +(function (Highcharts) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var H = Highcharts, + + addEvent = H.addEvent, + css = H.css, + discardElement = H.discardElement, + defined = H.defined, + each = H.each, + isFirefox = H.isFirefox, + marginNames = H.marginNames, + merge = H.merge, + pick = H.pick, + setAnimation = H.setAnimation, + stableSort = H.stableSort, + win = H.win, + wrap = H.wrap; + +/** + * The overview of the chart's series. The legend object is instanciated + * internally in the chart constructor, and available from `chart.legend`. Each + * chart has only one legend. + * + * @class + */ +Highcharts.Legend = function (chart, options) { + this.init(chart, options); +}; + +Highcharts.Legend.prototype = { + + /** + * Initialize the legend. + * + * @private + */ + init: function (chart, options) { + + this.chart = chart; + + this.setOptions(options); + + if (options.enabled) { + + // Render it + this.render(); + + // move checkboxes + addEvent(this.chart, 'endResize', function () { + this.legend.positionCheckboxes(); + }); + } + }, + + setOptions: function (options) { + + var padding = pick(options.padding, 8); + + this.options = options; + + + this.itemStyle = options.itemStyle; + this.itemHiddenStyle = merge(this.itemStyle, options.itemHiddenStyle); + + this.itemMarginTop = options.itemMarginTop || 0; + this.padding = padding; + this.initialItemY = padding - 5; // 5 is pixels above the text + this.maxItemWidth = 0; + this.itemHeight = 0; + this.symbolWidth = pick(options.symbolWidth, 16); + this.pages = []; + + }, + + /** + * Update the legend with new options. Equivalent to running `chart.update` + * with a legend configuration option. + * @param {LegendOptions} options + * Legend options. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart. + * + * @sample highcharts/legend/legend-update/ + * Legend update + */ + update: function (options, redraw) { + var chart = this.chart; + + this.setOptions(merge(true, this.options, options)); + this.destroy(); + chart.isDirtyLegend = chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Set the colors for the legend item. + * + * @private + * @param {Series|Point} item + * A Series or Point instance + * @param {Boolean} visible + * Dimmed or colored + */ + colorizeItem: function (item, visible) { + item.legendGroup[visible ? 'removeClass' : 'addClass']( + 'highcharts-legend-item-hidden' + ); + + + var legend = this, + options = legend.options, + legendItem = item.legendItem, + legendLine = item.legendLine, + legendSymbol = item.legendSymbol, + hiddenColor = legend.itemHiddenStyle.color, + textColor = visible ? options.itemStyle.color : hiddenColor, + symbolColor = visible ? (item.color || hiddenColor) : hiddenColor, + markerOptions = item.options && item.options.marker, + symbolAttr = { fill: symbolColor }; + + if (legendItem) { + legendItem.css({ + fill: textColor, + color: textColor // #1553, oldIE + }); + } + if (legendLine) { + legendLine.attr({ stroke: symbolColor }); + } + + if (legendSymbol) { + + // Apply marker options + if (markerOptions && legendSymbol.isMarker) { // #585 + symbolAttr = item.pointAttribs(); + if (!visible) { + symbolAttr.stroke = symbolAttr.fill = hiddenColor; // #6769 + } + } + + legendSymbol.attr(symbolAttr); + } + + }, + + /** + * Position the legend item. + * + * @private + * @param {Series|Point} item + * The item to position + */ + positionItem: function (item) { + var legend = this, + options = legend.options, + symbolPadding = options.symbolPadding, + ltr = !options.rtl, + legendItemPos = item._legendItemPos, + itemX = legendItemPos[0], + itemY = legendItemPos[1], + checkbox = item.checkbox, + legendGroup = item.legendGroup; + + if (legendGroup && legendGroup.element) { + legendGroup.translate( + ltr ? + itemX : + legend.legendWidth - itemX - 2 * symbolPadding - 4, + itemY + ); + } + + if (checkbox) { + checkbox.x = itemX; + checkbox.y = itemY; + } + }, + + /** + * Destroy a single legend item, used internally on removing series items. + * + * @param {Series|Point} item + * The item to remove + */ + destroyItem: function (item) { + var checkbox = item.checkbox; + + // destroy SVG elements + each( + ['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'], + function (key) { + if (item[key]) { + item[key] = item[key].destroy(); + } + } + ); + + if (checkbox) { + discardElement(item.checkbox); + } + }, + + /** + * Destroy the legend. Used internally. To reflow objects, `chart.redraw` + * must be called after destruction. + */ + destroy: function () { + function destroyItems(key) { + if (this[key]) { + this[key] = this[key].destroy(); + } + } + + // Destroy items + each(this.getAllItems(), function (item) { + each(['legendItem', 'legendGroup'], destroyItems, item); + }); + + // Destroy legend elements + each([ + 'clipRect', + 'up', + 'down', + 'pager', + 'nav', + 'box', + 'title', + 'group' + ], destroyItems, this); + this.display = null; // Reset in .render on update. + }, + + /** + * Position the checkboxes after the width is determined. + * + * @private + */ + positionCheckboxes: function () { + var alignAttr = this.group && this.group.alignAttr, + translateY, + clipHeight = this.clipHeight || this.legendHeight, + titleHeight = this.titleHeight; + + if (alignAttr) { + translateY = alignAttr.translateY; + each(this.allItems, function (item) { + var checkbox = item.checkbox, + top; + + if (checkbox) { + top = translateY + titleHeight + checkbox.y + + (this.scrollOffset || 0) + 3; + css(checkbox, { + left: (alignAttr.translateX + item.checkboxOffset + + checkbox.x - 20) + 'px', + top: top + 'px', + display: top > translateY - 6 && top < translateY + + clipHeight - 6 ? '' : 'none' + }); + } + }, this); + } + }, + + /** + * Render the legend title on top of the legend. + * + * @private + */ + renderTitle: function () { + var options = this.options, + padding = this.padding, + titleOptions = options.title, + titleHeight = 0, + bBox; + + if (titleOptions.text) { + if (!this.title) { + this.title = this.chart.renderer.label( + titleOptions.text, + padding - 3, + padding - 4, + null, + null, + null, + options.useHTML, + null, + 'legend-title' + ) + .attr({ zIndex: 1 }) + + .css(titleOptions.style) + + .add(this.group); + } + bBox = this.title.getBBox(); + titleHeight = bBox.height; + this.offsetWidth = bBox.width; // #1717 + this.contentGroup.attr({ translateY: titleHeight }); + } + this.titleHeight = titleHeight; + }, + + /** + * Set the legend item text. + * + * @param {Series|Point} item + * The item for which to update the text in the legend. + */ + setText: function (item) { + var options = this.options; + item.legendItem.attr({ + text: options.labelFormat ? + H.format(options.labelFormat, item, this.chart.time) : + options.labelFormatter.call(item) + }); + }, + + /** + * Render a single specific legend item. Called internally from the `render` + * function. + * + * @private + * @param {Series|Point} item + * The item to render. + */ + renderItem: function (item) { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + options = legend.options, + horizontal = options.layout === 'horizontal', + symbolWidth = legend.symbolWidth, + symbolPadding = options.symbolPadding, + + itemStyle = legend.itemStyle, + itemHiddenStyle = legend.itemHiddenStyle, + + padding = legend.padding, + itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, + ltr = !options.rtl, + itemHeight, + widthOption = options.width, + itemMarginBottom = options.itemMarginBottom || 0, + itemMarginTop = legend.itemMarginTop, + bBox, + itemWidth, + li = item.legendItem, + isSeries = !item.series, + series = !isSeries && item.series.drawLegendSymbol ? + item.series : + item, + seriesOptions = series.options, + showCheckbox = legend.createCheckboxForItem && + seriesOptions && + seriesOptions.showCheckbox, + // full width minus text width + itemExtraWidth = symbolWidth + symbolPadding + itemDistance + + (showCheckbox ? 20 : 0), + useHTML = options.useHTML, + fontSize = 12, + itemClassName = item.options.className; + + if (!li) { // generate it once, later move it + + // Generate the group box, a group to hold the symbol and text. Text + // is to be appended in Legend class. + item.legendGroup = renderer.g('legend-item') + .addClass( + 'highcharts-' + series.type + '-series ' + + 'highcharts-color-' + item.colorIndex + + (itemClassName ? ' ' + itemClassName : '') + + (isSeries ? ' highcharts-series-' + item.index : '') + ) + .attr({ zIndex: 1 }) + .add(legend.scrollGroup); + + // Generate the list item text and add it to the group + item.legendItem = li = renderer.text( + '', + ltr ? symbolWidth + symbolPadding : -symbolPadding, + legend.baseline || 0, + useHTML + ) + + // merge to prevent modifying original (#1021) + .css(merge(item.visible ? itemStyle : itemHiddenStyle)) + + .attr({ + align: ltr ? 'left' : 'right', + zIndex: 2 + }) + .add(item.legendGroup); + + // Get the baseline for the first item - the font size is equal for + // all + if (!legend.baseline) { + + fontSize = itemStyle.fontSize; + + legend.fontMetrics = renderer.fontMetrics( + fontSize, + li + ); + legend.baseline = legend.fontMetrics.f + 3 + itemMarginTop; + li.attr('y', legend.baseline); + } + + // Draw the legend symbol inside the group box + legend.symbolHeight = options.symbolHeight || legend.fontMetrics.f; + series.drawLegendSymbol(legend, item); + + if (legend.setItemEvents) { + legend.setItemEvents(item, li, useHTML); + } + + // add the HTML checkbox on top + if (showCheckbox) { + legend.createCheckboxForItem(item); + } + } + + // Colorize the items + legend.colorizeItem(item, item.visible); + + // Take care of max width and text overflow (#6659) + + if (!itemStyle.width) { + + li.css({ + width: ( + options.itemWidth || + options.width || + chart.spacingBox.width + ) - itemExtraWidth + }); + + } + + + // Always update the text + legend.setText(item); + + // calculate the positions for the next line + bBox = li.getBBox(); + + itemWidth = item.checkboxOffset = + options.itemWidth || + item.legendItemWidth || + bBox.width + itemExtraWidth; + legend.itemHeight = itemHeight = Math.round( + item.legendItemHeight || bBox.height || legend.symbolHeight + ); + + // If the item exceeds the width, start a new line + if ( + horizontal && + legend.itemX - padding + itemWidth > ( + widthOption || ( + chart.spacingBox.width - 2 * padding - options.x + ) + ) + ) { + legend.itemX = padding; + legend.itemY += itemMarginTop + legend.lastLineHeight + + itemMarginBottom; + legend.lastLineHeight = 0; // reset for next line (#915, #3976) + } + + // If the item exceeds the height, start a new column + /* + if (!horizontal && legend.itemY + options.y + + itemHeight > chart.chartHeight - spacingTop - spacingBottom) { + legend.itemY = legend.initialItemY; + legend.itemX += legend.maxItemWidth; + legend.maxItemWidth = 0; + } + */ + + // Set the edge positions + legend.maxItemWidth = Math.max(legend.maxItemWidth, itemWidth); + legend.lastItemY = itemMarginTop + legend.itemY + itemMarginBottom; + legend.lastLineHeight = Math.max( // #915 + itemHeight, + legend.lastLineHeight + ); + + // cache the position of the newly generated or reordered items + item._legendItemPos = [legend.itemX, legend.itemY]; + + // advance + if (horizontal) { + legend.itemX += itemWidth; + + } else { + legend.itemY += itemMarginTop + itemHeight + itemMarginBottom; + legend.lastLineHeight = itemHeight; + } + + // the width of the widest item + legend.offsetWidth = widthOption || Math.max( + ( + horizontal ? legend.itemX - padding - (item.checkbox ? + // decrease by itemDistance only when no checkbox #4853 + 0 : + itemDistance + ) : itemWidth + ) + padding, + legend.offsetWidth + ); + }, + + /** + * Get all items, which is one item per series for most series and one + * item per point for pie series and its derivatives. + * + * @return {Array.} + * The current items in the legend. + */ + getAllItems: function () { + var allItems = []; + each(this.chart.series, function (series) { + var seriesOptions = series && series.options; + + // Handle showInLegend. If the series is linked to another series, + // defaults to false. + if (series && pick( + seriesOptions.showInLegend, + !defined(seriesOptions.linkedTo) ? undefined : false, true + )) { + + // Use points or series for the legend item depending on + // legendType + allItems = allItems.concat( + series.legendItems || + ( + seriesOptions.legendType === 'point' ? + series.data : + series + ) + ); + } + }); + return allItems; + }, + + /** + * Get a short, three letter string reflecting the alignment and layout. + * + * @private + * @return {String} The alignment, empty string if floating + */ + getAlignment: function () { + var options = this.options; + + // Use the first letter of each alignment option in order to detect + // the side. (#4189 - use charAt(x) notation instead of [x] for IE7) + return options.floating ? '' : ( + options.align.charAt(0) + + options.verticalAlign.charAt(0) + + options.layout.charAt(0) + ); + }, + + /** + * Adjust the chart margins by reserving space for the legend on only one + * side of the chart. If the position is set to a corner, top or bottom is + * reserved for horizontal legends and left or right for vertical ones. + * + * @private + */ + adjustMargins: function (margin, spacing) { + var chart = this.chart, + options = this.options, + alignment = this.getAlignment(); + + if (alignment) { + + each([ + /(lth|ct|rth)/, + /(rtv|rm|rbv)/, + /(rbh|cb|lbh)/, + /(lbv|lm|ltv)/ + ], function (alignments, side) { + if (alignments.test(alignment) && !defined(margin[side])) { + + // Now we have detected on which side of the chart we should + // reserve space for the legend + chart[marginNames[side]] = Math.max( + chart[marginNames[side]], + ( + chart.legend[ + (side + 1) % 2 ? 'legendHeight' : 'legendWidth' + ] + + [1, -1, -1, 1][side] * options[ + (side % 2) ? 'x' : 'y' + ] + + pick(options.margin, 12) + + spacing[side] + + ( + side === 0 ? + chart.titleOffset + + chart.options.title.margin : + 0 + ) // #7428 + ) + ); + } + }); + } + }, + + /** + * Render the legend. This method can be called both before and after + * `chart.render`. If called after, it will only rearrange items instead + * of creating new ones. Called internally on initial render and after + * redraws. + */ + render: function () { + var legend = this, + chart = legend.chart, + renderer = chart.renderer, + legendGroup = legend.group, + allItems, + display, + legendWidth, + legendHeight, + box = legend.box, + options = legend.options, + padding = legend.padding, + alignTo; + + legend.itemX = padding; + legend.itemY = legend.initialItemY; + legend.offsetWidth = 0; + legend.lastItemY = 0; + + if (!legendGroup) { + legend.group = legendGroup = renderer.g('legend') + .attr({ zIndex: 7 }) + .add(); + legend.contentGroup = renderer.g() + .attr({ zIndex: 1 }) // above background + .add(legendGroup); + legend.scrollGroup = renderer.g() + .add(legend.contentGroup); + } + + legend.renderTitle(); + + // add each series or point + allItems = legend.getAllItems(); + + // sort by legendIndex + stableSort(allItems, function (a, b) { + return ((a.options && a.options.legendIndex) || 0) - + ((b.options && b.options.legendIndex) || 0); + }); + + // reversed legend + if (options.reversed) { + allItems.reverse(); + } + + legend.allItems = allItems; + legend.display = display = !!allItems.length; + + // render the items + legend.lastLineHeight = 0; + each(allItems, function (item) { + legend.renderItem(item); + }); + + // Get the box + legendWidth = (options.width || legend.offsetWidth) + padding; + legendHeight = legend.lastItemY + legend.lastLineHeight + + legend.titleHeight; + legendHeight = legend.handleOverflow(legendHeight); + legendHeight += padding; + + // Draw the border and/or background + if (!box) { + legend.box = box = renderer.rect() + .addClass('highcharts-legend-box') + .attr({ + r: options.borderRadius + }) + .add(legendGroup); + box.isNew = true; + } + + + // Presentational + box + .attr({ + stroke: options.borderColor, + 'stroke-width': options.borderWidth || 0, + fill: options.backgroundColor || 'none' + }) + .shadow(options.shadow); + + + if (legendWidth > 0 && legendHeight > 0) { + box[box.isNew ? 'attr' : 'animate']( + box.crisp.call({}, { // #7260 + x: 0, + y: 0, + width: legendWidth, + height: legendHeight + }, box.strokeWidth()) + ); + box.isNew = false; + } + + // hide the border if no items + box[display ? 'show' : 'hide'](); + + + + legend.legendWidth = legendWidth; + legend.legendHeight = legendHeight; + + // Now that the legend width and height are established, put the items + // in the final position + each(allItems, function (item) { + legend.positionItem(item); + }); + + if (display) { + // If aligning to the top and the layout is horizontal, adjust for + // the title (#7428) + alignTo = chart.spacingBox; + if (/(lth|ct|rth)/.test(legend.getAlignment())) { + alignTo = merge(alignTo, { + y: alignTo.y + chart.titleOffset + + chart.options.title.margin + }); + } + + legendGroup.align(merge(options, { + width: legendWidth, + height: legendHeight + }), true, alignTo); + } + + if (!chart.isResizing) { + this.positionCheckboxes(); + } + }, + + /** + * Set up the overflow handling by adding navigation with up and down arrows + * below the legend. + * + * @private + */ + handleOverflow: function (legendHeight) { + var legend = this, + chart = this.chart, + renderer = chart.renderer, + options = this.options, + optionsY = options.y, + alignTop = options.verticalAlign === 'top', + padding = this.padding, + spaceHeight = chart.spacingBox.height + + (alignTop ? -optionsY : optionsY) - padding, + maxHeight = options.maxHeight, + clipHeight, + clipRect = this.clipRect, + navOptions = options.navigation, + animation = pick(navOptions.animation, true), + arrowSize = navOptions.arrowSize || 12, + nav = this.nav, + pages = this.pages, + lastY, + allItems = this.allItems, + clipToHeight = function (height) { + if (typeof height === 'number') { + clipRect.attr({ + height: height + }); + } else if (clipRect) { // Reset (#5912) + legend.clipRect = clipRect.destroy(); + legend.contentGroup.clip(); + } + + // useHTML + if (legend.contentGroup.div) { + legend.contentGroup.div.style.clip = height ? + 'rect(' + padding + 'px,9999px,' + + (padding + height) + 'px,0)' : + 'auto'; + } + }; + + + // Adjust the height + if ( + options.layout === 'horizontal' && + options.verticalAlign !== 'middle' && + !options.floating + ) { + spaceHeight /= 2; + } + if (maxHeight) { + spaceHeight = Math.min(spaceHeight, maxHeight); + } + + // Reset the legend height and adjust the clipping rectangle + pages.length = 0; + if (legendHeight > spaceHeight && navOptions.enabled !== false) { + + this.clipHeight = clipHeight = + Math.max(spaceHeight - 20 - this.titleHeight - padding, 0); + this.currentPage = pick(this.currentPage, 1); + this.fullHeight = legendHeight; + + // Fill pages with Y positions so that the top of each a legend item + // defines the scroll top for each page (#2098) + each(allItems, function (item, i) { + var y = item._legendItemPos[1], + h = Math.round(item.legendItem.getBBox().height), + len = pages.length; + + if (!len || (y - pages[len - 1] > clipHeight && + (lastY || y) !== pages[len - 1])) { + pages.push(lastY || y); + len++; + } + + // Keep track of which page each item is on + item.pageIx = len - 1; + if (lastY) { + allItems[i - 1].pageIx = len - 1; + } + + if (i === allItems.length - 1 && + y + h - pages[len - 1] > clipHeight) { + pages.push(y); + item.pageIx = len; + } + if (y !== lastY) { + lastY = y; + } + }); + + // Only apply clipping if needed. Clipping causes blurred legend in + // PDF export (#1787) + if (!clipRect) { + clipRect = legend.clipRect = + renderer.clipRect(0, padding, 9999, 0); + legend.contentGroup.clip(clipRect); + } + + clipToHeight(clipHeight); + + // Add navigation elements + if (!nav) { + this.nav = nav = renderer.g() + .attr({ zIndex: 1 }) + .add(this.group); + + this.up = renderer + .symbol( + 'triangle', + 0, + 0, + arrowSize, + arrowSize + ) + .on('click', function () { + legend.scroll(-1, animation); + }) + .add(nav); + + this.pager = renderer.text('', 15, 10) + .addClass('highcharts-legend-navigation') + + .css(navOptions.style) + + .add(nav); + + this.down = renderer + .symbol( + 'triangle-down', + 0, + 0, + arrowSize, + arrowSize + ) + .on('click', function () { + legend.scroll(1, animation); + }) + .add(nav); + } + + // Set initial position + legend.scroll(0); + + legendHeight = spaceHeight; + + // Reset + } else if (nav) { + clipToHeight(); + this.nav = nav.destroy(); // #6322 + this.scrollGroup.attr({ + translateY: 1 + }); + this.clipHeight = 0; // #1379 + } + + return legendHeight; + }, + + /** + * Scroll the legend by a number of pages. + * @param {Number} scrollBy + * The number of pages to scroll. + * @param {AnimationOptions} animation + * Whether and how to apply animation. + */ + scroll: function (scrollBy, animation) { + var pages = this.pages, + pageCount = pages.length, + currentPage = this.currentPage + scrollBy, + clipHeight = this.clipHeight, + navOptions = this.options.navigation, + pager = this.pager, + padding = this.padding; + + // When resizing while looking at the last page + if (currentPage > pageCount) { + currentPage = pageCount; + } + + if (currentPage > 0) { + + if (animation !== undefined) { + setAnimation(animation, this.chart); + } + + this.nav.attr({ + translateX: padding, + translateY: clipHeight + this.padding + 7 + this.titleHeight, + visibility: 'visible' + }); + this.up.attr({ + 'class': currentPage === 1 ? + 'highcharts-legend-nav-inactive' : + 'highcharts-legend-nav-active' + }); + pager.attr({ + text: currentPage + '/' + pageCount + }); + this.down.attr({ + 'x': 18 + this.pager.getBBox().width, // adjust to text width + 'class': currentPage === pageCount ? + 'highcharts-legend-nav-inactive' : + 'highcharts-legend-nav-active' + }); + + + this.up + .attr({ + fill: currentPage === 1 ? + navOptions.inactiveColor : + navOptions.activeColor + }) + .css({ + cursor: currentPage === 1 ? 'default' : 'pointer' + }); + this.down + .attr({ + fill: currentPage === pageCount ? + navOptions.inactiveColor : + navOptions.activeColor + }) + .css({ + cursor: currentPage === pageCount ? 'default' : 'pointer' + }); + + + this.scrollOffset = -pages[currentPage - 1] + this.initialItemY; + + this.scrollGroup.animate({ + translateY: this.scrollOffset + }); + + this.currentPage = currentPage; + this.positionCheckboxes(); + } + + } + +}; + +/* + * LegendSymbolMixin + */ + +H.LegendSymbolMixin = { + + /** + * Get the series' symbol in the legend + * + * @param {Object} legend The legend object + * @param {Object} item The series (this) or point + */ + drawRectangle: function (legend, item) { + var options = legend.options, + symbolHeight = legend.symbolHeight, + square = options.squareSymbol, + symbolWidth = square ? symbolHeight : legend.symbolWidth; + + item.legendSymbol = this.chart.renderer.rect( + square ? (legend.symbolWidth - symbolHeight) / 2 : 0, + legend.baseline - symbolHeight + 1, // #3988 + symbolWidth, + symbolHeight, + pick(legend.options.symbolRadius, symbolHeight / 2) + ) + .addClass('highcharts-point') + .attr({ + zIndex: 3 + }).add(item.legendGroup); + + }, + + /** + * Get the series' symbol in the legend. This method should be overridable + * to create custom symbols through + * Highcharts.seriesTypes[type].prototype.drawLegendSymbols. + * + * @param {Object} legend The legend object + */ + drawLineMarker: function (legend) { + + var options = this.options, + markerOptions = options.marker, + radius, + legendSymbol, + symbolWidth = legend.symbolWidth, + symbolHeight = legend.symbolHeight, + generalRadius = symbolHeight / 2, + renderer = this.chart.renderer, + legendItemGroup = this.legendGroup, + verticalCenter = legend.baseline - + Math.round(legend.fontMetrics.b * 0.3), + attr = {}; + + // Draw the line + + attr = { + 'stroke-width': options.lineWidth || 0 + }; + if (options.dashStyle) { + attr.dashstyle = options.dashStyle; + } + + + this.legendLine = renderer.path([ + 'M', + 0, + verticalCenter, + 'L', + symbolWidth, + verticalCenter + ]) + .addClass('highcharts-graph') + .attr(attr) + .add(legendItemGroup); + + // Draw the marker + if (markerOptions && markerOptions.enabled !== false) { + + // Do not allow the marker to be larger than the symbolHeight + radius = Math.min( + pick(markerOptions.radius, generalRadius), + generalRadius + ); + + // Restrict symbol markers size + if (this.symbol.indexOf('url') === 0) { + markerOptions = merge(markerOptions, { + width: symbolHeight, + height: symbolHeight + }); + radius = 0; + } + + this.legendSymbol = legendSymbol = renderer.symbol( + this.symbol, + (symbolWidth / 2) - radius, + verticalCenter - radius, + 2 * radius, + 2 * radius, + markerOptions + ) + .addClass('highcharts-point') + .add(legendItemGroup); + legendSymbol.isMarker = true; + } + } +}; + +// Workaround for #2030, horizontal legend items not displaying in IE11 Preview, +// and for #2580, a similar drawing flaw in Firefox 26. +// Explore if there's a general cause for this. The problem may be related +// to nested group elements, as the legend item texts are within 4 group +// elements. +if (/Trident\/7\.0/.test(win.navigator.userAgent) || isFirefox) { + wrap(Highcharts.Legend.prototype, 'positionItem', function (proceed, item) { + var legend = this, + // If chart destroyed in sync, this is undefined (#2030) + runPositionItem = function () { + if (item._legendItemPos) { + proceed.call(legend, item); + } + }; + + // Do it now, for export and to get checkbox placement + runPositionItem(); + + // Do it after to work around the core issue + setTimeout(runPositionItem); + }); +} + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var addEvent = H.addEvent, + animate = H.animate, + animObject = H.animObject, + attr = H.attr, + doc = H.doc, + Axis = H.Axis, // @todo add as requirement + createElement = H.createElement, + defaultOptions = H.defaultOptions, + discardElement = H.discardElement, + charts = H.charts, + css = H.css, + defined = H.defined, + each = H.each, + extend = H.extend, + find = H.find, + fireEvent = H.fireEvent, + grep = H.grep, + isNumber = H.isNumber, + isObject = H.isObject, + isString = H.isString, + Legend = H.Legend, // @todo add as requirement + marginNames = H.marginNames, + merge = H.merge, + objectEach = H.objectEach, + Pointer = H.Pointer, // @todo add as requirement + pick = H.pick, + pInt = H.pInt, + removeEvent = H.removeEvent, + seriesTypes = H.seriesTypes, + splat = H.splat, + svg = H.svg, + syncTimeout = H.syncTimeout, + win = H.win; +/** + * The Chart class. The recommended constructor is {@link Highcharts#chart}. + * @class Highcharts.Chart + * @param {String|HTMLDOMElement} renderTo + * The DOM element to render to, or its id. + * @param {Options} options + * The chart options structure. + * @param {Function} [callback] + * Function to run when the chart has loaded and and all external images + * are loaded. Defining a {@link + * https://api.highcharts.com/highcharts/chart.events.load|chart.event.load} + * handler is equivalent. + * + * @example + * var chart = Highcharts.chart('container', { + * title: { + * text: 'My chart' + * }, + * series: [{ + * data: [1, 3, 2, 4] + * }] + * }) + */ +var Chart = H.Chart = function () { + this.getArgs.apply(this, arguments); +}; + +/** + * Factory function for basic charts. + * + * @function #chart + * @memberOf Highcharts + * @param {String|HTMLDOMElement} renderTo - The DOM element to render to, or + * its id. + * @param {Options} options - The chart options structure. + * @param {Function} [callback] - Function to run when the chart has loaded and + * and all external images are loaded. Defining a {@link + * https://api.highcharts.com/highcharts/chart.events.load|chart.event.load} + * handler is equivalent. + * @return {Highcharts.Chart} - Returns the Chart object. + * + * @example + * // Render a chart in to div#container + * var chart = Highcharts.chart('container', { + * title: { + * text: 'My chart' + * }, + * series: [{ + * data: [1, 3, 2, 4] + * }] + * }); + */ +H.chart = function (a, b, c) { + return new Chart(a, b, c); +}; + +extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { + + // Hook for adding callbacks in modules + callbacks: [], + + /** + * Handle the arguments passed to the constructor. + * + * @private + * @returns {Array} Arguments without renderTo + */ + getArgs: function () { + var args = [].slice.call(arguments); + + // Remove the optional first argument, renderTo, and + // set it on this. + if (isString(args[0]) || args[0].nodeName) { + this.renderTo = args.shift(); + } + this.init(args[0], args[1]); + }, + + /** + * Overridable function that initializes the chart. The constructor's + * arguments are passed on directly. + */ + init: function (userOptions, callback) { + + // Handle regular options + var options, + type, + seriesOptions = userOptions.series, // skip merging data points to increase performance + userPlotOptions = userOptions.plotOptions || {}; + + userOptions.series = null; + options = merge(defaultOptions, userOptions); // do the merge + + // Override (by copy of user options) or clear tooltip options + // in chart.options.plotOptions (#6218) + for (type in options.plotOptions) { + options.plotOptions[type].tooltip = ( + userPlotOptions[type] && + merge(userPlotOptions[type].tooltip) // override by copy + ) || undefined; // or clear + } + // User options have higher priority than default options (#6218). + // In case of exporting: path is changed + options.tooltip.userOptions = (userOptions.chart && + userOptions.chart.forExport && userOptions.tooltip.userOptions) || + userOptions.tooltip; + + options.series = userOptions.series = seriesOptions; // set back the series data + this.userOptions = userOptions; + + var optionsChart = options.chart; + + var chartEvents = optionsChart.events; + + this.margin = []; + this.spacing = []; + + this.bounds = { h: {}, v: {} }; // Pixel data bounds for touch zoom + + // An array of functions that returns labels that should be considered + // for anti-collision + this.labelCollectors = []; + + this.callback = callback; + this.isResizing = 0; + + /** + * The options structure for the chart. It contains members for the sub + * elements like series, legend, tooltip etc. + * + * @memberof Highcharts.Chart + * @name options + * @type {Options} + */ + this.options = options; + /** + * All the axes in the chart. + * + * @memberof Highcharts.Chart + * @name axes + * @see Highcharts.Chart.xAxis + * @see Highcharts.Chart.yAxis + * @type {Array.} + */ + this.axes = []; + + /** + * All the current series in the chart. + * + * @memberof Highcharts.Chart + * @name series + * @type {Array.} + */ + this.series = []; + + /** + * The chart title. The title has an `update` method that allows + * modifying the options directly or indirectly via `chart.update`. + * + * @memberof Highcharts.Chart + * @name title + * @type Object + * + * @sample highcharts/members/title-update/ + * Updating titles + */ + + /** + * The chart subtitle. The subtitle has an `update` method that allows + * modifying the options directly or indirectly via `chart.update`. + * + * @memberof Highcharts.Chart + * @name subtitle + * @type Object + */ + + /** + * The `Time` object associated with the chart. Since v6.0.5, time + * settings can be applied individually for each chart. If no individual + * settings apply, the `Time` object is shared by all instances. + * + * @memberof Highcharts.Chart + * @name time + * @type Highcharts.Time + */ + this.time = userOptions.time ? new H.Time(this) : H.time; + + + this.hasCartesianSeries = optionsChart.showAxes; + + var chart = this; + + // Add the chart to the global lookup + chart.index = charts.length; + + charts.push(chart); + H.chartCount++; + + // Chart event handlers + if (chartEvents) { + objectEach(chartEvents, function (event, eventType) { + addEvent(chart, eventType, event); + }); + } + + /** + * A collection of the X axes in the chart. + * @type {Array.} + * @name xAxis + * @memberOf Highcharts.Chart + */ + chart.xAxis = []; + /** + * A collection of the Y axes in the chart. + * @type {Array.} + * @name yAxis + * @memberOf Highcharts.Chart + */ + chart.yAxis = []; + + chart.pointCount = chart.colorCounter = chart.symbolCounter = 0; + + chart.firstRender(); + }, + + /** + * Internal function to unitialize an individual series. + * + * @private + */ + initSeries: function (options) { + var chart = this, + optionsChart = chart.options.chart, + type = options.type || optionsChart.type || optionsChart.defaultSeriesType, + series, + Constr = seriesTypes[type]; + + // No such series type + if (!Constr) { + H.error(17, true); + } + + series = new Constr(); + series.init(this, options); + return series; + }, + + /** + * Order all series above a given index. When series are added and ordered + * by configuration, only the last series is handled (#248, #1123, #2456, + * #6112). This function is called on series initialization and destroy. + * + * @private + * + * @param {number} fromIndex + * If this is given, only the series above this index are handled. + */ + orderSeries: function (fromIndex) { + var series = this.series, + i = fromIndex || 0; + for (; i < series.length; i++) { + if (series[i]) { + series[i].index = i; + series[i].name = series[i].name || + 'Series ' + (series[i].index + 1); + } + } + }, + + /** + * Check whether a given point is within the plot area. + * + * @param {Number} plotX + * Pixel x relative to the plot area. + * @param {Number} plotY + * Pixel y relative to the plot area. + * @param {Boolean} inverted + * Whether the chart is inverted. + * + * @return {Boolean} + * Returns true if the given point is inside the plot area. + */ + isInsidePlot: function (plotX, plotY, inverted) { + var x = inverted ? plotY : plotX, + y = inverted ? plotX : plotY; + + return x >= 0 && + x <= this.plotWidth && + y >= 0 && + y <= this.plotHeight; + }, + + /** + * Redraw the chart after changes have been done to the data, axis extremes + * chart size or chart elements. All methods for updating axes, series or + * points have a parameter for redrawing the chart. This is `true` by + * default. But in many cases you want to do more than one operation on the + * chart before redrawing, for example add a number of points. In those + * cases it is a waste of resources to redraw the chart for each new point + * added. So you add the points and call `chart.redraw()` after. + * + * @param {AnimationOptions} animation + * If or how to apply animation to the redraw. + */ + redraw: function (animation) { + var chart = this, + axes = chart.axes, + series = chart.series, + pointer = chart.pointer, + legend = chart.legend, + redrawLegend = chart.isDirtyLegend, + hasStackedSeries, + hasDirtyStacks, + hasCartesianSeries = chart.hasCartesianSeries, + isDirtyBox = chart.isDirtyBox, + i, + serie, + renderer = chart.renderer, + isHiddenChart = renderer.isHidden(), + afterRedraw = []; + + // Handle responsive rules, not only on resize (#6130) + if (chart.setResponsive) { + chart.setResponsive(false); + } + + H.setAnimation(animation, chart); + + if (isHiddenChart) { + chart.temporaryDisplay(); + } + + // Adjust title layout (reflow multiline text) + chart.layOutTitles(); + + // link stacked series + i = series.length; + while (i--) { + serie = series[i]; + + if (serie.options.stacking) { + hasStackedSeries = true; + + if (serie.isDirty) { + hasDirtyStacks = true; + break; + } + } + } + if (hasDirtyStacks) { // mark others as dirty + i = series.length; + while (i--) { + serie = series[i]; + if (serie.options.stacking) { + serie.isDirty = true; + } + } + } + + // Handle updated data in the series + each(series, function (serie) { + if (serie.isDirty) { + if (serie.options.legendType === 'point') { + if (serie.updateTotals) { + serie.updateTotals(); + } + redrawLegend = true; + } + } + if (serie.isDirtyData) { + fireEvent(serie, 'updatedData'); + } + }); + + // handle added or removed series + if (redrawLegend && legend.options.enabled) { // series or pie points are added or removed + // draw legend graphics + legend.render(); + + chart.isDirtyLegend = false; + } + + // reset stacks + if (hasStackedSeries) { + chart.getStacks(); + } + + + if (hasCartesianSeries) { + // set axes scales + each(axes, function (axis) { + axis.updateNames(); + axis.setScale(); + }); + } + + chart.getMargins(); // #3098 + + if (hasCartesianSeries) { + // If one axis is dirty, all axes must be redrawn (#792, #2169) + each(axes, function (axis) { + if (axis.isDirty) { + isDirtyBox = true; + } + }); + + // redraw axes + each(axes, function (axis) { + + // Fire 'afterSetExtremes' only if extremes are set + var key = axis.min + ',' + axis.max; + if (axis.extKey !== key) { // #821, #4452 + axis.extKey = key; + afterRedraw.push(function () { // prevent a recursive call to chart.redraw() (#1119) + fireEvent(axis, 'afterSetExtremes', extend(axis.eventArgs, axis.getExtremes())); // #747, #751 + delete axis.eventArgs; + }); + } + if (isDirtyBox || hasStackedSeries) { + axis.redraw(); + } + }); + } + + // the plot areas size has changed + if (isDirtyBox) { + chart.drawChartBox(); + } + + // Fire an event before redrawing series, used by the boost module to + // clear previous series renderings. + fireEvent(chart, 'predraw'); + + // redraw affected series + each(series, function (serie) { + if ((isDirtyBox || serie.isDirty) && serie.visible) { + serie.redraw(); + } + // Set it here, otherwise we will have unlimited 'updatedData' calls + // for a hidden series after setData(). Fixes #6012 + serie.isDirtyData = false; + }); + + // move tooltip or reset + if (pointer) { + pointer.reset(true); + } + + // redraw if canvas + renderer.draw(); + + // Fire the events + fireEvent(chart, 'redraw'); + fireEvent(chart, 'render'); + + if (isHiddenChart) { + chart.temporaryDisplay(true); + } + + // Fire callbacks that are put on hold until after the redraw + each(afterRedraw, function (callback) { + callback.call(); + }); + }, + + /** + * Get an axis, series or point object by `id` as given in the configuration + * options. Returns `undefined` if no item is found. + * @param id {String} The id as given in the configuration options. + * @return {Highcharts.Axis|Highcharts.Series|Highcharts.Point|undefined} + * The retrieved item. + * @sample highcharts/plotoptions/series-id/ + * Get series by id + */ + get: function (id) { + + var ret, + series = this.series, + i; + + function itemById(item) { + return item.id === id || (item.options && item.options.id === id); + } + + ret = + // Search axes + find(this.axes, itemById) || + + // Search series + find(this.series, itemById); + + // Search points + for (i = 0; !ret && i < series.length; i++) { + ret = find(series[i].points || [], itemById); + } + + return ret; + }, + + /** + * Create the Axis instances based on the config options. + * + * @private + */ + getAxes: function () { + var chart = this, + options = this.options, + xAxisOptions = options.xAxis = splat(options.xAxis || {}), + yAxisOptions = options.yAxis = splat(options.yAxis || {}), + optionsArray; + + // make sure the options are arrays and add some members + each(xAxisOptions, function (axis, i) { + axis.index = i; + axis.isX = true; + }); + + each(yAxisOptions, function (axis, i) { + axis.index = i; + }); + + // concatenate all axis options into one array + optionsArray = xAxisOptions.concat(yAxisOptions); + + each(optionsArray, function (axisOptions) { + new Axis(chart, axisOptions); // eslint-disable-line no-new + }); + }, + + + /** + * Returns an array of all currently selected points in the chart. Points + * can be selected by clicking or programmatically by the {@link + * Highcharts.Point#select} function. + * + * @return {Array.} + * The currently selected points. + * + * @sample highcharts/plotoptions/series-allowpointselect-line/ + * Get selected points + */ + getSelectedPoints: function () { + var points = []; + each(this.series, function (serie) { + // series.data - for points outside of viewed range (#6445) + points = points.concat(grep(serie.data || [], function (point) { + return point.selected; + })); + }); + return points; + }, + + /** + * Returns an array of all currently selected series in the chart. Series + * can be selected either programmatically by the {@link + * Highcharts.Series#select} function or by checking the checkbox next to + * the legend item if {@link + * https://api.highcharts.com/highcharts/plotOptions.series.showCheckbox| + * series.showCheckBox} is true. + * + * @return {Array.} + * The currently selected series. + * + * @sample highcharts/members/chart-getselectedseries/ + * Get selected series + */ + getSelectedSeries: function () { + return grep(this.series, function (serie) { + return serie.selected; + }); + }, + + /** + * Set a new title or subtitle for the chart. + * + * @param titleOptions {TitleOptions} + * New title options. The title text itself is set by the + * `titleOptions.text` property. + * @param subtitleOptions {SubtitleOptions} + * New subtitle options. The subtitle text itself is set by the + * `subtitleOptions.text` property. + * @param redraw {Boolean} + * Whether to redraw the chart or wait for a later call to + * `chart.redraw()`. + * + * @sample highcharts/members/chart-settitle/ Set title text and styles + * + */ + setTitle: function (titleOptions, subtitleOptions, redraw) { + var chart = this, + options = chart.options, + chartTitleOptions, + chartSubtitleOptions; + + chartTitleOptions = options.title = merge( + + // Default styles + { + style: { + color: '#333333', + fontSize: options.isStock ? '16px' : '18px' // #2944 + } + }, + + options.title, + titleOptions + ); + chartSubtitleOptions = options.subtitle = merge( + + // Default styles + { + style: { + color: '#666666' + } + }, + + options.subtitle, + subtitleOptions + ); + + // add title and subtitle + each([ + ['title', titleOptions, chartTitleOptions], + ['subtitle', subtitleOptions, chartSubtitleOptions] + ], function (arr, i) { + var name = arr[0], + title = chart[name], + titleOptions = arr[1], + chartTitleOptions = arr[2]; + + if (title && titleOptions) { + chart[name] = title = title.destroy(); // remove old + } + + if (chartTitleOptions && !title) { + chart[name] = chart.renderer.text( + chartTitleOptions.text, + 0, + 0, + chartTitleOptions.useHTML + ) + .attr({ + align: chartTitleOptions.align, + 'class': 'highcharts-' + name, + zIndex: chartTitleOptions.zIndex || 4 + }) + .add(); + + // Update methods, shortcut to Chart.setTitle + chart[name].update = function (o) { + chart.setTitle(!i && o, i && o); + }; + + + // Presentational + chart[name].css(chartTitleOptions.style); + + + } + }); + chart.layOutTitles(redraw); + }, + + /** + * Internal function to lay out the chart titles and cache the full offset + * height for use in `getMargins`. The result is stored in + * `this.titleOffset`. + * + * @private + */ + layOutTitles: function (redraw) { + var titleOffset = 0, + requiresDirtyBox, + renderer = this.renderer, + spacingBox = this.spacingBox; + + // Lay out the title and the subtitle respectively + each(['title', 'subtitle'], function (key) { + var title = this[key], + titleOptions = this.options[key], + offset = key === 'title' ? -3 : + // Floating subtitle (#6574) + titleOptions.verticalAlign ? 0 : titleOffset + 2, + titleSize; + + if (title) { + + titleSize = titleOptions.style.fontSize; + + titleSize = renderer.fontMetrics(titleSize, title).b; + + title + .css({ + width: (titleOptions.width || + spacingBox.width + titleOptions.widthAdjust) + 'px' + }) + .align(extend({ + y: offset + titleSize + }, titleOptions), false, 'spacingBox'); + + if (!titleOptions.floating && !titleOptions.verticalAlign) { + titleOffset = Math.ceil( + titleOffset + + // Skip the cache for HTML (#3481) + title.getBBox(titleOptions.useHTML).height + ); + } + } + }, this); + + requiresDirtyBox = this.titleOffset !== titleOffset; + this.titleOffset = titleOffset; // used in getMargins + + if (!this.isDirtyBox && requiresDirtyBox) { + this.isDirtyBox = requiresDirtyBox; + // Redraw if necessary (#2719, #2744) + if (this.hasRendered && pick(redraw, true) && this.isDirtyBox) { + this.redraw(); + } + } + }, + + /** + * Internal function to get the chart width and height according to options + * and container size. Sets {@link Chart.chartWidth} and {@link + * Chart.chartHeight}. + */ + getChartSize: function () { + var chart = this, + optionsChart = chart.options.chart, + widthOption = optionsChart.width, + heightOption = optionsChart.height, + renderTo = chart.renderTo; + + // Get inner width and height + if (!defined(widthOption)) { + chart.containerWidth = H.getStyle(renderTo, 'width'); + } + if (!defined(heightOption)) { + chart.containerHeight = H.getStyle(renderTo, 'height'); + } + + /** + * The current pixel width of the chart. + * + * @name chartWidth + * @memberOf Chart + * @type {Number} + */ + chart.chartWidth = Math.max( // #1393 + 0, + widthOption || chart.containerWidth || 600 // #1460 + ); + /** + * The current pixel height of the chart. + * + * @name chartHeight + * @memberOf Chart + * @type {Number} + */ + chart.chartHeight = Math.max( + 0, + H.relativeLength( + heightOption, + chart.chartWidth + ) || + (chart.containerHeight > 1 ? chart.containerHeight : 400) + ); + }, + + /** + * If the renderTo element has no offsetWidth, most likely one or more of + * its parents are hidden. Loop up the DOM tree to temporarily display the + * parents, then save the original display properties, and when the true + * size is retrieved, reset them. Used on first render and on redraws. + * + * @private + * + * @param {Boolean} revert + * Revert to the saved original styles. + */ + temporaryDisplay: function (revert) { + var node = this.renderTo, + tempStyle; + if (!revert) { + while (node && node.style) { + + // When rendering to a detached node, it needs to be temporarily + // attached in order to read styling and bounding boxes (#5783, + // #7024). + if (!doc.body.contains(node) && !node.parentNode) { + node.hcOrigDetached = true; + doc.body.appendChild(node); + } + if ( + H.getStyle(node, 'display', false) === 'none' || + node.hcOricDetached + ) { + node.hcOrigStyle = { + display: node.style.display, + height: node.style.height, + overflow: node.style.overflow + }; + tempStyle = { + display: 'block', + overflow: 'hidden' + }; + if (node !== this.renderTo) { + tempStyle.height = 0; + } + + H.css(node, tempStyle); + + // If it still doesn't have an offset width after setting + // display to block, it probably has an !important priority + // #2631, 6803 + if (!node.offsetWidth) { + node.style.setProperty('display', 'block', 'important'); + } + } + node = node.parentNode; + + if (node === doc.body) { + break; + } + } + } else { + while (node && node.style) { + if (node.hcOrigStyle) { + H.css(node, node.hcOrigStyle); + delete node.hcOrigStyle; + } + if (node.hcOrigDetached) { + doc.body.removeChild(node); + node.hcOrigDetached = false; + } + node = node.parentNode; + } + } + }, + + /** + * Set the {@link Chart.container|chart container's} class name, in + * addition to `highcharts-container`. + */ + setClassName: function (className) { + this.container.className = 'highcharts-container ' + (className || ''); + }, + + /** + * Get the containing element, determine the size and create the inner + * container div to hold the chart. + * + * @private + */ + getContainer: function () { + var chart = this, + container, + options = chart.options, + optionsChart = options.chart, + chartWidth, + chartHeight, + renderTo = chart.renderTo, + indexAttrName = 'data-highcharts-chart', + oldChartIndex, + Ren, + containerId = H.uniqueKey(), + containerStyle, + key; + + if (!renderTo) { + chart.renderTo = renderTo = optionsChart.renderTo; + } + + if (isString(renderTo)) { + chart.renderTo = renderTo = doc.getElementById(renderTo); + } + + // Display an error if the renderTo is wrong + if (!renderTo) { + H.error(13, true); + } + + // If the container already holds a chart, destroy it. The check for + // hasRendered is there because web pages that are saved to disk from + // the browser, will preserve the data-highcharts-chart attribute and + // the SVG contents, but not an interactive chart. So in this case, + // charts[oldChartIndex] will point to the wrong chart if any (#2609). + oldChartIndex = pInt(attr(renderTo, indexAttrName)); + if ( + isNumber(oldChartIndex) && + charts[oldChartIndex] && + charts[oldChartIndex].hasRendered + ) { + charts[oldChartIndex].destroy(); + } + + // Make a reference to the chart from the div + attr(renderTo, indexAttrName, chart.index); + + // remove previous chart + renderTo.innerHTML = ''; + + // If the container doesn't have an offsetWidth, it has or is a child of + // a node that has display:none. We need to temporarily move it out to a + // visible state to determine the size, else the legend and tooltips + // won't render properly. The skipClone option is used in sparklines as + // a micro optimization, saving about 1-2 ms each chart. + if (!optionsChart.skipClone && !renderTo.offsetWidth) { + chart.temporaryDisplay(); + } + + // get the width and height + chart.getChartSize(); + chartWidth = chart.chartWidth; + chartHeight = chart.chartHeight; + + // Create the inner container + + containerStyle = extend({ + position: 'relative', + overflow: 'hidden', // needed for context menu (avoid scrollbars) + // and content overflow in IE + width: chartWidth + 'px', + height: chartHeight + 'px', + textAlign: 'left', + lineHeight: 'normal', // #427 + zIndex: 0, // #1072 + '-webkit-tap-highlight-color': 'rgba(0,0,0,0)' + }, optionsChart.style); + + + /** + * The containing HTML element of the chart. The container is + * dynamically inserted into the element given as the `renderTo` + * parameterin the {@link Highcharts#chart} constructor. + * + * @memberOf Highcharts.Chart + * @type {HTMLDOMElement} + */ + container = createElement( + 'div', + { + id: containerId + }, + containerStyle, + renderTo + ); + chart.container = container; + + // cache the cursor (#1650) + chart._cursor = container.style.cursor; + + // Initialize the renderer + Ren = H[optionsChart.renderer] || H.Renderer; + + /** + * The renderer instance of the chart. Each chart instance has only one + * associated renderer. + * @type {SVGRenderer} + * @name renderer + * @memberOf Chart + */ + chart.renderer = new Ren( + container, + chartWidth, + chartHeight, + null, + optionsChart.forExport, + options.exporting && options.exporting.allowHTML + ); + + + chart.setClassName(optionsChart.className); + + chart.renderer.setStyle(optionsChart.style); + + + // Add a reference to the charts index + chart.renderer.chartIndex = chart.index; + }, + + /** + * Calculate margins by rendering axis labels in a preliminary position. + * Title, subtitle and legend have already been rendered at this stage, but + * will be moved into their final positions. + * + * @private + */ + getMargins: function (skipAxes) { + var chart = this, + spacing = chart.spacing, + margin = chart.margin, + titleOffset = chart.titleOffset; + + chart.resetMargins(); + + // Adjust for title and subtitle + if (titleOffset && !defined(margin[0])) { + chart.plotTop = Math.max( + chart.plotTop, + titleOffset + chart.options.title.margin + spacing[0] + ); + } + + // Adjust for legend + if (chart.legend && chart.legend.display) { + chart.legend.adjustMargins(margin, spacing); + } + + // adjust for scroller + if (chart.extraMargin) { + chart[chart.extraMargin.type] = + (chart[chart.extraMargin.type] || 0) + chart.extraMargin.value; + } + + // adjust for rangeSelector + if (chart.adjustPlotArea) { + chart.adjustPlotArea(); + } + + if (!skipAxes) { + this.getAxisMargins(); + } + }, + + getAxisMargins: function () { + + var chart = this, + // [top, right, bottom, left] + axisOffset = chart.axisOffset = [0, 0, 0, 0], + margin = chart.margin; + + // pre-render axes to get labels offset width + if (chart.hasCartesianSeries) { + each(chart.axes, function (axis) { + if (axis.visible) { + axis.getOffset(); + } + }); + } + + // Add the axis offsets + each(marginNames, function (m, side) { + if (!defined(margin[side])) { + chart[m] += axisOffset[side]; + } + }); + + chart.setChartSize(); + + }, + + /** + * Reflows the chart to its container. By default, the chart reflows + * automatically to its container following a `window.resize` event, as per + * the {@link https://api.highcharts/highcharts/chart.reflow|chart.reflow} + * option. However, there are no reliable events for div resize, so if the + * container is resized without a window resize event, this must be called + * explicitly. + * + * @param {Object} e + * Event arguments. Used primarily when the function is called + * internally as a response to window resize. + * + * @sample highcharts/members/chart-reflow/ + * Resize div and reflow + * @sample highcharts/chart/events-container/ + * Pop up and reflow + */ + reflow: function (e) { + var chart = this, + optionsChart = chart.options.chart, + renderTo = chart.renderTo, + hasUserSize = ( + defined(optionsChart.width) && + defined(optionsChart.height) + ), + width = optionsChart.width || H.getStyle(renderTo, 'width'), + height = optionsChart.height || H.getStyle(renderTo, 'height'), + target = e ? e.target : win; + + // Width and height checks for display:none. Target is doc in IE8 and + // Opera, win in Firefox, Chrome and IE9. + if ( + !hasUserSize && + !chart.isPrinting && + width && + height && + (target === win || target === doc) + ) { + if ( + width !== chart.containerWidth || + height !== chart.containerHeight + ) { + clearTimeout(chart.reflowTimeout); + // When called from window.resize, e is set, else it's called + // directly (#2224) + chart.reflowTimeout = syncTimeout(function () { + // Set size, it may have been destroyed in the meantime + // (#1257) + if (chart.container) { + chart.setSize(undefined, undefined, false); + } + }, e ? 100 : 0); + } + chart.containerWidth = width; + chart.containerHeight = height; + } + }, + + /** + * Add the event handlers necessary for auto resizing, depending on the + * `chart.events.reflow` option. + * + * @private + */ + initReflow: function () { + var chart = this, + unbind; + + unbind = addEvent(win, 'resize', function (e) { + chart.reflow(e); + }); + addEvent(chart, 'destroy', unbind); + + // The following will add listeners to re-fit the chart before and after + // printing (#2284). However it only works in WebKit. Should have worked + // in Firefox, but not supported in IE. + /* + if (win.matchMedia) { + win.matchMedia('print').addListener(function reflow() { + chart.reflow(); + }); + } + */ + }, + + /** + * Resize the chart to a given width and height. In order to set the width + * only, the height argument may be skipped. To set the height only, pass + * `undefined for the width. + * @param {Number|undefined|null} [width] + * The new pixel width of the chart. Since v4.2.6, the argument can + * be `undefined` in order to preserve the current value (when + * setting height only), or `null` to adapt to the width of the + * containing element. + * @param {Number|undefined|null} [height] + * The new pixel height of the chart. Since v4.2.6, the argument can + * be `undefined` in order to preserve the current value, or `null` + * in order to adapt to the height of the containing element. + * @param {AnimationOptions} [animation=true] + * Whether and how to apply animation. + * + * @sample highcharts/members/chart-setsize-button/ + * Test resizing from buttons + * @sample highcharts/members/chart-setsize-jquery-resizable/ + * Add a jQuery UI resizable + * @sample stock/members/chart-setsize/ + * Highstock with UI resizable + */ + setSize: function (width, height, animation) { + var chart = this, + renderer = chart.renderer, + globalAnimation; + + // Handle the isResizing counter + chart.isResizing += 1; + + // set the animation for the current process + H.setAnimation(animation, chart); + + chart.oldChartHeight = chart.chartHeight; + chart.oldChartWidth = chart.chartWidth; + if (width !== undefined) { + chart.options.chart.width = width; + } + if (height !== undefined) { + chart.options.chart.height = height; + } + chart.getChartSize(); + + // Resize the container with the global animation applied if enabled + // (#2503) + + globalAnimation = renderer.globalAnimation; + (globalAnimation ? animate : css)(chart.container, { + width: chart.chartWidth + 'px', + height: chart.chartHeight + 'px' + }, globalAnimation); + + + chart.setChartSize(true); + renderer.setSize(chart.chartWidth, chart.chartHeight, animation); + + // handle axes + each(chart.axes, function (axis) { + axis.isDirty = true; + axis.setScale(); + }); + + chart.isDirtyLegend = true; // force legend redraw + chart.isDirtyBox = true; // force redraw of plot and chart border + + chart.layOutTitles(); // #2857 + chart.getMargins(); + + chart.redraw(animation); + + + chart.oldChartHeight = null; + fireEvent(chart, 'resize'); + + // Fire endResize and set isResizing back. If animation is disabled, + // fire without delay + syncTimeout(function () { + if (chart) { + fireEvent(chart, 'endResize', null, function () { + chart.isResizing -= 1; + }); + } + }, animObject(globalAnimation).duration); + }, + + /** + * Set the public chart properties. This is done before and after the + * pre-render to determine margin sizes. + * + * @private + */ + setChartSize: function (skipAxes) { + var chart = this, + inverted = chart.inverted, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + optionsChart = chart.options.chart, + spacing = chart.spacing, + clipOffset = chart.clipOffset, + clipX, + clipY, + plotLeft, + plotTop, + plotWidth, + plotHeight, + plotBorderWidth; + + /** + * The current left position of the plot area in pixels. + * + * @name plotLeft + * @memberOf Chart + * @type {Number} + */ + chart.plotLeft = plotLeft = Math.round(chart.plotLeft); + + /** + * The current top position of the plot area in pixels. + * + * @name plotTop + * @memberOf Chart + * @type {Number} + */ + chart.plotTop = plotTop = Math.round(chart.plotTop); + + /** + * The current width of the plot area in pixels. + * + * @name plotWidth + * @memberOf Chart + * @type {Number} + */ + chart.plotWidth = plotWidth = Math.max( + 0, + Math.round(chartWidth - plotLeft - chart.marginRight) + ); + + /** + * The current height of the plot area in pixels. + * + * @name plotHeight + * @memberOf Chart + * @type {Number} + */ + chart.plotHeight = plotHeight = Math.max( + 0, + Math.round(chartHeight - plotTop - chart.marginBottom) + ); + + chart.plotSizeX = inverted ? plotHeight : plotWidth; + chart.plotSizeY = inverted ? plotWidth : plotHeight; + + chart.plotBorderWidth = optionsChart.plotBorderWidth || 0; + + // Set boxes used for alignment + chart.spacingBox = renderer.spacingBox = { + x: spacing[3], + y: spacing[0], + width: chartWidth - spacing[3] - spacing[1], + height: chartHeight - spacing[0] - spacing[2] + }; + chart.plotBox = renderer.plotBox = { + x: plotLeft, + y: plotTop, + width: plotWidth, + height: plotHeight + }; + + plotBorderWidth = 2 * Math.floor(chart.plotBorderWidth / 2); + clipX = Math.ceil(Math.max(plotBorderWidth, clipOffset[3]) / 2); + clipY = Math.ceil(Math.max(plotBorderWidth, clipOffset[0]) / 2); + chart.clipBox = { + x: clipX, + y: clipY, + width: Math.floor( + chart.plotSizeX - + Math.max(plotBorderWidth, clipOffset[1]) / 2 - + clipX + ), + height: Math.max( + 0, + Math.floor( + chart.plotSizeY - + Math.max(plotBorderWidth, clipOffset[2]) / 2 - + clipY + ) + ) + }; + + if (!skipAxes) { + each(chart.axes, function (axis) { + axis.setAxisSize(); + axis.setAxisTranslation(); + }); + } + }, + + /** + * Initial margins before auto size margins are applied. + * + * @private + */ + resetMargins: function () { + var chart = this, + chartOptions = chart.options.chart; + + // Create margin and spacing array + each(['margin', 'spacing'], function splashArrays(target) { + var value = chartOptions[target], + values = isObject(value) ? value : [value, value, value, value]; + + each(['Top', 'Right', 'Bottom', 'Left'], function (sideName, side) { + chart[target][side] = pick( + chartOptions[target + sideName], + values[side] + ); + }); + }); + + // Set margin names like chart.plotTop, chart.plotLeft, + // chart.marginRight, chart.marginBottom. + each(marginNames, function (m, side) { + chart[m] = pick(chart.margin[side], chart.spacing[side]); + }); + chart.axisOffset = [0, 0, 0, 0]; // top, right, bottom, left + chart.clipOffset = [0, 0, 0, 0]; + }, + + /** + * Internal function to draw or redraw the borders and backgrounds for chart + * and plot area. + * + * @private + */ + drawChartBox: function () { + var chart = this, + optionsChart = chart.options.chart, + renderer = chart.renderer, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + chartBackground = chart.chartBackground, + plotBackground = chart.plotBackground, + plotBorder = chart.plotBorder, + chartBorderWidth, + + plotBGImage = chart.plotBGImage, + chartBackgroundColor = optionsChart.backgroundColor, + plotBackgroundColor = optionsChart.plotBackgroundColor, + plotBackgroundImage = optionsChart.plotBackgroundImage, + + mgn, + bgAttr, + plotLeft = chart.plotLeft, + plotTop = chart.plotTop, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + plotBox = chart.plotBox, + clipRect = chart.clipRect, + clipBox = chart.clipBox, + verb = 'animate'; + + // Chart area + if (!chartBackground) { + chart.chartBackground = chartBackground = renderer.rect() + .addClass('highcharts-background') + .add(); + verb = 'attr'; + } + + + // Presentational + chartBorderWidth = optionsChart.borderWidth || 0; + mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); + + bgAttr = { + fill: chartBackgroundColor || 'none' + }; + + if (chartBorderWidth || chartBackground['stroke-width']) { // #980 + bgAttr.stroke = optionsChart.borderColor; + bgAttr['stroke-width'] = chartBorderWidth; + } + chartBackground + .attr(bgAttr) + .shadow(optionsChart.shadow); + + chartBackground[verb]({ + x: mgn / 2, + y: mgn / 2, + width: chartWidth - mgn - chartBorderWidth % 2, + height: chartHeight - mgn - chartBorderWidth % 2, + r: optionsChart.borderRadius + }); + + // Plot background + verb = 'animate'; + if (!plotBackground) { + verb = 'attr'; + chart.plotBackground = plotBackground = renderer.rect() + .addClass('highcharts-plot-background') + .add(); + } + plotBackground[verb](plotBox); + + + // Presentational attributes for the background + plotBackground + .attr({ + fill: plotBackgroundColor || 'none' + }) + .shadow(optionsChart.plotShadow); + + // Create the background image + if (plotBackgroundImage) { + if (!plotBGImage) { + chart.plotBGImage = renderer.image( + plotBackgroundImage, + plotLeft, + plotTop, + plotWidth, + plotHeight + ).add(); + } else { + plotBGImage.animate(plotBox); + } + } + + + // Plot clip + if (!clipRect) { + chart.clipRect = renderer.clipRect(clipBox); + } else { + clipRect.animate({ + width: clipBox.width, + height: clipBox.height + }); + } + + // Plot area border + verb = 'animate'; + if (!plotBorder) { + verb = 'attr'; + chart.plotBorder = plotBorder = renderer.rect() + .addClass('highcharts-plot-border') + .attr({ + zIndex: 1 // Above the grid + }) + .add(); + } + + + // Presentational + plotBorder.attr({ + stroke: optionsChart.plotBorderColor, + 'stroke-width': optionsChart.plotBorderWidth || 0, + fill: 'none' + }); + + + plotBorder[verb](plotBorder.crisp({ + x: plotLeft, + y: plotTop, + width: plotWidth, + height: plotHeight + }, -plotBorder.strokeWidth())); // #3282 plotBorder should be negative; + + // reset + chart.isDirtyBox = false; + }, + + /** + * Detect whether a certain chart property is needed based on inspecting its + * options and series. This mainly applies to the chart.inverted property, + * and in extensions to the chart.angular and chart.polar properties. + * + * @private + */ + propFromSeries: function () { + var chart = this, + optionsChart = chart.options.chart, + klass, + seriesOptions = chart.options.series, + i, + value; + + + each(['inverted', 'angular', 'polar'], function (key) { + + // The default series type's class + klass = seriesTypes[optionsChart.type || + optionsChart.defaultSeriesType]; + + // Get the value from available chart-wide properties + value = + optionsChart[key] || // It is set in the options + (klass && klass.prototype[key]); // The default series class + // requires it + + // 4. Check if any the chart's series require it + i = seriesOptions && seriesOptions.length; + while (!value && i--) { + klass = seriesTypes[seriesOptions[i].type]; + if (klass && klass.prototype[key]) { + value = true; + } + } + + // Set the chart property + chart[key] = value; + }); + + }, + + /** + * Internal function to link two or more series together, based on the + * `linkedTo` option. This is done from `Chart.render`, and after + * `Chart.addSeries` and `Series.remove`. + * + * @private + */ + linkSeries: function () { + var chart = this, + chartSeries = chart.series; + + // Reset links + each(chartSeries, function (series) { + series.linkedSeries.length = 0; + }); + + // Apply new links + each(chartSeries, function (series) { + var linkedTo = series.options.linkedTo; + if (isString(linkedTo)) { + if (linkedTo === ':previous') { + linkedTo = chart.series[series.index - 1]; + } else { + linkedTo = chart.get(linkedTo); + } + // #3341 avoid mutual linking + if (linkedTo && linkedTo.linkedParent !== series) { + linkedTo.linkedSeries.push(series); + series.linkedParent = linkedTo; + series.visible = pick( + series.options.visible, + linkedTo.options.visible, + series.visible + ); // #3879 + } + } + }); + }, + + /** + * Render series for the chart. + * + * @private + */ + renderSeries: function () { + each(this.series, function (serie) { + serie.translate(); + serie.render(); + }); + }, + + /** + * Render labels for the chart. + * + * @private + */ + renderLabels: function () { + var chart = this, + labels = chart.options.labels; + if (labels.items) { + each(labels.items, function (label) { + var style = extend(labels.style, label.style), + x = pInt(style.left) + chart.plotLeft, + y = pInt(style.top) + chart.plotTop + 12; + + // delete to prevent rewriting in IE + delete style.left; + delete style.top; + + chart.renderer.text( + label.html, + x, + y + ) + .attr({ zIndex: 2 }) + .css(style) + .add(); + + }); + } + }, + + /** + * Render all graphics for the chart. Runs internally on initialization. + * + * @private + */ + render: function () { + var chart = this, + axes = chart.axes, + renderer = chart.renderer, + options = chart.options, + tempWidth, + tempHeight, + redoHorizontal, + redoVertical; + + // Title + chart.setTitle(); + + + // Legend + chart.legend = new Legend(chart, options.legend); + + // Get stacks + if (chart.getStacks) { + chart.getStacks(); + } + + // Get chart margins + chart.getMargins(true); + chart.setChartSize(); + + // Record preliminary dimensions for later comparison + tempWidth = chart.plotWidth; + // 21 is the most common correction for X axis labels + // use Math.max to prevent negative plotHeight + tempHeight = chart.plotHeight = Math.max(chart.plotHeight - 21, 0); + + // Get margins by pre-rendering axes + each(axes, function (axis) { + axis.setScale(); + }); + chart.getAxisMargins(); + + // If the plot area size has changed significantly, calculate tick positions again + redoHorizontal = tempWidth / chart.plotWidth > 1.1; + redoVertical = tempHeight / chart.plotHeight > 1.05; // Height is more sensitive + + if (redoHorizontal || redoVertical) { + + each(axes, function (axis) { + if ((axis.horiz && redoHorizontal) || (!axis.horiz && redoVertical)) { + axis.setTickInterval(true); // update to reflect the new margins + } + }); + chart.getMargins(); // second pass to check for new labels + } + + // Draw the borders and backgrounds + chart.drawChartBox(); + + + // Axes + if (chart.hasCartesianSeries) { + each(axes, function (axis) { + if (axis.visible) { + axis.render(); + } + }); + } + + // The series + if (!chart.seriesGroup) { + chart.seriesGroup = renderer.g('series-group') + .attr({ zIndex: 3 }) + .add(); + } + chart.renderSeries(); + + // Labels + chart.renderLabels(); + + // Credits + chart.addCredits(); + + // Handle responsiveness + if (chart.setResponsive) { + chart.setResponsive(); + } + + // Set flag + chart.hasRendered = true; + + }, + + /** + * Set a new credits label for the chart. + * + * @param {CreditOptions} options + * A configuration object for the new credits. + * @sample highcharts/credits/credits-update/ Add and update credits + */ + addCredits: function (credits) { + var chart = this; + + credits = merge(true, this.options.credits, credits); + if (credits.enabled && !this.credits) { + + /** + * The chart's credits label. The label has an `update` method that + * allows setting new options as per the {@link + * https://api.highcharts.com/highcharts/credits| + * credits options set}. + * + * @memberof Highcharts.Chart + * @name credits + * @type {Highcharts.SVGElement} + */ + this.credits = this.renderer.text( + credits.text + (this.mapCredits || ''), + 0, + 0 + ) + .addClass('highcharts-credits') + .on('click', function () { + if (credits.href) { + win.location.href = credits.href; + } + }) + .attr({ + align: credits.position.align, + zIndex: 8 + }) + + .css(credits.style) + + .add() + .align(credits.position); + + // Dynamically update + this.credits.update = function (options) { + chart.credits = chart.credits.destroy(); + chart.addCredits(options); + }; + } + }, + + /** + * Remove the chart and purge memory. This method is called internally + * before adding a second chart into the same container, as well as on + * window unload to prevent leaks. + * + * @sample highcharts/members/chart-destroy/ + * Destroy the chart from a button + * @sample stock/members/chart-destroy/ + * Destroy with Highstock + */ + destroy: function () { + var chart = this, + axes = chart.axes, + series = chart.series, + container = chart.container, + i, + parentNode = container && container.parentNode; + + // fire the chart.destoy event + fireEvent(chart, 'destroy'); + + // Delete the chart from charts lookup array + if (chart.renderer.forExport) { + H.erase(charts, chart); // #6569 + } else { + charts[chart.index] = undefined; + } + H.chartCount--; + chart.renderTo.removeAttribute('data-highcharts-chart'); + + // remove events + removeEvent(chart); + + // ==== Destroy collections: + // Destroy axes + i = axes.length; + while (i--) { + axes[i] = axes[i].destroy(); + } + + // Destroy scroller & scroller series before destroying base series + if (this.scroller && this.scroller.destroy) { + this.scroller.destroy(); + } + + // Destroy each series + i = series.length; + while (i--) { + series[i] = series[i].destroy(); + } + + // ==== Destroy chart properties: + each([ + 'title', 'subtitle', 'chartBackground', 'plotBackground', + 'plotBGImage', 'plotBorder', 'seriesGroup', 'clipRect', 'credits', + 'pointer', 'rangeSelector', 'legend', 'resetZoomButton', 'tooltip', + 'renderer' + ], function (name) { + var prop = chart[name]; + + if (prop && prop.destroy) { + chart[name] = prop.destroy(); + } + }); + + // remove container and all SVG + if (container) { // can break in IE when destroyed before finished loading + container.innerHTML = ''; + removeEvent(container); + if (parentNode) { + discardElement(container); + } + + } + + // clean it all up + objectEach(chart, function (val, key) { + delete chart[key]; + }); + + }, + + + /** + * VML namespaces can't be added until after complete. Listening + * for Perini's doScroll hack is not enough. + * + * @private + */ + isReadyToRender: function () { + var chart = this; + + // Note: win == win.top is required + if ((!svg && (win == win.top && doc.readyState !== 'complete'))) { // eslint-disable-line eqeqeq + doc.attachEvent('onreadystatechange', function () { + doc.detachEvent('onreadystatechange', chart.firstRender); + if (doc.readyState === 'complete') { + chart.firstRender(); + } + }); + return false; + } + return true; + }, + + /** + * Prepare for first rendering after all data are loaded. + * + * @private + */ + firstRender: function () { + var chart = this, + options = chart.options; + + // Check whether the chart is ready to render + if (!chart.isReadyToRender()) { + return; + } + + // Create the container + chart.getContainer(); + + // Run an early event after the container and renderer are established + fireEvent(chart, 'init'); + + + chart.resetMargins(); + chart.setChartSize(); + + // Set the common chart properties (mainly invert) from the given series + chart.propFromSeries(); + + // get axes + chart.getAxes(); + + // Initialize the series + each(options.series || [], function (serieOptions) { + chart.initSeries(serieOptions); + }); + + chart.linkSeries(); + + // Run an event after axes and series are initialized, but before render. At this stage, + // the series data is indexed and cached in the xData and yData arrays, so we can access + // those before rendering. Used in Highstock. + fireEvent(chart, 'beforeRender'); + + // depends on inverted and on margins being set + if (Pointer) { + + /** + * The Pointer that keeps track of mouse and touch interaction. + * + * @memberof Chart + * @name pointer + * @type Pointer + */ + chart.pointer = new Pointer(chart, options); + } + + chart.render(); + + // Fire the load event if there are no external images + if (!chart.renderer.imgCount && chart.onload) { + chart.onload(); + } + + // If the chart was rendered outside the top container, put it back in (#3679) + chart.temporaryDisplay(true); + + }, + + /** + * Internal function that runs on chart load, async if any images are loaded + * in the chart. Runs the callbacks and triggers the `load` and `render` + * events. + * + * @private + */ + onload: function () { + + // Run callbacks + each([this.callback].concat(this.callbacks), function (fn) { + if (fn && this.index !== undefined) { // Chart destroyed in its own callback (#3600) + fn.apply(this, [this]); + } + }, this); + + fireEvent(this, 'load'); + fireEvent(this, 'render'); + + + // Set up auto resize, check for not destroyed (#6068) + if (defined(this.index) && this.options.chart.reflow !== false) { + this.initReflow(); + } + + // Don't run again + this.onload = null; + } + +}); // end Chart + +}(Highcharts)); +(function (Highcharts) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var Point, + H = Highcharts, + + each = H.each, + extend = H.extend, + erase = H.erase, + fireEvent = H.fireEvent, + format = H.format, + isArray = H.isArray, + isNumber = H.isNumber, + pick = H.pick, + removeEvent = H.removeEvent; + +/** + * The Point object. The point objects are generated from the `series.data` + * configuration objects or raw numbers. They can be accessed from the + * `Series.points` array. Other ways to instaniate points are through {@link + * Highcharts.Series#addPoint} or {@link Highcharts.Series#setData}. + * + * @class + */ + +Highcharts.Point = Point = function () {}; +Highcharts.Point.prototype = { + + /** + * Initialize the point. Called internally based on the `series.data` + * option. + * @param {Series} series + * The series object containing this point. + * @param {Number|Array|Object} options + * The data in either number, array or object format. + * @param {Number} x Optionally, the X value of the point. + * @return {Point} The Point instance. + */ + init: function (series, options, x) { + + var point = this, + colors, + colorCount = series.chart.options.chart.colorCount, + colorIndex; + + /** + * The series object associated with the point. + * + * @name series + * @memberof Highcharts.Point + * @type Highcharts.Series + */ + point.series = series; + + + /** + * The point's current color. + * @name color + * @memberof Highcharts.Point + * @type {Color} + */ + point.color = series.color; // #3445 + + point.applyOptions(options, x); + + if (series.options.colorByPoint) { + + colors = series.options.colors || series.chart.options.colors; + point.color = point.color || colors[series.colorCounter]; + colorCount = colors.length; + + colorIndex = series.colorCounter; + series.colorCounter++; + // loop back to zero + if (series.colorCounter === colorCount) { + series.colorCounter = 0; + } + } else { + colorIndex = series.colorIndex; + } + + /** + * The point's current color index, used in styled mode instead of + * `color`. The color index is inserted in class names used for styling. + * @name colorIndex + * @memberof Highcharts.Point + * @type {Number} + */ + point.colorIndex = pick(point.colorIndex, colorIndex); + + series.chart.pointCount++; + return point; + }, + /** + * Apply the options containing the x and y data and possible some extra + * properties. Called on point init or from point.update. + * + * @private + * @param {Object} options The point options as defined in series.data. + * @param {Number} x Optionally, the X value. + * @returns {Object} The Point instance. + */ + applyOptions: function (options, x) { + var point = this, + series = point.series, + pointValKey = series.options.pointValKey || series.pointValKey; + + options = Point.prototype.optionsToObject.call(this, options); + + // copy options directly to point + extend(point, options); + point.options = point.options ? extend(point.options, options) : options; + + // Since options are copied into the Point instance, some accidental options must be shielded (#5681) + if (options.group) { + delete point.group; + } + + // For higher dimension series types. For instance, for ranges, point.y is mapped to point.low. + if (pointValKey) { + point.y = point[pointValKey]; + } + point.isNull = pick( + point.isValid && !point.isValid(), + point.x === null || !isNumber(point.y, true) + ); // #3571, check for NaN + + // The point is initially selected by options (#5777) + if (point.selected) { + point.state = 'select'; + } + + // If no x is set by now, get auto incremented value. All points must have an + // x value, however the y value can be null to create a gap in the series + if ('name' in point && x === undefined && series.xAxis && series.xAxis.hasNames) { + point.x = series.xAxis.nameToX(point); + } + if (point.x === undefined && series) { + if (x === undefined) { + point.x = series.autoIncrement(point); + } else { + point.x = x; + } + } + + return point; + }, + + /** + * Transform number or array configs into objects. Used internally to unify + * the different configuration formats for points. For example, a simple + * number `10` in a line series will be transformed to `{ y: 10 }`, and an + * array config like `[1, 10]` in a scatter series will be transformed to + * `{ x: 1, y: 10 }`. + * + * @param {Number|Array|Object} options + * The input options + * @return {Object} Transformed options. + */ + optionsToObject: function (options) { + var ret = {}, + series = this.series, + keys = series.options.keys, + pointArrayMap = keys || series.pointArrayMap || ['y'], + valueCount = pointArrayMap.length, + firstItemType, + i = 0, + j = 0; + + if (isNumber(options) || options === null) { + ret[pointArrayMap[0]] = options; + + } else if (isArray(options)) { + // with leading x value + if (!keys && options.length > valueCount) { + firstItemType = typeof options[0]; + if (firstItemType === 'string') { + ret.name = options[0]; + } else if (firstItemType === 'number') { + ret.x = options[0]; + } + i++; + } + while (j < valueCount) { + if (!keys || options[i] !== undefined) { // Skip undefined positions for keys + ret[pointArrayMap[j]] = options[i]; + } + i++; + j++; + } + } else if (typeof options === 'object') { + ret = options; + + // This is the fastest way to detect if there are individual point dataLabels that need + // to be considered in drawDataLabels. These can only occur in object configs. + if (options.dataLabels) { + series._hasPointLabels = true; + } + + // Same approach as above for markers + if (options.marker) { + series._hasPointMarkers = true; + } + } + return ret; + }, + + /** + * Get the CSS class names for individual points. Used internally where the + * returned value is set on every point. + * + * @returns {String} The class names. + */ + getClassName: function () { + return 'highcharts-point' + + (this.selected ? ' highcharts-point-select' : '') + + (this.negative ? ' highcharts-negative' : '') + + (this.isNull ? ' highcharts-null-point' : '') + + (this.colorIndex !== undefined ? ' highcharts-color-' + + this.colorIndex : '') + + (this.options.className ? ' ' + this.options.className : '') + + (this.zone && this.zone.className ? ' ' + + this.zone.className.replace('highcharts-negative', '') : ''); + }, + + /** + * In a series with `zones`, return the zone that the point belongs to. + * + * @return {Object} + * The zone item. + */ + getZone: function () { + var series = this.series, + zones = series.zones, + zoneAxis = series.zoneAxis || 'y', + i = 0, + zone; + + zone = zones[i]; + while (this[zoneAxis] >= zone.value) { + zone = zones[++i]; + } + + if (zone && zone.color && !this.options.color) { + this.color = zone.color; + } + + return zone; + }, + + /** + * Destroy a point to clear memory. Its reference still stays in + * `series.data`. + * + * @private + */ + destroy: function () { + var point = this, + series = point.series, + chart = series.chart, + hoverPoints = chart.hoverPoints, + prop; + + chart.pointCount--; + + if (hoverPoints) { + point.setState(); + erase(hoverPoints, point); + if (!hoverPoints.length) { + chart.hoverPoints = null; + } + + } + if (point === chart.hoverPoint) { + point.onMouseOut(); + } + + // remove all events + if (point.graphic || point.dataLabel) { // removeEvent and destroyElements are performance expensive + removeEvent(point); + point.destroyElements(); + } + + if (point.legendItem) { // pies have legend items + chart.legend.destroyItem(point); + } + + for (prop in point) { + point[prop] = null; + } + + + }, + + /** + * Destroy SVG elements associated with the point. + * + * @private + */ + destroyElements: function () { + var point = this, + props = ['graphic', 'dataLabel', 'dataLabelUpper', 'connector', 'shadowGroup'], + prop, + i = 6; + while (i--) { + prop = props[i]; + if (point[prop]) { + point[prop] = point[prop].destroy(); + } + } + }, + + /** + * Return the configuration hash needed for the data label and tooltip + * formatters. + * + * @returns {Object} + * Abstract object used in formatters and formats. + */ + getLabelConfig: function () { + return { + x: this.category, + y: this.y, + color: this.color, + colorIndex: this.colorIndex, + key: this.name || this.category, + series: this.series, + point: this, + percentage: this.percentage, + total: this.total || this.stackTotal + }; + }, + + /** + * Extendable method for formatting each point's tooltip line. + * + * @param {String} pointFormat + * The point format. + * @return {String} + * A string to be concatenated in to the common tooltip text. + */ + tooltipFormatter: function (pointFormat) { + + // Insert options for valueDecimals, valuePrefix, and valueSuffix + var series = this.series, + seriesTooltipOptions = series.tooltipOptions, + valueDecimals = pick(seriesTooltipOptions.valueDecimals, ''), + valuePrefix = seriesTooltipOptions.valuePrefix || '', + valueSuffix = seriesTooltipOptions.valueSuffix || ''; + + // Loop over the point array map and replace unformatted values with sprintf formatting markup + each(series.pointArrayMap || ['y'], function (key) { + key = '{point.' + key; // without the closing bracket + if (valuePrefix || valueSuffix) { + pointFormat = pointFormat.replace(key + '}', valuePrefix + key + '}' + valueSuffix); + } + pointFormat = pointFormat.replace(key + '}', key + ':,.' + valueDecimals + 'f}'); + }); + + return format(pointFormat, { + point: this, + series: this.series + }, series.chart.time); + }, + + /** + * Fire an event on the Point object. + * + * @private + * @param {String} eventType + * @param {Object} eventArgs Additional event arguments + * @param {Function} defaultFunction Default event handler + */ + firePointEvent: function (eventType, eventArgs, defaultFunction) { + var point = this, + series = this.series, + seriesOptions = series.options; + + // load event handlers on demand to save time on mouseover/out + if (seriesOptions.point.events[eventType] || (point.options && point.options.events && point.options.events[eventType])) { + this.importEvents(); + } + + // add default handler if in selection mode + if (eventType === 'click' && seriesOptions.allowPointSelect) { + defaultFunction = function (event) { + // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera + if (point.select) { // Could be destroyed by prior event handlers (#2911) + point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); + } + }; + } + + fireEvent(this, eventType, eventArgs, defaultFunction); + }, + + /** + * For certain series types, like pie charts, where individual points can + * be shown or hidden. + * + * @name visible + * @memberOf Highcharts.Point + * @type {Boolean} + */ + visible: true +}; + +/** + * For categorized axes this property holds the category name for the + * point. For other axes it holds the X value. + * + * @name category + * @memberOf Highcharts.Point + * @type {String|Number} + */ + +/** + * The name of the point. The name can be given as the first position of the + * point configuration array, or as a `name` property in the configuration: + * + * @example + * // Array config + * data: [ + * ['John', 1], + * ['Jane', 2] + * ] + * + * // Object config + * data: [{ + * name: 'John', + * y: 1 + * }, { + * name: 'Jane', + * y: 2 + * }] + * + * @name name + * @memberOf Highcharts.Point + * @type {String} + */ + + +/** + * The percentage for points in a stacked series or pies. + * + * @name percentage + * @memberOf Highcharts.Point + * @type {Number} + */ + +/** + * The total of values in either a stack for stacked series, or a pie in a pie + * series. + * + * @name total + * @memberOf Highcharts.Point + * @type {Number} + */ + +/** + * The x value of the point. + * + * @name x + * @memberOf Highcharts.Point + * @type {Number} + */ + +/** + * The y value of the point. + * + * @name y + * @memberOf Highcharts.Point + * @type {Number} + */ + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var addEvent = H.addEvent, + animObject = H.animObject, + arrayMax = H.arrayMax, + arrayMin = H.arrayMin, + correctFloat = H.correctFloat, + defaultOptions = H.defaultOptions, + defaultPlotOptions = H.defaultPlotOptions, + defined = H.defined, + each = H.each, + erase = H.erase, + extend = H.extend, + fireEvent = H.fireEvent, + grep = H.grep, + isArray = H.isArray, + isNumber = H.isNumber, + isString = H.isString, + LegendSymbolMixin = H.LegendSymbolMixin, // @todo add as a requirement + merge = H.merge, + objectEach = H.objectEach, + pick = H.pick, + Point = H.Point, // @todo add as a requirement + removeEvent = H.removeEvent, + splat = H.splat, + SVGElement = H.SVGElement, + syncTimeout = H.syncTimeout, + win = H.win; + +/** + * This is the base series prototype that all other series types inherit from. + * A new series is initialized either through the + * {@link https://api.highcharts.com/highcharts/series|series} option structure, + * or after the chart is initialized, through + * {@link Highcharts.Chart#addSeries}. + * + * The object can be accessed in a number of ways. All series and point event + * handlers give a reference to the `series` object. The chart object has a + * {@link Highcharts.Chart.series|series} property that is a collection of all + * the chart's series. The point objects and axis objects also have the same + * reference. + * + * Another way to reference the series programmatically is by `id`. Add an id + * in the series configuration options, and get the series object by {@link + * Highcharts.Chart#get}. + * + * Configuration options for the series are given in three levels. Options for + * all series in a chart are given in the + * {@link https://api.highcharts.com/highcharts/plotOptions.series| + * plotOptions.series} object. Then options for all series of a specific type + * are given in the plotOptions of that type, for example `plotOptions.line`. + * Next, options for one single series are given in the series array, or as + * arguements to `chart.addSeries`. + * + * The data in the series is stored in various arrays. + * + * - First, `series.options.data` contains all the original config options for + * each point whether added by options or methods like `series.addPoint`. + * - Next, `series.data` contains those values converted to points, but in case + * the series data length exceeds the `cropThreshold`, or if the data is + * grouped, `series.data` doesn't contain all the points. It only contains the + * points that have been created on demand. + * - Then there's `series.points` that contains all currently visible point + * objects. In case of cropping, the cropped-away points are not part of this + * array. The `series.points` array starts at `series.cropStart` compared to + * `series.data` and `series.options.data`. If however the series data is + * grouped, these can't be correlated one to one. + * - `series.xData` and `series.processedXData` contain clean x values, + * equivalent to `series.data` and `series.points`. + * - `series.yData` and `series.processedYData` contain clean y values, + * equivalent to `series.data` and `series.points`. + * + * @class Highcharts.Series + * @param {Highcharts.Chart} chart + * The chart instance. + * @param {Options.plotOptions.series} options + * The series options. + * + */ + +/** + * General options for all series types. + * @optionparent plotOptions.series + */ +H.Series = H.seriesType('line', null, { // base series options + + /** + * The SVG value used for the `stroke-linecap` and `stroke-linejoin` + * of a line graph. Round means that lines are rounded in the ends and + * bends. + * + * @validvalue ["round", "butt", "square"] + * @type {String} + * @default round + * @since 3.0.7 + * @apioption plotOptions.line.linecap + */ + + /** + * Pixel with of the graph line. + * + * @type {Number} + * @see In styled mode, the line stroke-width can be set with the + * `.highcharts-graph` class name. + * @sample {highcharts} highcharts/plotoptions/series-linewidth-general/ + * On all series + * @sample {highcharts} highcharts/plotoptions/series-linewidth-specific/ + * On one single series + * @default 2 + * @product highcharts highstock + */ + lineWidth: 2, + + + /** + * For some series, there is a limit that shuts down initial animation + * by default when the total number of points in the chart is too high. + * For example, for a column chart and its derivatives, animation doesn't + * run if there is more than 250 points totally. To disable this cap, set + * `animationLimit` to `Infinity`. + * + * @type {Number} + * @apioption plotOptions.series.animationLimit + */ + + /** + * Allow this series' points to be selected by clicking on the graphic + * (columns, point markers, pie slices, map areas etc). + * + * @see [Chart#getSelectedPoints] + * (../class-reference/Highcharts.Chart#getSelectedPoints). + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-allowpointselect-line/ + * Line + * @sample {highcharts} + * highcharts/plotoptions/series-allowpointselect-column/ + * Column + * @sample {highcharts} highcharts/plotoptions/series-allowpointselect-pie/ + * Pie + * @sample {highmaps} maps/plotoptions/series-allowpointselect/ + * Map area + * @sample {highmaps} maps/plotoptions/mapbubble-allowpointselect/ + * Map bubble + * @default false + * @since 1.2.0 + */ + allowPointSelect: false, + + + + /** + * If true, a checkbox is displayed next to the legend item to allow + * selecting the series. The state of the checkbox is determined by + * the `selected` option. + * + * @productdesc {highmaps} + * Note that if a `colorAxis` is defined, the color axis is represented in + * the legend, not the series. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-showcheckbox-true/ + * Show select box + * @default false + * @since 1.2.0 + */ + showCheckbox: false, + + + + /** + * Enable or disable the initial animation when a series is displayed. + * The animation can also be set as a configuration object. Please + * note that this option only applies to the initial animation of the + * series itself. For other animations, see [chart.animation](#chart. + * animation) and the animation parameter under the API methods. The + * following properties are supported: + * + *
+ * + *
duration
+ * + *
The duration of the animation in milliseconds.
+ * + *
easing
+ * + *
A string reference to an easing function set on the `Math` object. + * See the _Custom easing function_ demo below.
+ * + *
+ * + * Due to poor performance, animation is disabled in old IE browsers + * for several chart types. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-animation-disabled/ + * Animation disabled + * @sample {highcharts} highcharts/plotoptions/series-animation-slower/ + * Slower animation + * @sample {highcharts} highcharts/plotoptions/series-animation-easing/ + * Custom easing function + * @sample {highstock} stock/plotoptions/animation-slower/ + * Slower animation + * @sample {highstock} stock/plotoptions/animation-easing/ + * Custom easing function + * @sample {highmaps} maps/plotoptions/series-animation-true/ + * Animation enabled on map series + * @sample {highmaps} maps/plotoptions/mapbubble-animation-false/ + * Disabled on mapbubble series + * @default {highcharts} true + * @default {highstock} true + * @default {highmaps} false + */ + animation: { + duration: 1000 + }, + + /** + * A class name to apply to the series' graphical elements. + * + * @type {String} + * @since 5.0.0 + * @apioption plotOptions.series.className + */ + + /** + * The main color of the series. In line type series it applies to the + * line and the point markers unless otherwise specified. In bar type + * series it applies to the bars unless a color is specified per point. + * The default value is pulled from the `options.colors` array. + * + * In styled mode, the color can be defined by the + * [colorIndex](#plotOptions.series.colorIndex) option. Also, the series + * color can be set with the `.highcharts-series`, `.highcharts-color-{n}`, + * `.highcharts-{type}-series` or `.highcharts-series-{n}` class, or + * individual classes given by the `className` option. + * + * @productdesc {highmaps} + * In maps, the series color is rarely used, as most choropleth maps use the + * color to denote the value of each point. The series color can however be + * used in a map with multiple series holding categorized data. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-color-general/ + * General plot option + * @sample {highcharts} highcharts/plotoptions/series-color-specific/ + * One specific series + * @sample {highcharts} highcharts/plotoptions/series-color-area/ + * Area color + * @sample {highmaps} maps/demo/category-map/ + * Category map by multiple series + * @apioption plotOptions.series.color + */ + + /** + * Styled mode only. A specific color index to use for the series, so its + * graphic representations are given the class name `highcharts-color- + * {n}`. + * + * @type {Number} + * @since 5.0.0 + * @apioption plotOptions.series.colorIndex + */ + + + /** + * Whether to connect a graph line across null points, or render a gap + * between the two points on either side of the null. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-connectnulls-false/ + * False by default + * @sample {highcharts} highcharts/plotoptions/series-connectnulls-true/ + * True + * @product highcharts highstock + * @apioption plotOptions.series.connectNulls + */ + + + /** + * You can set the cursor to "pointer" if you have click events attached + * to the series, to signal to the user that the points and lines can + * be clicked. + * + * @validvalue [null, "default", "none", "help", "pointer", "crosshair"] + * @type {String} + * @see In styled mode, the series cursor can be set with the same classes + * as listed under [series.color](#plotOptions.series.color). + * @sample {highcharts} highcharts/plotoptions/series-cursor-line/ + * On line graph + * @sample {highcharts} highcharts/plotoptions/series-cursor-column/ + * On columns + * @sample {highcharts} highcharts/plotoptions/series-cursor-scatter/ + * On scatter markers + * @sample {highstock} stock/plotoptions/cursor/ + * Pointer on a line graph + * @sample {highmaps} maps/plotoptions/series-allowpointselect/ + * Map area + * @sample {highmaps} maps/plotoptions/mapbubble-allowpointselect/ + * Map bubble + * @apioption plotOptions.series.cursor + */ + + + /** + * A name for the dash style to use for the graph, or for some series types + * the outline of each shape. The value for the `dashStyle` include: + * + * * Solid + * * ShortDash + * * ShortDot + * * ShortDashDot + * * ShortDashDotDot + * * Dot + * * Dash + * * LongDash + * * DashDot + * * LongDashDot + * * LongDashDotDot + * + * @validvalue ["Solid", "ShortDash", "ShortDot", "ShortDashDot", + * "ShortDashDotDot", "Dot", "Dash" ,"LongDash", "DashDot", + * "LongDashDot", "LongDashDotDot"] + * @type {String} + * @see In styled mode, the [stroke dash-array](http://jsfiddle.net/gh/get/ + * library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/ + * series-dashstyle/) can be set with the same classes as listed under + * [series.color](#plotOptions.series.color). + * + * @sample {highcharts} highcharts/plotoptions/series-dashstyle-all/ + * Possible values demonstrated + * @sample {highcharts} highcharts/plotoptions/series-dashstyle/ + * Chart suitable for printing in black and white + * @sample {highstock} highcharts/plotoptions/series-dashstyle-all/ + * Possible values demonstrated + * @sample {highmaps} highcharts/plotoptions/series-dashstyle-all/ + * Possible values demonstrated + * @sample {highmaps} maps/plotoptions/series-dashstyle/ + * Dotted borders on a map + * @default Solid + * @since 2.1 + * @apioption plotOptions.series.dashStyle + */ + + /** + * Requires the Accessibility module. + * + * A description of the series to add to the screen reader information + * about the series. + * + * @type {String} + * @default undefined + * @since 5.0.0 + * @apioption plotOptions.series.description + */ + + + + + + /** + * Enable or disable the mouse tracking for a specific series. This + * includes point tooltips and click events on graphs and points. For + * large datasets it improves performance. + * + * @type {Boolean} + * @sample {highcharts} + * highcharts/plotoptions/series-enablemousetracking-false/ + * No mouse tracking + * @sample {highmaps} + * maps/plotoptions/series-enablemousetracking-false/ + * No mouse tracking + * @default true + * @apioption plotOptions.series.enableMouseTracking + */ + + /** + * By default, series are exposed to screen readers as regions. By enabling + * this option, the series element itself will be exposed in the same + * way as the data points. This is useful if the series is not used + * as a grouping entity in the chart, but you still want to attach a + * description to the series. + * + * Requires the Accessibility module. + * + * @type {Boolean} + * @sample highcharts/accessibility/art-grants/ + * Accessible data visualization + * @default undefined + * @since 5.0.12 + * @apioption plotOptions.series.exposeElementToA11y + */ + + /** + * Whether to use the Y extremes of the total chart width or only the + * zoomed area when zooming in on parts of the X axis. By default, the + * Y axis adjusts to the min and max of the visible data. Cartesian + * series only. + * + * @type {Boolean} + * @default false + * @since 4.1.6 + * @product highcharts highstock + * @apioption plotOptions.series.getExtremesFromAll + */ + + /** + * An id for the series. This can be used after render time to get a + * pointer to the series object through `chart.get()`. + * + * @type {String} + * @sample {highcharts} highcharts/plotoptions/series-id/ Get series by id + * @since 1.2.0 + * @apioption series.id + */ + + /** + * The index of the series in the chart, affecting the internal index + * in the `chart.series` array, the visible Z index as well as the order + * in the legend. + * + * @type {Number} + * @default undefined + * @since 2.3.0 + * @apioption series.index + */ + + /** + * An array specifying which option maps to which key in the data point + * array. This makes it convenient to work with unstructured data arrays + * from different sources. + * + * @type {Array} + * @see [series.data](#series.line.data) + * @sample {highcharts|highstock} highcharts/series/data-keys/ + * An extended data array with keys + * @since 4.1.6 + * @product highcharts highstock + * @apioption plotOptions.series.keys + */ + + /** + * The sequential index of the series in the legend. + * + * @sample {highcharts|highstock} highcharts/series/legendindex/ + * Legend in opposite order + * @type {Number} + * @see [legend.reversed](#legend.reversed), [yAxis.reversedStacks](#yAxis. + * reversedStacks) + * @apioption series.legendIndex + */ + + /** + * The line cap used for line ends and line joins on the graph. + * + * @validvalue ["round", "square"] + * @type {String} + * @default round + * @product highcharts highstock + * @apioption plotOptions.series.linecap + */ + + /** + * The [id](#series.id) of another series to link to. Additionally, + * the value can be ":previous" to link to the previous series. When + * two series are linked, only the first one appears in the legend. + * Toggling the visibility of this also toggles the linked series. + * + * @type {String} + * @sample {highcharts} highcharts/demo/arearange-line/ Linked series + * @sample {highstock} highcharts/demo/arearange-line/ Linked series + * @since 3.0 + * @product highcharts highstock + * @apioption plotOptions.series.linkedTo + */ + + /** + * The name of the series as shown in the legend, tooltip etc. + * + * @type {String} + * @sample {highcharts} highcharts/series/name/ Series name + * @sample {highmaps} maps/demo/category-map/ Series name + * @apioption series.name + */ + + /** + * The color for the parts of the graph or points that are below the + * [threshold](#plotOptions.series.threshold). + * + * @type {Color} + * @see In styled mode, a negative color is applied by setting this + * option to `true` combined with the `.highcharts-negative` class name. + * + * @sample {highcharts} highcharts/plotoptions/series-negative-color/ + * Spline, area and column + * @sample {highcharts} highcharts/plotoptions/arearange-negativecolor/ + * Arearange + * @sample {highcharts} highcharts/css/series-negative-color/ + * Styled mode + * @sample {highstock} highcharts/plotoptions/series-negative-color/ + * Spline, area and column + * @sample {highstock} highcharts/plotoptions/arearange-negativecolor/ + * Arearange + * @sample {highmaps} highcharts/plotoptions/series-negative-color/ + * Spline, area and column + * @sample {highmaps} highcharts/plotoptions/arearange-negativecolor/ + * Arearange + * @default null + * @since 3.0 + * @apioption plotOptions.series.negativeColor + */ + + /** + * Same as [accessibility.pointDescriptionFormatter](#accessibility. + * pointDescriptionFormatter), but for an individual series. Overrides + * the chart wide configuration. + * + * @type {Function} + * @since 5.0.12 + * @apioption plotOptions.series.pointDescriptionFormatter + */ + + /** + * If no x values are given for the points in a series, `pointInterval` + * defines the interval of the x values. For example, if a series contains + * one value every decade starting from year 0, set `pointInterval` to + * `10`. In true `datetime` axes, the `pointInterval` is set in + * milliseconds. + * + * It can be also be combined with `pointIntervalUnit` to draw irregular + * time intervals. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-pointstart-datetime/ + * Datetime X axis + * @sample {highstock} stock/plotoptions/pointinterval-pointstart/ + * Using pointStart and pointInterval + * @default 1 + * @product highcharts highstock + * @apioption plotOptions.series.pointInterval + */ + + /** + * On datetime series, this allows for setting the + * [pointInterval](#plotOptions.series.pointInterval) to irregular time + * units, `day`, `month` and `year`. A day is usually the same as 24 hours, + * but `pointIntervalUnit` also takes the DST crossover into consideration + * when dealing with local time. Combine this option with `pointInterval` + * to draw weeks, quarters, 6 months, 10 years etc. + * + * @validvalue [null, "day", "month", "year"] + * @type {String} + * @sample {highcharts} highcharts/plotoptions/series-pointintervalunit/ + * One point a month + * @sample {highstock} highcharts/plotoptions/series-pointintervalunit/ + * One point a month + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.pointIntervalUnit + */ + + /** + * Possible values: `null`, `"on"`, `"between"`. + * + * In a column chart, when pointPlacement is `"on"`, the point will + * not create any padding of the X axis. In a polar column chart this + * means that the first column points directly north. If the pointPlacement + * is `"between"`, the columns will be laid out between ticks. This + * is useful for example for visualising an amount between two points + * in time or in a certain sector of a polar chart. + * + * Since Highcharts 3.0.2, the point placement can also be numeric, + * where 0 is on the axis value, -0.5 is between this value and the + * previous, and 0.5 is between this value and the next. Unlike the + * textual options, numeric point placement options won't affect axis + * padding. + * + * Note that pointPlacement needs a [pointRange](#plotOptions.series. + * pointRange) to work. For column series this is computed, but for + * line-type series it needs to be set. + * + * Defaults to `null` in cartesian charts, `"between"` in polar charts. + * + * @validvalue [null, "on", "between"] + * @type {String|Number} + * @see [xAxis.tickmarkPlacement](#xAxis.tickmarkPlacement) + * @sample {highcharts|highstock} + * highcharts/plotoptions/series-pointplacement-between/ + * Between in a column chart + * @sample {highcharts|highstock} + * highcharts/plotoptions/series-pointplacement-numeric/ + * Numeric placement for custom layout + * @default null + * @since 2.3.0 + * @product highcharts highstock + * @apioption plotOptions.series.pointPlacement + */ + + /** + * If no x values are given for the points in a series, pointStart defines + * on what value to start. For example, if a series contains one yearly + * value starting from 1945, set pointStart to 1945. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-pointstart-linear/ + * Linear + * @sample {highcharts} highcharts/plotoptions/series-pointstart-datetime/ + * Datetime + * @sample {highstock} stock/plotoptions/pointinterval-pointstart/ + * Using pointStart and pointInterval + * @default 0 + * @product highcharts highstock + * @apioption plotOptions.series.pointStart + */ + + /** + * Whether to select the series initially. If `showCheckbox` is true, + * the checkbox next to the series name in the legend will be checked for a + * selected series. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-selected/ + * One out of two series selected + * @default false + * @since 1.2.0 + * @apioption plotOptions.series.selected + */ + + /** + * Whether to apply a drop shadow to the graph line. Since 2.3 the shadow + * can be an object configuration containing `color`, `offsetX`, `offsetY`, + * `opacity` and `width`. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/plotoptions/series-shadow/ Shadow enabled + * @default false + * @apioption plotOptions.series.shadow + */ + + /** + * Whether to display this particular series or series type in the legend. + * The default value is `true` for standalone series, `false` for linked + * series. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-showinlegend/ + * One series in the legend, one hidden + * @default true + * @apioption plotOptions.series.showInLegend + */ + + /** + * If set to `True`, the accessibility module will skip past the points + * in this series for keyboard navigation. + * + * @type {Boolean} + * @since 5.0.12 + * @apioption plotOptions.series.skipKeyboardNavigation + */ + + /** + * This option allows grouping series in a stacked chart. The stack + * option can be a string or a number or anything else, as long as the + * grouped series' stack options match each other. + * + * @type {String} + * @sample {highcharts} highcharts/series/stack/ Stacked and grouped columns + * @default null + * @since 2.1 + * @product highcharts highstock + * @apioption series.stack + */ + + /** + * Whether to stack the values of each series on top of each other. + * Possible values are `null` to disable, `"normal"` to stack by value or + * `"percent"`. When stacking is enabled, data must be sorted in ascending + * X order. A special stacking option is with the streamgraph series type, + * where the stacking option is set to `"stream"`. + * + * @validvalue [null, "normal", "percent"] + * @type {String} + * @see [yAxis.reversedStacks](#yAxis.reversedStacks) + * @sample {highcharts} highcharts/plotoptions/series-stacking-line/ + * Line + * @sample {highcharts} highcharts/plotoptions/series-stacking-column/ + * Column + * @sample {highcharts} highcharts/plotoptions/series-stacking-bar/ + * Bar + * @sample {highcharts} highcharts/plotoptions/series-stacking-area/ + * Area + * @sample {highcharts} highcharts/plotoptions/series-stacking-percent-line/ + * Line + * @sample {highcharts} + * highcharts/plotoptions/series-stacking-percent-column/ + * Column + * @sample {highcharts} highcharts/plotoptions/series-stacking-percent-bar/ + * Bar + * @sample {highcharts} highcharts/plotoptions/series-stacking-percent-area/ + * Area + * @sample {highstock} stock/plotoptions/stacking/ + * Area + * @default null + * @product highcharts highstock + * @apioption plotOptions.series.stacking + */ + + /** + * Whether to apply steps to the line. Possible values are `left`, `center` + * and `right`. + * + * @validvalue [null, "left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/plotoptions/line-step/ + * Different step line options + * @sample {highcharts} highcharts/plotoptions/area-step/ + * Stepped, stacked area + * @sample {highstock} stock/plotoptions/line-step/ + * Step line + * @default {highcharts} null + * @default {highstock} false + * @since 1.2.5 + * @product highcharts highstock + * @apioption plotOptions.series.step + */ + + /** + * The threshold, also called zero level or base level. For line type + * series this is only used in conjunction with + * [negativeColor](#plotOptions.series.negativeColor). + * + * @type {Number} + * @see [softThreshold](#plotOptions.series.softThreshold). + * @default 0 + * @since 3.0 + * @product highcharts highstock + * @apioption plotOptions.series.threshold + */ + + /** + * The type of series, for example `line` or `column`. By default, the + * series type is inherited from [chart.type](#chart.type), so unless the + * chart is a combination of series types, there is no need to set it on the + * series level. + * + * @validvalue [null, "line", "spline", "column", "area", "areaspline", + * "pie", "arearange", "areasplinerange", "boxplot", "bubble", + * "columnrange", "errorbar", "funnel", "gauge", "scatter", + * "waterfall"] + * @type {String} + * @sample {highcharts} highcharts/series/type/ + * Line and column in the same chart + * @sample {highmaps} maps/demo/mapline-mappoint/ + * Multiple types in the same map + * @apioption series.type + */ + + /** + * Set the initial visibility of the series. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-visible/ + * Two series, one hidden and one visible + * @sample {highstock} stock/plotoptions/series-visibility/ + * Hidden series + * @default true + * @apioption plotOptions.series.visible + */ + + /** + * When using dual or multiple x axes, this number defines which xAxis + * the particular series is connected to. It refers to either the [axis + * id](#xAxis.id) or the index of the axis in the xAxis array, with + * 0 being the first. + * + * @type {Number|String} + * @default 0 + * @product highcharts highstock + * @apioption series.xAxis + */ + + /** + * When using dual or multiple y axes, this number defines which yAxis + * the particular series is connected to. It refers to either the [axis + * id](#yAxis.id) or the index of the axis in the yAxis array, with + * 0 being the first. + * + * @type {Number|String} + * @sample {highcharts} highcharts/series/yaxis/ + * Apply the column series to the secondary Y axis + * @default 0 + * @product highcharts highstock + * @apioption series.yAxis + */ + + /** + * Defines the Axis on which the zones are applied. + * + * @type {String} + * @see [zones](#plotOptions.series.zones) + * @sample {highcharts} highcharts/series/color-zones-zoneaxis-x/ + * Zones on the X-Axis + * @sample {highstock} highcharts/series/color-zones-zoneaxis-x/ + * Zones on the X-Axis + * @default y + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zoneAxis + */ + + /** + * Define the visual z index of the series. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-zindex-default/ + * With no z index, the series defined last are on top + * @sample {highcharts} highcharts/plotoptions/series-zindex/ + * With a z index, the series with the highest z index is on top + * @sample {highstock} highcharts/plotoptions/series-zindex-default/ + * With no z index, the series defined last are on top + * @sample {highstock} highcharts/plotoptions/series-zindex/ + * With a z index, the series with the highest z index is on top + * @product highcharts highstock + * @apioption series.zIndex + */ + + /** + * General event handlers for the series items. These event hooks can also + * be attached to the series at run time using the `Highcharts.addEvent` + * function. + */ + events: { + + /** + * Fires after the series has finished its initial animation, or in + * case animation is disabled, immediately as the series is displayed. + * + * @type {Function} + * @context Series + * @sample {highcharts} + * highcharts/plotoptions/series-events-afteranimate/ + * Show label after animate + * @sample {highstock} + * highcharts/plotoptions/series-events-afteranimate/ + * Show label after animate + * @since 4.0 + * @product highcharts highstock + * @apioption plotOptions.series.events.afterAnimate + */ + + /** + * Fires when the checkbox next to the series' name in the legend is + * clicked. One parameter, `event`, is passed to the function. The state + * of the checkbox is found by `event.checked`. The checked item is + * found by `event.item`. Return `false` to prevent the default action + * which is to toggle the select state of the series. + * + * @type {Function} + * @context Series + * @sample {highcharts} + * highcharts/plotoptions/series-events-checkboxclick/ + * Alert checkbox status + * @since 1.2.0 + * @apioption plotOptions.series.events.checkboxClick + */ + + /** + * Fires when the series is clicked. One parameter, `event`, is passed + * to the function, containing common event information. Additionally, + * `event.point` holds a pointer to the nearest point on the graph. + * + * @type {Function} + * @context Series + * @sample {highcharts} highcharts/plotoptions/series-events-click/ + * Alert click info + * @sample {highstock} stock/plotoptions/series-events-click/ + * Alert click info + * @sample {highmaps} maps/plotoptions/series-events-click/ + * Display click info in subtitle + * @apioption plotOptions.series.events.click + */ + + /** + * Fires when the series is hidden after chart generation time, either + * by clicking the legend item or by calling `.hide()`. + * + * @type {Function} + * @context Series + * @sample {highcharts} highcharts/plotoptions/series-events-hide/ + * Alert when the series is hidden by clicking the legend item + * @since 1.2.0 + * @apioption plotOptions.series.events.hide + */ + + /** + * Fires when the legend item belonging to the series is clicked. One + * parameter, `event`, is passed to the function. The default action + * is to toggle the visibility of the series. This can be prevented + * by returning `false` or calling `event.preventDefault()`. + * + * @type {Function} + * @context Series + * @sample {highcharts} + * highcharts/plotoptions/series-events-legenditemclick/ + * Confirm hiding and showing + * @apioption plotOptions.series.events.legendItemClick + */ + + /** + * Fires when the mouse leaves the graph. One parameter, `event`, is + * passed to the function, containing common event information. If the + * [stickyTracking](#plotOptions.series) option is true, `mouseOut` + * doesn't happen before the mouse enters another graph or leaves the + * plot area. + * + * @type {Function} + * @context Series + * @sample {highcharts} + * highcharts/plotoptions/series-events-mouseover-sticky/ + * With sticky tracking by default + * @sample {highcharts} + * highcharts/plotoptions/series-events-mouseover-no-sticky/ + * Without sticky tracking + * @apioption plotOptions.series.events.mouseOut + */ + + /** + * Fires when the mouse enters the graph. One parameter, `event`, is + * passed to the function, containing common event information. + * + * @type {Function} + * @context Series + * @sample {highcharts} + * highcharts/plotoptions/series-events-mouseover-sticky/ + * With sticky tracking by default + * @sample {highcharts} + * highcharts/plotoptions/series-events-mouseover-no-sticky/ + * Without sticky tracking + * @apioption plotOptions.series.events.mouseOver + */ + + /** + * Fires when the series is shown after chart generation time, either + * by clicking the legend item or by calling `.show()`. + * + * @type {Function} + * @context Series + * @sample {highcharts} highcharts/plotoptions/series-events-show/ + * Alert when the series is shown by clicking the legend item. + * @since 1.2.0 + * @apioption plotOptions.series.events.show + */ + + }, + + + + /** + * Options for the point markers of line-like series. Properties like + * `fillColor`, `lineColor` and `lineWidth` define the visual appearance + * of the markers. Other series types, like column series, don't have + * markers, but have visual options on the series level instead. + * + * In styled mode, the markers can be styled with the `.highcharts-point`, + * `.highcharts-point-hover` and `.highcharts-point-select` + * class names. + * + * @product highcharts highstock + */ + marker: { + + + + /** + * The width of the point marker's outline. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-marker-fillcolor/ + * 2px blue marker + * @default 0 + * @product highcharts highstock + */ + lineWidth: 0, + + + /** + * The color of the point marker's outline. When `null`, the series' + * or point's color is used. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-marker-fillcolor/ + * Inherit from series color (null) + * @product highcharts highstock + */ + lineColor: '#ffffff', + + /** + * The fill color of the point marker. When `null`, the series' or + * point's color is used. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-marker-fillcolor/ + * White fill + * @default null + * @product highcharts highstock + * @apioption plotOptions.series.marker.fillColor + */ + + + + /** + * Enable or disable the point marker. If `null`, the markers are hidden + * when the data is dense, and shown for more widespread data points. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-marker-enabled/ + * Disabled markers + * @sample {highcharts} + * highcharts/plotoptions/series-marker-enabled-false/ + * Disabled in normal state but enabled on hover + * @sample {highstock} stock/plotoptions/series-marker/ + * Enabled markers + * @default {highcharts} null + * @default {highstock} false + * @product highcharts highstock + * @apioption plotOptions.series.marker.enabled + */ + + /** + * Image markers only. Set the image width explicitly. When using this + * option, a `width` must also be set. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-width-height/ + * Fixed width and height + * @sample {highstock} + * highcharts/plotoptions/series-marker-width-height/ + * Fixed width and height + * @default null + * @since 4.0.4 + * @product highcharts highstock + * @apioption plotOptions.series.marker.height + */ + + /** + * A predefined shape or symbol for the marker. When null, the symbol + * is pulled from options.symbols. Other possible values are "circle", + * "square", "diamond", "triangle" and "triangle-down". + * + * Additionally, the URL to a graphic can be given on this form: + * "url(graphic.png)". Note that for the image to be applied to exported + * charts, its URL needs to be accessible by the export server. + * + * Custom callbacks for symbol path generation can also be added to + * `Highcharts.SVGRenderer.prototype.symbols`. The callback is then + * used by its method name, as shown in the demo. + * + * @validvalue [null, "circle", "square", "diamond", "triangle", + * "triangle-down"] + * @type {String} + * @sample {highcharts} highcharts/plotoptions/series-marker-symbol/ + * Predefined, graphic and custom markers + * @sample {highstock} highcharts/plotoptions/series-marker-symbol/ + * Predefined, graphic and custom markers + * @default null + * @product highcharts highstock + * @apioption plotOptions.series.marker.symbol + */ + + /** + * The radius of the point marker. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-marker-radius/ + * Bigger markers + * @default 4 + * @product highcharts highstock + */ + radius: 4, + + /** + * Image markers only. Set the image width explicitly. When using this + * option, a `height` must also be set. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-width-height/ + * Fixed width and height + * @sample {highstock} + * highcharts/plotoptions/series-marker-width-height/ + * Fixed width and height + * @default null + * @since 4.0.4 + * @product highcharts highstock + * @apioption plotOptions.series.marker.width + */ + + + /** + * States for a single point marker. + * @product highcharts highstock + */ + states: { + /** + * The hover state for a single point marker. + * @product highcharts highstock + */ + hover: { + + /** + * Animation when hovering over the marker. + * @type {Boolean|Object} + */ + animation: { + duration: 50 + }, + + /** + * Enable or disable the point marker. + * + * @type {Boolean} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-hover-enabled/ + * Disabled hover state + * @default true + * @product highcharts highstock + */ + enabled: true, + + /** + * The fill color of the marker in hover state. + * + * @type {Color} + * @default null + * @product highcharts highstock + * @apioption plotOptions.series.marker.states.hover.fillColor + */ + + /** + * The color of the point marker's outline. When `null`, the + * series' or point's color is used. + * + * @type {Color} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-hover-linecolor/ + * White fill color, black line color + * @default #ffffff + * @product highcharts highstock + * @apioption plotOptions.series.marker.states.hover.lineColor + */ + + /** + * The width of the point marker's outline. + * + * @type {Number} + * @sample {highcharts} + * highcharts/plotoptions/series-marker-states-hover-linewidth/ + * 3px line width + * @default 0 + * @product highcharts highstock + * @apioption plotOptions.series.marker.states.hover.lineWidth + */ + + /** + * The radius of the point marker. In hover state, it defaults to the + * normal state's radius + 2 as per the [radiusPlus](#plotOptions.series. + * marker.states.hover.radiusPlus) option. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-marker-states-hover-radius/ 10px radius + * @product highcharts highstock + * @apioption plotOptions.series.marker.states.hover.radius + */ + + /** + * The number of pixels to increase the radius of the hovered point. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-states-hover-linewidthplus/ 5 pixels greater radius on hover + * @sample {highstock} highcharts/plotoptions/series-states-hover-linewidthplus/ 5 pixels greater radius on hover + * @default 2 + * @since 4.0.3 + * @product highcharts highstock + */ + radiusPlus: 2, + + + + /** + * The additional line width for a hovered point. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-states-hover-linewidthplus/ 2 pixels wider on hover + * @sample {highstock} highcharts/plotoptions/series-states-hover-linewidthplus/ 2 pixels wider on hover + * @default 1 + * @since 4.0.3 + * @product highcharts highstock + */ + lineWidthPlus: 1 + + }, + + + + + /** + * The appearance of the point marker when selected. In order to + * allow a point to be selected, set the `series.allowPointSelect` + * option to true. + * + * @product highcharts highstock + */ + select: { + + /** + * Enable or disable visible feedback for selection. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-marker-states-select-enabled/ + * Disabled select state + * @default true + * @product highcharts highstock + * @apioption plotOptions.series.marker.states.select.enabled + */ + + /** + * The fill color of the point marker. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-marker-states-select-fillcolor/ + * Solid red discs for selected points + * @default null + * @product highcharts highstock + */ + fillColor: '#cccccc', + + + + /** + * The color of the point marker's outline. When `null`, the series' + * or point's color is used. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-marker-states-select-linecolor/ + * Red line color for selected points + * @default #000000 + * @product highcharts highstock + */ + lineColor: '#000000', + + + + /** + * The width of the point marker's outline. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-marker-states-select-linewidth/ + * 3px line width for selected points + * @default 0 + * @product highcharts highstock + */ + lineWidth: 2 + + /** + * The radius of the point marker. In hover state, it defaults to the + * normal state's radius + 2. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-marker-states-select-radius/ + * 10px radius for selected points + * @product highcharts highstock + * @apioption plotOptions.series.marker.states.select.radius + */ + + } + + } + }, + + + + /** + * Properties for each single point. + */ + point: { + + + /** + * Events for each single point. + */ + events: { + + /** + * Fires when a point is clicked. One parameter, `event`, is passed + * to the function, containing common event information. + * + * If the `series.allowPointSelect` option is true, the default action + * for the point's click event is to toggle the point's select state. + * Returning `false` cancels this action. + * + * @type {Function} + * @context Point + * @sample {highcharts} highcharts/plotoptions/series-point-events-click/ Click marker to alert values + * @sample {highcharts} highcharts/plotoptions/series-point-events-click-column/ Click column + * @sample {highcharts} highcharts/plotoptions/series-point-events-click-url/ Go to URL + * @sample {highmaps} maps/plotoptions/series-point-events-click/ Click marker to display values + * @sample {highmaps} maps/plotoptions/series-point-events-click-url/ Go to URL + * @apioption plotOptions.series.point.events.click + */ + + /** + * Fires when the mouse leaves the area close to the point. One parameter, + * `event`, is passed to the function, containing common event information. + * + * @type {Function} + * @context Point + * @sample {highcharts} highcharts/plotoptions/series-point-events-mouseover/ Show values in the chart's corner on mouse over + * @apioption plotOptions.series.point.events.mouseOut + */ + + /** + * Fires when the mouse enters the area close to the point. One parameter, + * `event`, is passed to the function, containing common event information. + * + * @type {Function} + * @context Point + * @sample {highcharts} highcharts/plotoptions/series-point-events-mouseover/ Show values in the chart's corner on mouse over + * @apioption plotOptions.series.point.events.mouseOver + */ + + /** + * Fires when the point is removed using the `.remove()` method. One + * parameter, `event`, is passed to the function. Returning `false` + * cancels the operation. + * + * @type {Function} + * @context Point + * @sample {highcharts} highcharts/plotoptions/series-point-events-remove/ Remove point and confirm + * @since 1.2.0 + * @apioption plotOptions.series.point.events.remove + */ + + /** + * Fires when the point is selected either programmatically or following + * a click on the point. One parameter, `event`, is passed to the function. + * Returning `false` cancels the operation. + * + * @type {Function} + * @context Point + * @sample {highcharts} highcharts/plotoptions/series-point-events-select/ Report the last selected point + * @sample {highmaps} maps/plotoptions/series-allowpointselect/ Report select and unselect + * @since 1.2.0 + * @apioption plotOptions.series.point.events.select + */ + + /** + * Fires when the point is unselected either programmatically or following + * a click on the point. One parameter, `event`, is passed to the function. + * Returning `false` cancels the operation. + * + * @type {Function} + * @context Point + * @sample {highcharts} highcharts/plotoptions/series-point-events-unselect/ Report the last unselected point + * @sample {highmaps} maps/plotoptions/series-allowpointselect/ Report select and unselect + * @since 1.2.0 + * @apioption plotOptions.series.point.events.unselect + */ + + /** + * Fires when the point is updated programmatically through the `.update()` + * method. One parameter, `event`, is passed to the function. The new + * point options can be accessed through `event.options`. Returning + * `false` cancels the operation. + * + * @type {Function} + * @context Point + * @sample {highcharts} highcharts/plotoptions/series-point-events-update/ Confirm point updating + * @since 1.2.0 + * @apioption plotOptions.series.point.events.update + */ + + } + }, + + + + /** + * Options for the series data labels, appearing next to each data + * point. + * + * In styled mode, the data labels can be styled wtih the `.highcharts-data-label-box` and `.highcharts-data-label` class names ([see example](http://jsfiddle. + * net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/series- + * datalabels)). + */ + dataLabels: { + + + /** + * The alignment of the data label compared to the point. If `right`, + * the right side of the label should be touching the point. For + * points with an extent, like columns, the alignments also dictates + * how to align it inside the box, as given with the [inside](#plotOptions. + * column.dataLabels.inside) option. Can be one of "left", "center" + * or "right". + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-align-left/ Left aligned + * @default center + */ + align: 'center', + + + /** + * Whether to allow data labels to overlap. To make the labels less + * sensitive for overlapping, the [dataLabels.padding](#plotOptions. + * series.dataLabels.padding) can be set to 0. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-allowoverlap-false/ Don't allow overlap + * @sample {highstock} highcharts/plotoptions/series-datalabels-allowoverlap-false/ Don't allow overlap + * @sample {highmaps} highcharts/plotoptions/series-datalabels-allowoverlap-false/ Don't allow overlap + * @default false + * @since 4.1.0 + * @apioption plotOptions.series.dataLabels.allowOverlap + */ + + + /** + * The border radius in pixels for the data label. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ Data labels box options + * @sample {highstock} highcharts/plotoptions/series-datalabels-box/ Data labels box options + * @sample {highmaps} maps/plotoptions/series-datalabels-box/ Data labels box options + * @default 0 + * @since 2.2.1 + * @apioption plotOptions.series.dataLabels.borderRadius + */ + + + /** + * The border width in pixels for the data label. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ Data labels box options + * @sample {highstock} highcharts/plotoptions/series-datalabels-box/ Data labels box options + * @default 0 + * @since 2.2.1 + * @apioption plotOptions.series.dataLabels.borderWidth + */ + + /** + * A class name for the data label. Particularly in styled mode, this can + * be used to give each series' or point's data label unique styling. + * In addition to this option, a default color class name is added + * so that we can give the labels a [contrast text shadow](http://jsfiddle. + * net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/data- + * label-contrast/). + * + * @type {String} + * @sample {highcharts} highcharts/css/series-datalabels/ Styling by CSS + * @sample {highstock} highcharts/css/series-datalabels/ Styling by CSS + * @sample {highmaps} highcharts/css/series-datalabels/ Styling by CSS + * @since 5.0.0 + * @apioption plotOptions.series.dataLabels.className + */ + + /** + * The text color for the data labels. Defaults to `null`. For certain series + * types, like column or map, the data labels can be drawn inside the points. + * In this case the data label will be drawn with maximum contrast by default. + * Additionally, it will be given a `text-outline` style with the opposite + * color, to further increase the contrast. This can be overridden by setting + * the `text-outline` style to `none` in the `dataLabels.style` option. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-color/ + * Red data labels + * @sample {highmaps} maps/demo/color-axis/ + * White data labels + * @apioption plotOptions.series.dataLabels.color + */ + + /** + * Whether to hide data labels that are outside the plot area. By default, + * the data label is moved inside the plot area according to the [overflow](#plotOptions. + * series.dataLabels.overflow) option. + * + * @type {Boolean} + * @default true + * @since 2.3.3 + * @apioption plotOptions.series.dataLabels.crop + */ + + /** + * Whether to defer displaying the data labels until the initial series + * animation has finished. + * + * @type {Boolean} + * @default true + * @since 4.0 + * @product highcharts highstock + * @apioption plotOptions.series.dataLabels.defer + */ + + /** + * Enable or disable the data labels. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-enabled/ Data labels enabled + * @sample {highmaps} maps/demo/color-axis/ Data labels enabled + * @default false + * @apioption plotOptions.series.dataLabels.enabled + */ + + /** + * A [format string](http://www.highcharts.com/docs/chart-concepts/labels- + * and-string-formatting) for the data label. Available variables are + * the same as for `formatter`. + * + * @type {String} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-format/ Add a unit + * @sample {highstock} highcharts/plotoptions/series-datalabels-format/ Add a unit + * @sample {highmaps} maps/plotoptions/series-datalabels-format/ Formatted value in the data label + * @default {highcharts} {y} + * @default {highstock} {y} + * @default {highmaps} {point.value} + * @since 3.0 + * @apioption plotOptions.series.dataLabels.format + */ + + /** + * Callback JavaScript function to format the data label. Note that + * if a `format` is defined, the format takes precedence and the formatter + * is ignored. Available data are: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
`this.percentage`Stacked series and pies only. The point's percentage of the + * total.
`this.point`The point object. The point name, if defined, is available + * through `this.point.name`.
`this.series`:The series object. The series name is available through `this. + * series.name`.
`this.total`Stacked series only. The total value at this point's x value. + *
`this.x`:The x value.
`this.y`:The y value.
+ * + * @type {Function} + * @sample {highmaps} maps/plotoptions/series-datalabels-format/ Formatted value + */ + formatter: function () { + return this.y === null ? '' : H.numberFormat(this.y, -1); + }, + + + + /** + * Styles for the label. The default `color` setting is `"contrast"`, + * which is a pseudo color that Highcharts picks up and applies the + * maximum contrast to the underlying point item, for example the + * bar in a bar chart. + * + * The `textOutline` is a pseudo property that + * applies an outline of the given width with the given color, which + * by default is the maximum contrast to the text. So a bright text + * color will result in a black text outline for maximum readability + * on a mixed background. In some cases, especially with grayscale + * text, the text outline doesn't work well, in which cases it can + * be disabled by setting it to `"none"`. When `useHTML` is true, the + * `textOutline` will not be picked up. In this, case, the same effect + * can be acheived through the `text-shadow` CSS property. + * + * @type {CSSObject} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-style/ + * Bold labels + * @sample {highmaps} maps/demo/color-axis/ Bold labels + * @default {"color": "contrast", "fontSize": "11px", "fontWeight": "bold", "textOutline": "1px contrast" } + * @since 4.1.0 + */ + style: { + fontSize: '11px', + fontWeight: 'bold', + color: 'contrast', + textOutline: '1px contrast' + }, + + /** + * The background color or gradient for the data label. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ Data labels box options + * @sample {highmaps} maps/plotoptions/series-datalabels-box/ Data labels box options + * @since 2.2.1 + * @apioption plotOptions.series.dataLabels.backgroundColor + */ + + /** + * The border color for the data label. Defaults to `undefined`. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ Data labels box options + * @sample {highstock} highcharts/plotoptions/series-datalabels-box/ Data labels box options + * @default undefined + * @since 2.2.1 + * @apioption plotOptions.series.dataLabels.borderColor + */ + + /** + * The shadow of the box. Works best with `borderWidth` or `backgroundColor`. + * Since 2.3 the shadow can be an object configuration containing `color`, + * `offsetX`, `offsetY`, `opacity` and `width`. + * + * @type {Boolean|Object} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ Data labels box options + * @sample {highstock} highcharts/plotoptions/series-datalabels-box/ Data labels box options + * @default false + * @since 2.2.1 + * @apioption plotOptions.series.dataLabels.shadow + */ + + + /** + * For points with an extent, like columns or map areas, whether to align the data + * label inside the box or to the actual value point. Defaults to `false` + * in most cases, `true` in stacked columns. + * + * @type {Boolean} + * @since 3.0 + * @apioption plotOptions.series.dataLabels.inside + */ + + /** + * How to handle data labels that flow outside the plot area. The default + * is `justify`, which aligns them inside the plot area. For columns + * and bars, this means it will be moved inside the bar. To display + * data labels outside the plot area, set `crop` to `false` and `overflow` + * to `"none"`. + * + * @validvalue ["justify", "none"] + * @type {String} + * @default justify + * @since 3.0.6 + * @apioption plotOptions.series.dataLabels.overflow + */ + + /** + * Text rotation in degrees. Note that due to a more complex structure, + * backgrounds, borders and padding will be lost on a rotated data + * label. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-rotation/ Vertical labels + * @default 0 + * @apioption plotOptions.series.dataLabels.rotation + */ + + /** + * Whether to [use HTML](http://www.highcharts.com/docs/chart-concepts/labels- + * and-string-formatting#html) to render the labels. + * + * @type {Boolean} + * @default false + * @apioption plotOptions.series.dataLabels.useHTML + */ + + /** + * The vertical alignment of a data label. Can be one of `top`, `middle` + * or `bottom`. The default value depends on the data, for instance + * in a column chart, the label is above positive values and below + * negative values. + * + * @validvalue ["top", "middle", "bottom"] + * @type {String} + * @since 2.3.3 + */ + verticalAlign: 'bottom', // above singular point + + + /** + * The x position offset of the label relative to the point. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-rotation/ Vertical and positioned + * @default 0 + */ + x: 0, + + + /** + * The y position offset of the label relative to the point. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-rotation/ Vertical and positioned + * @default -6 + */ + y: 0, + + + /** + * When either the `borderWidth` or the `backgroundColor` is set, + * this is the padding within the box. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-box/ Data labels box options + * @sample {highstock} highcharts/plotoptions/series-datalabels-box/ Data labels box options + * @sample {highmaps} maps/plotoptions/series-datalabels-box/ Data labels box options + * @default {highcharts} 5 + * @default {highstock} 5 + * @default {highmaps} 0 + * @since 2.2.1 + */ + padding: 5 + + /** + * The name of a symbol to use for the border around the label. Symbols + * are predefined functions on the Renderer object. + * + * @type {String} + * @sample {highcharts} highcharts/plotoptions/series-datalabels-shape/ A callout for annotations + * @sample {highstock} highcharts/plotoptions/series-datalabels-shape/ A callout for annotations + * @sample {highmaps} highcharts/plotoptions/series-datalabels-shape/ A callout for annotations (Highcharts demo) + * @default square + * @since 4.1.2 + * @apioption plotOptions.series.dataLabels.shape + */ + + /** + * The Z index of the data labels. The default Z index puts it above + * the series. Use a Z index of 2 to display it behind the series. + * + * @type {Number} + * @default 6 + * @since 2.3.5 + * @apioption plotOptions.series.dataLabels.zIndex + */ + + /** + * A declarative filter for which data labels to display. The + * declarative filter is designed for use when callback functions are + * not available, like when the chart options require a pure JSON + * structure or for use with graphical editors. For programmatic + * control, use the `formatter` instead, and return `false` to disable + * a single data label. + * + * @example + * filter: { + * property: 'percentage', + * operator: '>', + * value: 4 + * } + * + * @sample highcharts/demo/pie-monochrome + * Data labels filtered by percentage + * + * @type {Object} + * @since 6.0.3 + * @apioption plotOptions.series.dataLabels.filter + */ + + /** + * The point property to filter by. Point options are passed directly to + * properties, additionally there are `y` value, `percentage` and others + * listed under [Point](https://api.highcharts.com/class-reference/Highcharts.Point) + * members. + * + * @type {String} + * @apioption plotOptions.series.dataLabels.filter.property + */ + + /** + * The operator to compare by. Can be one of `>`, `<`, `>=`, `<=`, `==`, + * and `===`. + * + * @type {String} + * @validvalue [">", "<", ">=", "<=", "==", "===""] + * @apioption plotOptions.series.dataLabels.filter.operator + */ + + /** + * The value to compare against. + * + * @type {Mixed} + * @apioption plotOptions.series.dataLabels.filter.value + */ + }, + // draw points outside the plot area when the number of points is less than + // this + + + + /** + * When the series contains less points than the crop threshold, all + * points are drawn, even if the points fall outside the visible plot + * area at the current zoom. The advantage of drawing all points (including + * markers and columns), is that animation is performed on updates. + * On the other hand, when the series contains more points than the + * crop threshold, the series data is cropped to only contain points + * that fall within the plot area. The advantage of cropping away invisible + * points is to increase performance on large series. + * + * @type {Number} + * @default 300 + * @since 2.2 + * @product highcharts highstock + */ + cropThreshold: 300, + + + + /** + * The width of each point on the x axis. For example in a column chart + * with one value each day, the pointRange would be 1 day (= 24 * 3600 + * * 1000 milliseconds). This is normally computed automatically, but + * this option can be used to override the automatic value. + * + * @type {Number} + * @default 0 + * @product highstock + */ + pointRange: 0, + + /** + * When this is true, the series will not cause the Y axis to cross + * the zero plane (or [threshold](#plotOptions.series.threshold) option) + * unless the data actually crosses the plane. + * + * For example, if `softThreshold` is `false`, a series of 0, 1, 2, + * 3 will make the Y axis show negative values according to the `minPadding` + * option. If `softThreshold` is `true`, the Y axis starts at 0. + * + * @type {Boolean} + * @default true + * @since 4.1.9 + * @product highcharts highstock + */ + softThreshold: true, + + + + /** + * A wrapper object for all the series options in specific states. + * + * @type {plotOptions.series.states} + */ + states: { + + + /** + * Options for the hovered series. These settings override the normal + * state options when a series is moused over or touched. + * + */ + hover: { + + /** + * Enable separate styles for the hovered series to visualize that the + * user hovers either the series itself or the legend. . + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-states-hover-enabled/ Line + * @sample {highcharts} highcharts/plotoptions/series-states-hover-enabled-column/ Column + * @sample {highcharts} highcharts/plotoptions/series-states-hover-enabled-pie/ Pie + * @default true + * @since 1.2 + * @apioption plotOptions.series.states.hover.enabled + */ + + + /** + * Animation setting for hovering the graph in line-type series. + * + * @type {Boolean|Object} + * @default { "duration": 50 } + * @since 5.0.8 + * @product highcharts + */ + animation: { + /** + * The duration of the hover animation in milliseconds. By + * default the hover state animates quickly in, and slowly back + * to normal. + */ + duration: 50 + }, + + /** + * Pixel with of the graph line. By default this property is + * undefined, and the `lineWidthPlus` property dictates how much + * to increase the linewidth from normal state. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-states-hover-linewidth/ + * 5px line on hover + * @default undefined + * @product highcharts highstock + * @apioption plotOptions.series.states.hover.lineWidth + */ + + + /** + * The additional line width for the graph of a hovered series. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/series-states-hover-linewidthplus/ + * 5 pixels wider + * @sample {highstock} highcharts/plotoptions/series-states-hover-linewidthplus/ + * 5 pixels wider + * @default 1 + * @since 4.0.3 + * @product highcharts highstock + */ + lineWidthPlus: 1, + + + + /** + * In Highcharts 1.0, the appearance of all markers belonging to + * the hovered series. For settings on the hover state of the individual + * point, see [marker.states.hover](#plotOptions.series.marker.states. + * hover). + * + * @extends plotOptions.series.marker + * @deprecated + * @product highcharts highstock + */ + marker: { + // lineWidth: base + 1, + // radius: base + 1 + }, + + + + /** + * Options for the halo appearing around the hovered point in line- + * type series as well as outside the hovered slice in pie charts. + * By default the halo is filled by the current point or series + * color with an opacity of 0.25\. The halo can be disabled by setting + * the `halo` option to `false`. + * + * In styled mode, the halo is styled with the `.highcharts-halo` class, with colors inherited from `.highcharts-color-{n}`. + * + * @type {Object} + * @sample {highcharts} highcharts/plotoptions/halo/ Halo options + * @sample {highstock} highcharts/plotoptions/halo/ Halo options + * @since 4.0 + * @product highcharts highstock + */ + halo: { + + /** + * A collection of SVG attributes to override the appearance of the + * halo, for example `fill`, `stroke` and `stroke-width`. + * + * @type {Object} + * @since 4.0 + * @product highcharts highstock + * @apioption plotOptions.series.states.hover.halo.attributes + */ + + + /** + * The pixel size of the halo. For point markers this is the radius + * of the halo. For pie slices it is the width of the halo outside + * the slice. For bubbles it defaults to 5 and is the width of the + * halo outside the bubble. + * + * @type {Number} + * @default 10 + * @since 4.0 + * @product highcharts highstock + */ + size: 10, + + + + + /** + * Opacity for the halo unless a specific fill is overridden using + * the `attributes` setting. Note that Highcharts is only able to + * apply opacity to colors of hex or rgb(a) formats. + * + * @type {Number} + * @default 0.25 + * @since 4.0 + * @product highcharts highstock + */ + opacity: 0.25 + + } + }, + + + /** + * Specific options for point in selected states, after being selected + * by [allowPointSelect](#plotOptions.series.allowPointSelect) or + * programmatically. + * + * @type {Object} + * @extends plotOptions.series.states.hover + * @excluding brightness + * @sample {highmaps} maps/plotoptions/series-allowpointselect/ + * Allow point select demo + * @product highmaps + */ + select: { + marker: {} + } + }, + + + + /** + * Sticky tracking of mouse events. When true, the `mouseOut` event + * on a series isn't triggered until the mouse moves over another series, + * or out of the plot area. When false, the `mouseOut` event on a + * series is triggered when the mouse leaves the area around the series' + * graph or markers. This also implies the tooltip when not shared. When + * `stickyTracking` is false and `tooltip.shared` is false, the tooltip will + * be hidden when moving the mouse between series. Defaults to true for line + * and area type series, but to false for columns, pies etc. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-stickytracking-true/ + * True by default + * @sample {highcharts} highcharts/plotoptions/series-stickytracking-false/ + * False + * @default {highcharts} true + * @default {highstock} true + * @default {highmaps} false + * @since 2.0 + */ + stickyTracking: true, + + /** + * A configuration object for the tooltip rendering of each single series. + * Properties are inherited from [tooltip](#tooltip), but only the + * following properties can be defined on a series level. + * + * @type {Object} + * @extends tooltip + * @excluding animation,backgroundColor,borderColor,borderRadius,borderWidth,crosshairs,enabled,formatter,positioner,shadow,shared,shape,snap,style,useHTML + * @since 2.3 + * @apioption plotOptions.series.tooltip + */ + + /** + * When a series contains a data array that is longer than this, only + * one dimensional arrays of numbers, or two dimensional arrays with + * x and y values are allowed. Also, only the first point is tested, + * and the rest are assumed to be the same format. This saves expensive + * data checking and indexing in long series. Set it to `0` disable. + * + * @type {Number} + * @default 1000 + * @since 2.2 + * @product highcharts highstock + */ + turboThreshold: 1000, + + /** + * An array defining zones within a series. Zones can be applied to + * the X axis, Y axis or Z axis for bubbles, according to the `zoneAxis` + * option. + * + * In styled mode, the color zones are styled with the `.highcharts- + * zone-{n}` class, or custom classed from the `className` option ([view + * live demo](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/color- + * zones/)). + * + * @type {Array} + * @see [zoneAxis](#plotOptions.series.zoneAxis) + * @sample {highcharts} highcharts/series/color-zones-simple/ Color zones + * @sample {highstock} highcharts/series/color-zones-simple/ Color zones + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zones + */ + + /** + * Styled mode only. A custom class name for the zone. + * + * @type {String} + * @sample {highcharts} highcharts/css/color-zones/ Zones styled by class name + * @sample {highstock} highcharts/css/color-zones/ Zones styled by class name + * @sample {highmaps} highcharts/css/color-zones/ Zones styled by class name + * @since 5.0.0 + * @apioption plotOptions.series.zones.className + */ + + /** + * Defines the color of the series. + * + * @type {Color} + * @see [series color](#plotOptions.series.color) + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zones.color + */ + + /** + * A name for the dash style to use for the graph. + * + * @type {String} + * @see [series.dashStyle](#plotOptions.series.dashStyle) + * @sample {highcharts} highcharts/series/color-zones-dashstyle-dot/ + * Dashed line indicates prognosis + * @sample {highstock} highcharts/series/color-zones-dashstyle-dot/ + * Dashed line indicates prognosis + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zones.dashStyle + */ + + /** + * Defines the fill color for the series (in area type series) + * + * @type {Color} + * @see [fillColor](#plotOptions.area.fillColor) + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zones.fillColor + */ + + /** + * The value up to where the zone extends, if undefined the zones stretches + * to the last value in the series. + * + * @type {Number} + * @default undefined + * @since 4.1.0 + * @product highcharts highstock + * @apioption plotOptions.series.zones.value + */ + + + + /** + * Determines whether the series should look for the nearest point + * in both dimensions or just the x-dimension when hovering the series. + * Defaults to `'xy'` for scatter series and `'x'` for most other + * series. If the data has duplicate x-values, it is recommended to + * set this to `'xy'` to allow hovering over all points. + * + * Applies only to series types using nearest neighbor search (not + * direct hover) for tooltip. + * + * @validvalue ['x', 'xy'] + * @type {String} + * @sample {highcharts} highcharts/series/findnearestpointby/ + * Different hover behaviors + * @sample {highstock} highcharts/series/findnearestpointby/ + * Different hover behaviors + * @sample {highmaps} highcharts/series/findnearestpointby/ + * Different hover behaviors + * @since 5.0.10 + */ + findNearestPointBy: 'x' + +}, /** @lends Highcharts.Series.prototype */ { + isCartesian: true, + pointClass: Point, + sorted: true, // requires the data to be sorted + requireSorting: true, + directTouch: false, + axisTypes: ['xAxis', 'yAxis'], + colorCounter: 0, + // each point's x and y values are stored in this.xData and this.yData + parallelArrays: ['x', 'y'], + coll: 'series', + init: function (chart, options) { + var series = this, + events, + chartSeries = chart.series, + lastSeries; + + /** + * Read only. The chart that the series belongs to. + * + * @name chart + * @memberOf Series + * @type {Chart} + */ + series.chart = chart; + + /** + * Read only. The series' type, like "line", "area", "column" etc. The + * type in the series options anc can be altered using {@link + * Series#update}. + * + * @name type + * @memberOf Series + * @type String + */ + + /** + * Read only. The series' current options. To update, use {@link + * Series#update}. + * + * @name options + * @memberOf Series + * @type SeriesOptions + */ + series.options = options = series.setOptions(options); + series.linkedSeries = []; + + // bind the axes + series.bindAxes(); + + // set some variables + extend(series, { + /** + * The series name as given in the options. Defaults to + * "Series {n}". + * + * @name name + * @memberOf Series + * @type {String} + */ + name: options.name, + state: '', + /** + * Read only. The series' visibility state as set by {@link + * Series#show}, {@link Series#hide}, or in the initial + * configuration. + * + * @name visible + * @memberOf Series + * @type {Boolean} + */ + visible: options.visible !== false, // true by default + /** + * Read only. The series' selected state as set by {@link + * Highcharts.Series#select}. + * + * @name selected + * @memberOf Series + * @type {Boolean} + */ + selected: options.selected === true // false by default + }); + + // register event listeners + events = options.events; + + objectEach(events, function (event, eventType) { + addEvent(series, eventType, event); + }); + if ( + (events && events.click) || + ( + options.point && + options.point.events && + options.point.events.click + ) || + options.allowPointSelect + ) { + chart.runTrackerClick = true; + } + + series.getColor(); + series.getSymbol(); + + // Set the data + each(series.parallelArrays, function (key) { + series[key + 'Data'] = []; + }); + series.setData(options.data, false); + + // Mark cartesian + if (series.isCartesian) { + chart.hasCartesianSeries = true; + } + + // Get the index and register the series in the chart. The index is one + // more than the current latest series index (#5960). + if (chartSeries.length) { + lastSeries = chartSeries[chartSeries.length - 1]; + } + series._i = pick(lastSeries && lastSeries._i, -1) + 1; + + // Insert the series and re-order all series above the insertion point. + chart.orderSeries(this.insert(chartSeries)); + }, + + /** + * Insert the series in a collection with other series, either the chart + * series or yAxis series, in the correct order according to the index + * option. Used internally when adding series. + * + * @private + * @param {Array.} collection + * A collection of series, like `chart.series` or `xAxis.series`. + * @returns {Number} The index of the series in the collection. + */ + insert: function (collection) { + var indexOption = this.options.index, + i; + + // Insert by index option + if (isNumber(indexOption)) { + i = collection.length; + while (i--) { + // Loop down until the interted element has higher index + if (indexOption >= + pick(collection[i].options.index, collection[i]._i)) { + collection.splice(i + 1, 0, this); + break; + } + } + if (i === -1) { + collection.unshift(this); + } + i = i + 1; + + // Or just push it to the end + } else { + collection.push(this); + } + return pick(i, collection.length - 1); + }, + + /** + * Set the xAxis and yAxis properties of cartesian series, and register the + * series in the `axis.series` array. + * + * @private + */ + bindAxes: function () { + var series = this, + seriesOptions = series.options, + chart = series.chart, + axisOptions; + + // repeat for xAxis and yAxis + each(series.axisTypes || [], function (AXIS) { + + // loop through the chart's axis objects + each(chart[AXIS], function (axis) { + axisOptions = axis.options; + + // apply if the series xAxis or yAxis option mathches the number + // of the axis, or if undefined, use the first axis + if ( + seriesOptions[AXIS] === axisOptions.index || + ( + seriesOptions[AXIS] !== undefined && + seriesOptions[AXIS] === axisOptions.id + ) || + ( + seriesOptions[AXIS] === undefined && + axisOptions.index === 0 + ) + ) { + + // register this series in the axis.series lookup + series.insert(axis.series); + + // set this series.xAxis or series.yAxis reference + /** + * Read only. The unique xAxis object associated with the + * series. + * + * @name xAxis + * @memberOf Series + * @type Axis + */ + /** + * Read only. The unique yAxis object associated with the + * series. + * + * @name yAxis + * @memberOf Series + * @type Axis + */ + series[AXIS] = axis; + + // mark dirty for redraw + axis.isDirty = true; + } + }); + + // The series needs an X and an Y axis + if (!series[AXIS] && series.optionalAxis !== AXIS) { + H.error(18, true); + } + + }); + }, + + /** + * For simple series types like line and column, the data values are held in + * arrays like xData and yData for quick lookup to find extremes and more. + * For multidimensional series like bubble and map, this can be extended + * with arrays like zData and valueData by adding to the + * `series.parallelArrays` array. + * + * @private + */ + updateParallelArrays: function (point, i) { + var series = point.series, + args = arguments, + fn = isNumber(i) ? + // Insert the value in the given position + function (key) { + var val = key === 'y' && series.toYData ? + series.toYData(point) : + point[key]; + series[key + 'Data'][i] = val; + } : + // Apply the method specified in i with the following arguments + // as arguments + function (key) { + Array.prototype[i].apply( + series[key + 'Data'], + Array.prototype.slice.call(args, 2) + ); + }; + + each(series.parallelArrays, fn); + }, + + /** + * Return an auto incremented x value based on the pointStart and + * pointInterval options. This is only used if an x value is not given for + * the point that calls autoIncrement. + * + * @private + */ + autoIncrement: function () { + + var options = this.options, + xIncrement = this.xIncrement, + date, + pointInterval, + pointIntervalUnit = options.pointIntervalUnit, + dstCrossover = 0, + time = this.chart.time; + + xIncrement = pick(xIncrement, options.pointStart, 0); + + this.pointInterval = pointInterval = pick( + this.pointInterval, + options.pointInterval, + 1 + ); + + // Added code for pointInterval strings + if (pointIntervalUnit) { + date = new time.Date(xIncrement); + + if (pointIntervalUnit === 'day') { + date = +date[time.setDate]( + date[time.getDate]() + pointInterval + ); + } else if (pointIntervalUnit === 'month') { + date = +date[time.setMonth]( + date[time.getMonth]() + pointInterval + ); + } else if (pointIntervalUnit === 'year') { + date = +date[time.setFullYear]( + date[time.getFullYear]() + pointInterval + ); + } + + if (time.variableTimezone) { + dstCrossover = ( + time.getTimezoneOffset(date) - + time.getTimezoneOffset(xIncrement) + ); + } + pointInterval = date - xIncrement + dstCrossover; + + } + + this.xIncrement = xIncrement + pointInterval; + return xIncrement; + }, + + /** + * Set the series options by merging from the options tree. Called + * internally on initiating and updating series. This function will not + * redraw the series. For API usage, use {@link Series#update}. + * + * @param {Options.plotOptions.series} itemOptions + * The series options. + */ + setOptions: function (itemOptions) { + var chart = this.chart, + chartOptions = chart.options, + plotOptions = chartOptions.plotOptions, + userOptions = chart.userOptions || {}, + userPlotOptions = userOptions.plotOptions || {}, + typeOptions = plotOptions[this.type], + options, + zones; + + this.userOptions = itemOptions; + + // General series options take precedence over type options because + // otherwise, default type options like column.animation would be + // overwritten by the general option. But issues have been raised here + // (#3881), and the solution may be to distinguish between default + // option and userOptions like in the tooltip below. + options = merge( + typeOptions, + plotOptions.series, + itemOptions + ); + + // The tooltip options are merged between global and series specific + // options. Importance order asscendingly: + // globals: (1)tooltip, (2)plotOptions.series, (3)plotOptions[this.type] + // init userOptions with possible later updates: 4-6 like 1-3 and + // (7)this series options + this.tooltipOptions = merge( + defaultOptions.tooltip, // 1 + defaultOptions.plotOptions.series && + defaultOptions.plotOptions.series.tooltip, // 2 + defaultOptions.plotOptions[this.type].tooltip, // 3 + chartOptions.tooltip.userOptions, // 4 + plotOptions.series && plotOptions.series.tooltip, // 5 + plotOptions[this.type].tooltip, // 6 + itemOptions.tooltip // 7 + ); + + // When shared tooltip, stickyTracking is true by default, + // unless user says otherwise. + this.stickyTracking = pick( + itemOptions.stickyTracking, + userPlotOptions[this.type] && + userPlotOptions[this.type].stickyTracking, + userPlotOptions.series && userPlotOptions.series.stickyTracking, + ( + this.tooltipOptions.shared && !this.noSharedTooltip ? + true : + options.stickyTracking + ) + ); + + // Delete marker object if not allowed (#1125) + if (typeOptions.marker === null) { + delete options.marker; + } + + // Handle color zones + this.zoneAxis = options.zoneAxis; + zones = this.zones = (options.zones || []).slice(); + if ( + (options.negativeColor || options.negativeFillColor) && + !options.zones + ) { + zones.push({ + value: + options[this.zoneAxis + 'Threshold'] || + options.threshold || + 0, + className: 'highcharts-negative', + + color: options.negativeColor, + fillColor: options.negativeFillColor + + }); + } + if (zones.length) { // Push one extra zone for the rest + if (defined(zones[zones.length - 1].value)) { + zones.push({ + + color: this.color, + fillColor: this.fillColor + + }); + } + } + return options; + }, + + getCyclic: function (prop, value, defaults) { + var i, + chart = this.chart, + userOptions = this.userOptions, + indexName = prop + 'Index', + counterName = prop + 'Counter', + len = defaults ? defaults.length : pick( + chart.options.chart[prop + 'Count'], + chart[prop + 'Count'] + ), + setting; + + if (!value) { + // Pick up either the colorIndex option, or the _colorIndex after + // Series.update() + setting = pick( + userOptions[indexName], + userOptions['_' + indexName] + ); + if (defined(setting)) { // after Series.update() + i = setting; + } else { + // #6138 + if (!chart.series.length) { + chart[counterName] = 0; + } + userOptions['_' + indexName] = i = chart[counterName] % len; + chart[counterName] += 1; + } + if (defaults) { + value = defaults[i]; + } + } + // Set the colorIndex + if (i !== undefined) { + this[indexName] = i; + } + this[prop] = value; + }, + + /** + * Get the series' color based on either the options or pulled from global + * options. + * + * @return {Color} The series color. + */ + + getColor: function () { + if (this.options.colorByPoint) { + // #4359, selected slice got series.color even when colorByPoint was + // set. + this.options.color = null; + } else { + this.getCyclic( + 'color', + this.options.color || defaultPlotOptions[this.type].color, + this.chart.options.colors + ); + } + }, + + /** + * Get the series' symbol based on either the options or pulled from global + * options. + */ + getSymbol: function () { + var seriesMarkerOption = this.options.marker; + + this.getCyclic( + 'symbol', + seriesMarkerOption.symbol, + this.chart.options.symbols + ); + }, + + drawLegendSymbol: LegendSymbolMixin.drawLineMarker, + + /** + * Apply a new set of data to the series and optionally redraw it. The new + * data array is passed by reference (except in case of `updatePoints`), and + * may later be mutated when updating the chart data. + * + * Note the difference in behaviour when setting the same amount of points, + * or a different amount of points, as handled by the `updatePoints` + * parameter. + * + * @param {SeriesDataOptions} data + * Takes an array of data in the same format as described under + * `series.typedata` for the given series type. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the series is altered. If doing + * more operations on the chart, it is a good idea to set redraw to + * false and call {@link Chart#redraw} after. + * @param {AnimationOptions} [animation] + * When the updated data is the same length as the existing data, + * points will be updated by default, and animation visualizes how + * the points are changed. Set false to disable animation, or a + * configuration object to set duration or easing. + * @param {Boolean} [updatePoints=true] + * When the updated data is the same length as the existing data, + * points will be updated instead of replaced. This allows updating + * with animation and performs better. In this case, the original + * array is not passed by reference. Set false to prevent. + * + * @sample highcharts/members/series-setdata/ + * Set new data from a button + * @sample highcharts/members/series-setdata-pie/ + * Set data in a pie + * @sample stock/members/series-setdata/ + * Set new data in Highstock + * @sample maps/members/series-setdata/ + * Set new data in Highmaps + */ + setData: function (data, redraw, animation, updatePoints) { + var series = this, + oldData = series.points, + oldDataLength = (oldData && oldData.length) || 0, + dataLength, + options = series.options, + chart = series.chart, + firstPoint = null, + xAxis = series.xAxis, + i, + turboThreshold = options.turboThreshold, + pt, + xData = this.xData, + yData = this.yData, + pointArrayMap = series.pointArrayMap, + valueCount = pointArrayMap && pointArrayMap.length; + + data = data || []; + dataLength = data.length; + redraw = pick(redraw, true); + + // If the point count is the same as is was, just run Point.update which + // is cheaper, allows animation, and keeps references to points. + if ( + updatePoints !== false && + dataLength && + oldDataLength === dataLength && + !series.cropped && + !series.hasGroupedData && + series.visible + ) { + each(data, function (point, i) { + // .update doesn't exist on a linked, hidden series (#3709) + if (oldData[i].update && point !== options.data[i]) { + oldData[i].update(point, false, null, false); + } + }); + + } else { + + // Reset properties + series.xIncrement = null; + + series.colorCounter = 0; // for series with colorByPoint (#1547) + + // Update parallel arrays + each(this.parallelArrays, function (key) { + series[key + 'Data'].length = 0; + }); + + // In turbo mode, only one- or twodimensional arrays of numbers are + // allowed. The first value is tested, and we assume that all the + // rest are defined the same way. Although the 'for' loops are + // similar, they are repeated inside each if-else conditional for + // max performance. + if (turboThreshold && dataLength > turboThreshold) { + + // find the first non-null point + i = 0; + while (firstPoint === null && i < dataLength) { + firstPoint = data[i]; + i++; + } + + + if (isNumber(firstPoint)) { // assume all points are numbers + for (i = 0; i < dataLength; i++) { + xData[i] = this.autoIncrement(); + yData[i] = data[i]; + } + + // Assume all points are arrays when first point is + } else if (isArray(firstPoint)) { + if (valueCount) { // [x, low, high] or [x, o, h, l, c] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt.slice(1, valueCount + 1); + } + } else { // [x, y] + for (i = 0; i < dataLength; i++) { + pt = data[i]; + xData[i] = pt[0]; + yData[i] = pt[1]; + } + } + } else { + // Highcharts expects configs to be numbers or arrays in + // turbo mode + H.error(12); + } + } else { + for (i = 0; i < dataLength; i++) { + if (data[i] !== undefined) { // stray commas in oldIE + pt = { series: series }; + series.pointClass.prototype.applyOptions.apply( + pt, + [data[i]] + ); + series.updateParallelArrays(pt, i); + } + } + } + + // Forgetting to cast strings to numbers is a common caveat when + // handling CSV or JSON + if (yData && isString(yData[0])) { + H.error(14, true); + } + + series.data = []; + series.options.data = series.userOptions.data = data; + + // destroy old points + i = oldDataLength; + while (i--) { + if (oldData[i] && oldData[i].destroy) { + oldData[i].destroy(); + } + } + + // reset minRange (#878) + if (xAxis) { + xAxis.minRange = xAxis.userMinRange; + } + + // redraw + series.isDirty = chart.isDirtyBox = true; + series.isDirtyData = !!oldData; + animation = false; + } + + // Typically for pie series, points need to be processed and generated + // prior to rendering the legend + if (options.legendType === 'point') { + this.processData(); + this.generatePoints(); + } + + if (redraw) { + chart.redraw(animation); + } + }, + + /** + * Internal function to process the data by cropping away unused data points + * if the series is longer than the crop threshold. This saves computing + * time for large series. In Highstock, this function is extended to + * provide data grouping. + * + * @private + * @param {Boolean} force + * Force data grouping. + */ + processData: function (force) { + var series = this, + processedXData = series.xData, // copied during slice operation + processedYData = series.yData, + dataLength = processedXData.length, + croppedData, + cropStart = 0, + cropped, + distance, + closestPointRange, + xAxis = series.xAxis, + i, // loop variable + options = series.options, + cropThreshold = options.cropThreshold, + getExtremesFromAll = + series.getExtremesFromAll || + options.getExtremesFromAll, // #4599 + isCartesian = series.isCartesian, + xExtremes, + val2lin = xAxis && xAxis.val2lin, + isLog = xAxis && xAxis.isLog, + throwOnUnsorted = series.requireSorting, + min, + max; + + // If the series data or axes haven't changed, don't go through this. + // Return false to pass the message on to override methods like in data + // grouping. + if ( + isCartesian && + !series.isDirty && + !xAxis.isDirty && + !series.yAxis.isDirty && + !force + ) { + return false; + } + + if (xAxis) { + xExtremes = xAxis.getExtremes(); // corrected for log axis (#3053) + min = xExtremes.min; + max = xExtremes.max; + } + + // optionally filter out points outside the plot area + if ( + isCartesian && + series.sorted && + !getExtremesFromAll && + (!cropThreshold || dataLength > cropThreshold || series.forceCrop) + ) { + + // it's outside current extremes + if ( + processedXData[dataLength - 1] < min || + processedXData[0] > max + ) { + processedXData = []; + processedYData = []; + + // only crop if it's actually spilling out + } else if ( + processedXData[0] < min || + processedXData[dataLength - 1] > max + ) { + croppedData = this.cropData( + series.xData, + series.yData, + min, + max + ); + processedXData = croppedData.xData; + processedYData = croppedData.yData; + cropStart = croppedData.start; + cropped = true; + } + } + + + // Find the closest distance between processed points + i = processedXData.length || 1; + while (--i) { + distance = isLog ? + val2lin(processedXData[i]) - val2lin(processedXData[i - 1]) : + processedXData[i] - processedXData[i - 1]; + + if ( + distance > 0 && + ( + closestPointRange === undefined || + distance < closestPointRange + ) + ) { + closestPointRange = distance; + + // Unsorted data is not supported by the line tooltip, as well as + // data grouping and navigation in Stock charts (#725) and width + // calculation of columns (#1900) + } else if (distance < 0 && throwOnUnsorted) { + H.error(15); + throwOnUnsorted = false; // Only once + } + } + + // Record the properties + series.cropped = cropped; // undefined or true + series.cropStart = cropStart; + series.processedXData = processedXData; + series.processedYData = processedYData; + + series.closestPointRange = closestPointRange; + + }, + + /** + * Iterate over xData and crop values between min and max. Returns object + * containing crop start/end cropped xData with corresponding part of yData, + * dataMin and dataMax within the cropped range. + * + * @private + */ + cropData: function (xData, yData, min, max) { + var dataLength = xData.length, + cropStart = 0, + cropEnd = dataLength, + // line-type series need one point outside + cropShoulder = pick(this.cropShoulder, 1), + i, + j; + + // iterate up to find slice start + for (i = 0; i < dataLength; i++) { + if (xData[i] >= min) { + cropStart = Math.max(0, i - cropShoulder); + break; + } + } + + // proceed to find slice end + for (j = i; j < dataLength; j++) { + if (xData[j] > max) { + cropEnd = j + cropShoulder; + break; + } + } + + return { + xData: xData.slice(cropStart, cropEnd), + yData: yData.slice(cropStart, cropEnd), + start: cropStart, + end: cropEnd + }; + }, + + + /** + * Generate the data point after the data has been processed by cropping + * away unused points and optionally grouped in Highcharts Stock. + * + * @private + */ + generatePoints: function () { + var series = this, + options = series.options, + dataOptions = options.data, + data = series.data, + dataLength, + processedXData = series.processedXData, + processedYData = series.processedYData, + PointClass = series.pointClass, + processedDataLength = processedXData.length, + cropStart = series.cropStart || 0, + cursor, + hasGroupedData = series.hasGroupedData, + keys = options.keys, + point, + points = [], + i; + + if (!data && !hasGroupedData) { + var arr = []; + arr.length = dataOptions.length; + data = series.data = arr; + } + + if (keys && hasGroupedData) { + // grouped data has already applied keys (#6590) + series.options.keys = false; + } + + for (i = 0; i < processedDataLength; i++) { + cursor = cropStart + i; + if (!hasGroupedData) { + point = data[cursor]; + if (!point && dataOptions[cursor] !== undefined) { // #970 + data[cursor] = point = (new PointClass()).init( + series, + dataOptions[cursor], + processedXData[i] + ); + } + } else { + // splat the y data in case of ohlc data array + point = (new PointClass()).init( + series, + [processedXData[i]].concat(splat(processedYData[i])) + ); + + /** + * Highstock only. If a point object is created by data + * grouping, it doesn't reflect actual points in the raw data. + * In this case, the `dataGroup` property holds information + * that points back to the raw data. + * + * - `dataGroup.start` is the index of the first raw data point + * in the group. + * - `dataGroup.length` is the amount of points in the group. + * + * @name dataGroup + * @memberOf Point + * @type {Object} + * + */ + point.dataGroup = series.groupMap[i]; + } + if (point) { // #6279 + point.index = cursor; // For faster access in Point.update + points[i] = point; + } + } + + // restore keys options (#6590) + series.options.keys = keys; + + // Hide cropped-away points - this only runs when the number of points + // is above cropThreshold, or when swithching view from non-grouped + // data to grouped data (#637) + if ( + data && + ( + processedDataLength !== (dataLength = data.length) || + hasGroupedData + ) + ) { + for (i = 0; i < dataLength; i++) { + // when has grouped data, clear all points + if (i === cropStart && !hasGroupedData) { + i += processedDataLength; + } + if (data[i]) { + data[i].destroyElements(); + data[i].plotX = undefined; // #1003 + } + } + } + + /** + * Read only. An array containing those values converted to points, but + * in case the series data length exceeds the `cropThreshold`, or if the + * data is grouped, `series.data` doesn't contain all the points. It + * only contains the points that have been created on demand. To + * modify the data, use {@link Highcharts.Series#setData} or {@link + * Highcharts.Point#update}. + * + * @name data + * @memberOf Highcharts.Series + * @see Series.points + * @type {Array.} + */ + series.data = data; + + /** + * An array containing all currently visible point objects. In case of + * cropping, the cropped-away points are not part of this array. The + * `series.points` array starts at `series.cropStart` compared to + * `series.data` and `series.options.data`. If however the series data + * is grouped, these can't be correlated one to one. To + * modify the data, use {@link Highcharts.Series#setData} or {@link + * Highcharts.Point#update}. + * @name points + * @memberof Series + * @type {Array.} + */ + series.points = points; + }, + + /** + * Calculate Y extremes for the visible data. The result is set as + * `dataMin` and `dataMax` on the Series item. + * + * @param {Array.} [yData] + * The data to inspect. Defaults to the current data within the + * visible range. + * + */ + getExtremes: function (yData) { + var xAxis = this.xAxis, + yAxis = this.yAxis, + xData = this.processedXData, + yDataLength, + activeYData = [], + activeCounter = 0, + // #2117, need to compensate for log X axis + xExtremes = xAxis.getExtremes(), + xMin = xExtremes.min, + xMax = xExtremes.max, + validValue, + withinRange, + x, + y, + i, + j; + + yData = yData || this.stackedYData || this.processedYData || []; + yDataLength = yData.length; + + for (i = 0; i < yDataLength; i++) { + + x = xData[i]; + y = yData[i]; + + // For points within the visible range, including the first point + // outside the visible range (#7061), consider y extremes. + validValue = + (isNumber(y, true) || isArray(y)) && + (!yAxis.positiveValuesOnly || (y.length || y > 0)); + withinRange = + this.getExtremesFromAll || + this.options.getExtremesFromAll || + this.cropped || + ((xData[i + 1] || x) >= xMin && (xData[i - 1] || x) <= xMax); + + if (validValue && withinRange) { + + j = y.length; + if (j) { // array, like ohlc or range data + while (j--) { + if (typeof y[j] === 'number') { // #7380 + activeYData[activeCounter++] = y[j]; + } + } + } else { + activeYData[activeCounter++] = y; + } + } + } + + this.dataMin = arrayMin(activeYData); + this.dataMax = arrayMax(activeYData); + }, + + /** + * Translate data points from raw data values to chart specific positioning + * data needed later in the `drawPoints` and `drawGraph` functions. This + * function can be overridden in plugins and custom series type + * implementations. + */ + translate: function () { + if (!this.processedXData) { // hidden series + this.processData(); + } + this.generatePoints(); + var series = this, + options = series.options, + stacking = options.stacking, + xAxis = series.xAxis, + categories = xAxis.categories, + yAxis = series.yAxis, + points = series.points, + dataLength = points.length, + hasModifyValue = !!series.modifyValue, + i, + pointPlacement = options.pointPlacement, + dynamicallyPlaced = + pointPlacement === 'between' || + isNumber(pointPlacement), + threshold = options.threshold, + stackThreshold = options.startFromThreshold ? threshold : 0, + plotX, + plotY, + lastPlotX, + stackIndicator, + closestPointRangePx = Number.MAX_VALUE; + + /* + * Plotted coordinates need to be within a limited range. Drawing too + * far outside the viewport causes various rendering issues (#3201, + * #3923, #7555). + */ + function limitedRange(val) { + return Math.min(Math.max(-1e5, val), 1e5); + } + + // Point placement is relative to each series pointRange (#5889) + if (pointPlacement === 'between') { + pointPlacement = 0.5; + } + if (isNumber(pointPlacement)) { + pointPlacement *= pick(options.pointRange || xAxis.pointRange); + } + + // Translate each point + for (i = 0; i < dataLength; i++) { + var point = points[i], + xValue = point.x, + yValue = point.y, + yBottom = point.low, + stack = stacking && yAxis.stacks[( + series.negStacks && + yValue < (stackThreshold ? 0 : threshold) ? '-' : '' + ) + series.stackKey], + pointStack, + stackValues; + + // Discard disallowed y values for log axes (#3434) + if (yAxis.positiveValuesOnly && yValue !== null && yValue <= 0) { + point.isNull = true; + } + + // Get the plotX translation + point.plotX = plotX = correctFloat( // #5236 + limitedRange(xAxis.translate( // #3923 + xValue, + 0, + 0, + 0, + 1, + pointPlacement, + this.type === 'flags' + )) // #3923 + ); + + // Calculate the bottom y value for stacked series + if ( + stacking && + series.visible && + !point.isNull && + stack && + stack[xValue] + ) { + stackIndicator = series.getStackIndicator( + stackIndicator, + xValue, + series.index + ); + pointStack = stack[xValue]; + stackValues = pointStack.points[stackIndicator.key]; + yBottom = stackValues[0]; + yValue = stackValues[1]; + + if ( + yBottom === stackThreshold && + stackIndicator.key === stack[xValue].base + ) { + yBottom = pick(threshold, yAxis.min); + } + if (yAxis.positiveValuesOnly && yBottom <= 0) { // #1200, #1232 + yBottom = null; + } + + point.total = point.stackTotal = pointStack.total; + point.percentage = + pointStack.total && + (point.y / pointStack.total * 100); + point.stackY = yValue; + + // Place the stack label + pointStack.setOffset( + series.pointXOffset || 0, + series.barW || 0 + ); + + } + + // Set translated yBottom or remove it + point.yBottom = defined(yBottom) ? + limitedRange(yAxis.translate(yBottom, 0, 1, 0, 1)) : + null; + + // general hook, used for Highstock compare mode + if (hasModifyValue) { + yValue = series.modifyValue(yValue, point); + } + + // Set the the plotY value, reset it for redraws + point.plotY = plotY = + (typeof yValue === 'number' && yValue !== Infinity) ? + limitedRange(yAxis.translate(yValue, 0, 1, 0, 1)) : // #3201 + undefined; + + point.isInside = + plotY !== undefined && + plotY >= 0 && + plotY <= yAxis.len && // #3519 + plotX >= 0 && + plotX <= xAxis.len; + + + // Set client related positions for mouse tracking + point.clientX = dynamicallyPlaced ? + correctFloat( + xAxis.translate(xValue, 0, 0, 0, 1, pointPlacement) + ) : + plotX; // #1514, #5383, #5518 + + point.negative = point.y < (threshold || 0); + + // some API data + point.category = categories && categories[point.x] !== undefined ? + categories[point.x] : point.x; + + // Determine auto enabling of markers (#3635, #5099) + if (!point.isNull) { + if (lastPlotX !== undefined) { + closestPointRangePx = Math.min( + closestPointRangePx, + Math.abs(plotX - lastPlotX) + ); + } + lastPlotX = plotX; + } + + // Find point zone + point.zone = this.zones.length && point.getZone(); + } + series.closestPointRangePx = closestPointRangePx; + }, + + /** + * Return the series points with null points filtered out. + * + * @param {Array.} [points] + * The points to inspect, defaults to {@link Series.points}. + * @param {Boolean} [insideOnly=false] + * Whether to inspect only the points that are inside the visible + * view. + * + * @return {Array.} + * The valid points. + */ + getValidPoints: function (points, insideOnly) { + var chart = this.chart; + // #3916, #5029, #5085 + return grep(points || this.points || [], function isValidPoint(point) { + if (insideOnly && !chart.isInsidePlot( + point.plotX, + point.plotY, + chart.inverted + )) { + return false; + } + return !point.isNull; + }); + }, + + /** + * Set the clipping for the series. For animated series it is called twice, + * first to initiate animating the clip then the second time without the + * animation to set the final clip. + * + * @private + */ + setClip: function (animation) { + var chart = this.chart, + options = this.options, + renderer = chart.renderer, + inverted = chart.inverted, + seriesClipBox = this.clipBox, + clipBox = seriesClipBox || chart.clipBox, + sharedClipKey = + this.sharedClipKey || + [ + '_sharedClip', + animation && animation.duration, + animation && animation.easing, + clipBox.height, + options.xAxis, + options.yAxis + ].join(','), // #4526 + clipRect = chart[sharedClipKey], + markerClipRect = chart[sharedClipKey + 'm']; + + // If a clipping rectangle with the same properties is currently present + // in the chart, use that. + if (!clipRect) { + + // When animation is set, prepare the initial positions + if (animation) { + clipBox.width = 0; + if (inverted) { + clipBox.x = chart.plotSizeX; + } + + chart[sharedClipKey + 'm'] = markerClipRect = renderer.clipRect( + inverted ? chart.plotSizeX + 99 : -99, // include the width of the first marker + inverted ? -chart.plotLeft : -chart.plotTop, + 99, + inverted ? chart.chartWidth : chart.chartHeight + ); + } + chart[sharedClipKey] = clipRect = renderer.clipRect(clipBox); + // Create hashmap for series indexes + clipRect.count = { length: 0 }; + + } + if (animation) { + if (!clipRect.count[this.index]) { + clipRect.count[this.index] = true; + clipRect.count.length += 1; + } + } + + if (options.clip !== false) { + this.group.clip(animation || seriesClipBox ? clipRect : chart.clipRect); + this.markerGroup.clip(markerClipRect); + this.sharedClipKey = sharedClipKey; + } + + // Remove the shared clipping rectangle when all series are shown + if (!animation) { + if (clipRect.count[this.index]) { + delete clipRect.count[this.index]; + clipRect.count.length -= 1; + } + + if (clipRect.count.length === 0 && sharedClipKey && chart[sharedClipKey]) { + if (!seriesClipBox) { + chart[sharedClipKey] = chart[sharedClipKey].destroy(); + } + if (chart[sharedClipKey + 'm']) { + chart[sharedClipKey + 'm'] = chart[sharedClipKey + 'm'].destroy(); + } + } + } + }, + + /** + * Animate in the series. Called internally twice. First with the `init` + * parameter set to true, which sets up the initial state of the animation. + * Then when ready, it is called with the `init` parameter undefined, in + * order to perform the actual animation. After the second run, the function + * is removed. + * + * @param {Boolean} init + * Initialize the animation. + */ + animate: function (init) { + var series = this, + chart = series.chart, + clipRect, + animation = animObject(series.options.animation), + sharedClipKey; + + // Initialize the animation. Set up the clipping rectangle. + if (init) { + + series.setClip(animation); + + // Run the animation + } else { + sharedClipKey = this.sharedClipKey; + clipRect = chart[sharedClipKey]; + if (clipRect) { + clipRect.animate({ + width: chart.plotSizeX, + x: 0 + }, animation); + } + if (chart[sharedClipKey + 'm']) { + chart[sharedClipKey + 'm'].animate({ + width: chart.plotSizeX + 99, + x: 0 + }, animation); + } + + // Delete this function to allow it only once + series.animate = null; + + } + }, + + /** + * This runs after animation to land on the final plot clipping. + * + * @private + */ + afterAnimate: function () { + this.setClip(); + fireEvent(this, 'afterAnimate'); + this.finishedAnimating = true; + }, + + /** + * Draw the markers for line-like series types, and columns or other + * graphical representation for {@link Point} objects for other series + * types. The resulting element is typically stored as {@link + * Point.graphic}, and is created on the first call and updated and moved on + * subsequent calls. + */ + drawPoints: function () { + var series = this, + points = series.points, + chart = series.chart, + i, + point, + symbol, + graphic, + options = series.options, + seriesMarkerOptions = options.marker, + pointMarkerOptions, + hasPointMarker, + enabled, + isInside, + markerGroup = series[series.specialGroup] || series.markerGroup, + xAxis = series.xAxis, + markerAttribs, + globallyEnabled = pick( + seriesMarkerOptions.enabled, + xAxis.isRadial ? true : null, + // Use larger or equal as radius is null in bubbles (#6321) + series.closestPointRangePx >= 2 * seriesMarkerOptions.radius + ); + + if (seriesMarkerOptions.enabled !== false || series._hasPointMarkers) { + + for (i = 0; i < points.length; i++) { + point = points[i]; + graphic = point.graphic; + pointMarkerOptions = point.marker || {}; + hasPointMarker = !!point.marker; + enabled = (globallyEnabled && pointMarkerOptions.enabled === undefined) || pointMarkerOptions.enabled; + isInside = point.isInside; + + // only draw the point if y is defined + if (enabled && !point.isNull) { + + // Shortcuts + symbol = pick(pointMarkerOptions.symbol, series.symbol); + point.hasImage = symbol.indexOf('url') === 0; + + markerAttribs = series.markerAttribs( + point, + point.selected && 'select' + ); + + if (graphic) { // update + graphic[isInside ? 'show' : 'hide'](true) // Since the marker group isn't clipped, each individual marker must be toggled + .animate(markerAttribs); + } else if (isInside && (markerAttribs.width > 0 || point.hasImage)) { + + /** + * The graphic representation of the point. Typically + * this is a simple shape, like a `rect` for column + * charts or `path` for line markers, but for some + * complex series types like boxplot or 3D charts, the + * graphic may be a `g` element containing other shapes. + * The graphic is generated the first time {@link + * Series#drawPoints} runs, and updated and moved on + * subsequent runs. + * + * @memberof Point + * @name graphic + * @type {SVGElement} + */ + point.graphic = graphic = chart.renderer.symbol( + symbol, + markerAttribs.x, + markerAttribs.y, + markerAttribs.width, + markerAttribs.height, + hasPointMarker ? pointMarkerOptions : seriesMarkerOptions + ) + .add(markerGroup); + } + + + // Presentational attributes + if (graphic) { + graphic.attr(series.pointAttribs(point, point.selected && 'select')); + } + + + if (graphic) { + graphic.addClass(point.getClassName(), true); + } + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + } + } + + }, + + /** + * Get non-presentational attributes for a point. Used internally for both + * styled mode and classic. Can be overridden for different series types. + * + * @see Series#pointAttribs + * + * @param {Point} point + * The Point to inspect. + * @param {String} [state] + * The state, can be either `hover`, `select` or undefined. + * + * @return {SVGAttributes} + * A hash containing those attributes that are not settable from + * CSS. + */ + markerAttribs: function (point, state) { + var seriesMarkerOptions = this.options.marker, + seriesStateOptions, + pointMarkerOptions = point.marker || {}, + pointStateOptions, + radius = pick( + pointMarkerOptions.radius, + seriesMarkerOptions.radius + ), + attribs; + + // Handle hover and select states + if (state) { + seriesStateOptions = seriesMarkerOptions.states[state]; + pointStateOptions = pointMarkerOptions.states && + pointMarkerOptions.states[state]; + + radius = pick( + pointStateOptions && pointStateOptions.radius, + seriesStateOptions && seriesStateOptions.radius, + radius + (seriesStateOptions && seriesStateOptions.radiusPlus || 0) + ); + } + + if (point.hasImage) { + radius = 0; // and subsequently width and height is not set + } + + attribs = { + x: Math.floor(point.plotX) - radius, // Math.floor for #1843 + y: point.plotY - radius + }; + + if (radius) { + attribs.width = attribs.height = 2 * radius; + } + + return attribs; + + }, + + + /** + * Internal function to get presentational attributes for each point. Unlike + * {@link Series#markerAttribs}, this function should return those + * attributes that can also be set in CSS. In styled mode, `pointAttribs` + * won't be called. + * + * @param {Point} point + * The point instance to inspect. + * @param {String} [state] + * The point state, can be either `hover`, `select` or undefined for + * normal state. + * + * @return {SVGAttributes} + * The presentational attributes to be set on the point. + */ + pointAttribs: function (point, state) { + var seriesMarkerOptions = this.options.marker, + seriesStateOptions, + pointOptions = point && point.options, + pointMarkerOptions = (pointOptions && pointOptions.marker) || {}, + pointStateOptions, + color = this.color, + pointColorOption = pointOptions && pointOptions.color, + pointColor = point && point.color, + strokeWidth = pick( + pointMarkerOptions.lineWidth, + seriesMarkerOptions.lineWidth + ), + zoneColor = point && point.zone && point.zone.color, + fill, + stroke; + + color = pointColorOption || zoneColor || pointColor || color; + fill = pointMarkerOptions.fillColor || seriesMarkerOptions.fillColor || color; + stroke = pointMarkerOptions.lineColor || seriesMarkerOptions.lineColor || color; + + // Handle hover and select states + if (state) { + seriesStateOptions = seriesMarkerOptions.states[state]; + pointStateOptions = (pointMarkerOptions.states && pointMarkerOptions.states[state]) || {}; + strokeWidth = pick( + pointStateOptions.lineWidth, + seriesStateOptions.lineWidth, + strokeWidth + pick( + pointStateOptions.lineWidthPlus, + seriesStateOptions.lineWidthPlus, + 0 + ) + ); + fill = pointStateOptions.fillColor || seriesStateOptions.fillColor || fill; + stroke = pointStateOptions.lineColor || seriesStateOptions.lineColor || stroke; + } + + return { + 'stroke': stroke, + 'stroke-width': strokeWidth, + 'fill': fill + }; + }, + + /** + * Clear DOM objects and free up memory. + * + * @private + */ + destroy: function () { + var series = this, + chart = series.chart, + issue134 = /AppleWebKit\/533/.test(win.navigator.userAgent), + destroy, + i, + data = series.data || [], + point, + axis; + + // add event hook + fireEvent(series, 'destroy'); + + // remove all events + removeEvent(series); + + // erase from axes + each(series.axisTypes || [], function (AXIS) { + axis = series[AXIS]; + if (axis && axis.series) { + erase(axis.series, series); + axis.isDirty = axis.forceRedraw = true; + } + }); + + // remove legend items + if (series.legendItem) { + series.chart.legend.destroyItem(series); + } + + // destroy all points with their elements + i = data.length; + while (i--) { + point = data[i]; + if (point && point.destroy) { + point.destroy(); + } + } + series.points = null; + + // Clear the animation timeout if we are destroying the series during initial animation + clearTimeout(series.animationTimeout); + + // Destroy all SVGElements associated to the series + objectEach(series, function (val, prop) { + if (val instanceof SVGElement && !val.survive) { // Survive provides a hook for not destroying + + // issue 134 workaround + destroy = issue134 && prop === 'group' ? + 'hide' : + 'destroy'; + + val[destroy](); + } + }); + + // remove from hoverSeries + if (chart.hoverSeries === series) { + chart.hoverSeries = null; + } + erase(chart.series, series); + chart.orderSeries(); + + // clear all members + objectEach(series, function (val, prop) { + delete series[prop]; + }); + }, + + /** + * Get the graph path. + * + * @private + */ + getGraphPath: function (points, nullsAsZeroes, connectCliffs) { + var series = this, + options = series.options, + step = options.step, + reversed, + graphPath = [], + xMap = [], + gap; + + points = points || series.points; + + // Bottom of a stack is reversed + reversed = points.reversed; + if (reversed) { + points.reverse(); + } + // Reverse the steps (#5004) + step = { right: 1, center: 2 }[step] || (step && 3); + if (step && reversed) { + step = 4 - step; + } + + // Remove invalid points, especially in spline (#5015) + if (options.connectNulls && !nullsAsZeroes && !connectCliffs) { + points = this.getValidPoints(points); + } + + // Build the line + each(points, function (point, i) { + + var plotX = point.plotX, + plotY = point.plotY, + lastPoint = points[i - 1], + pathToPoint; // the path to this point from the previous + + if ((point.leftCliff || (lastPoint && lastPoint.rightCliff)) && !connectCliffs) { + gap = true; // ... and continue + } + + // Line series, nullsAsZeroes is not handled + if (point.isNull && !defined(nullsAsZeroes) && i > 0) { + gap = !options.connectNulls; + + // Area series, nullsAsZeroes is set + } else if (point.isNull && !nullsAsZeroes) { + gap = true; + + } else { + + if (i === 0 || gap) { + pathToPoint = ['M', point.plotX, point.plotY]; + + } else if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object + + pathToPoint = series.getPointSpline(points, point, i); + + } else if (step) { + + if (step === 1) { // right + pathToPoint = [ + 'L', + lastPoint.plotX, + plotY + ]; + + } else if (step === 2) { // center + pathToPoint = [ + 'L', + (lastPoint.plotX + plotX) / 2, + lastPoint.plotY, + 'L', + (lastPoint.plotX + plotX) / 2, + plotY + ]; + + } else { + pathToPoint = [ + 'L', + plotX, + lastPoint.plotY + ]; + } + pathToPoint.push('L', plotX, plotY); + + } else { + // normal line to next point + pathToPoint = [ + 'L', + plotX, + plotY + ]; + } + + // Prepare for animation. When step is enabled, there are two path nodes for each x value. + xMap.push(point.x); + if (step) { + xMap.push(point.x); + } + + graphPath.push.apply(graphPath, pathToPoint); + gap = false; + } + }); + + graphPath.xMap = xMap; + series.graphPath = graphPath; + + return graphPath; + + }, + + /** + * Draw the graph. Called internally when rendering line-like series types. + * The first time it generates the `series.graph` item and optionally other + * series-wide items like `series.area` for area charts. On subsequent calls + * these items are updated with new positions and attributes. + */ + drawGraph: function () { + var series = this, + options = this.options, + graphPath = (this.gappedPath || this.getGraphPath).call(this), + props = [[ + 'graph', + 'highcharts-graph', + + options.lineColor || this.color, + options.dashStyle + + ]]; + + // Add the zone properties if any + each(this.zones, function (zone, i) { + props.push([ + 'zone-graph-' + i, + 'highcharts-graph highcharts-zone-graph-' + i + ' ' + (zone.className || ''), + + zone.color || series.color, + zone.dashStyle || options.dashStyle + + ]); + }); + + // Draw the graph + each(props, function (prop, i) { + var graphKey = prop[0], + graph = series[graphKey], + attribs; + + if (graph) { + graph.endX = series.preventGraphAnimation ? + null : + graphPath.xMap; + graph.animate({ d: graphPath }); + + } else if (graphPath.length) { // #1487 + + series[graphKey] = series.chart.renderer.path(graphPath) + .addClass(prop[1]) + .attr({ zIndex: 1 }) // #1069 + .add(series.group); + + + attribs = { + 'stroke': prop[2], + 'stroke-width': options.lineWidth, + 'fill': (series.fillGraph && series.color) || 'none' // Polygon series use filled graph + }; + + if (prop[3]) { + attribs.dashstyle = prop[3]; + } else if (options.linecap !== 'square') { + attribs['stroke-linecap'] = attribs['stroke-linejoin'] = 'round'; + } + + graph = series[graphKey] + .attr(attribs) + .shadow((i < 2) && options.shadow); // add shadow to normal series (0) or to first zone (1) #3932 + + } + + // Helpers for animation + if (graph) { + graph.startX = graphPath.xMap; + graph.isArea = graphPath.isArea; // For arearange animation + } + }); + }, + + /** + * Clip the graphs into zones for colors and styling. + * + * @private + */ + applyZones: function () { + var series = this, + chart = this.chart, + renderer = chart.renderer, + zones = this.zones, + translatedFrom, + translatedTo, + clips = this.clips || [], + clipAttr, + graph = this.graph, + area = this.area, + chartSizeMax = Math.max(chart.chartWidth, chart.chartHeight), + axis = this[(this.zoneAxis || 'y') + 'Axis'], + extremes, + reversed, + inverted = chart.inverted, + horiz, + pxRange, + pxPosMin, + pxPosMax, + ignoreZones = false; + + if (zones.length && (graph || area) && axis && axis.min !== undefined) { + reversed = axis.reversed; + horiz = axis.horiz; + // The use of the Color Threshold assumes there are no gaps + // so it is safe to hide the original graph and area + if (graph) { + graph.hide(); + } + if (area) { + area.hide(); + } + + // Create the clips + extremes = axis.getExtremes(); + each(zones, function (threshold, i) { + + translatedFrom = reversed ? + (horiz ? chart.plotWidth : 0) : + (horiz ? 0 : axis.toPixels(extremes.min)); + translatedFrom = Math.min(Math.max(pick(translatedTo, translatedFrom), 0), chartSizeMax); + translatedTo = Math.min(Math.max(Math.round(axis.toPixels(pick(threshold.value, extremes.max), true)), 0), chartSizeMax); + + if (ignoreZones) { + translatedFrom = translatedTo = axis.toPixels(extremes.max); + } + + pxRange = Math.abs(translatedFrom - translatedTo); + pxPosMin = Math.min(translatedFrom, translatedTo); + pxPosMax = Math.max(translatedFrom, translatedTo); + if (axis.isXAxis) { + clipAttr = { + x: inverted ? pxPosMax : pxPosMin, + y: 0, + width: pxRange, + height: chartSizeMax + }; + if (!horiz) { + clipAttr.x = chart.plotHeight - clipAttr.x; + } + } else { + clipAttr = { + x: 0, + y: inverted ? pxPosMax : pxPosMin, + width: chartSizeMax, + height: pxRange + }; + if (horiz) { + clipAttr.y = chart.plotWidth - clipAttr.y; + } + } + + + // VML SUPPPORT + if (inverted && renderer.isVML) { + if (axis.isXAxis) { + clipAttr = { + x: 0, + y: reversed ? pxPosMin : pxPosMax, + height: clipAttr.width, + width: chart.chartWidth + }; + } else { + clipAttr = { + x: clipAttr.y - chart.plotLeft - chart.spacingBox.x, + y: 0, + width: clipAttr.height, + height: chart.chartHeight + }; + } + } + // END OF VML SUPPORT + + + if (clips[i]) { + clips[i].animate(clipAttr); + } else { + clips[i] = renderer.clipRect(clipAttr); + + if (graph) { + series['zone-graph-' + i].clip(clips[i]); + } + + if (area) { + series['zone-area-' + i].clip(clips[i]); + } + } + // if this zone extends out of the axis, ignore the others + ignoreZones = threshold.value > extremes.max; + }); + this.clips = clips; + } + }, + + /** + * Initialize and perform group inversion on series.group and + * series.markerGroup. + * + * @private + */ + invertGroups: function (inverted) { + var series = this, + chart = series.chart, + remover; + + function setInvert() { + each(['group', 'markerGroup'], function (groupName) { + if (series[groupName]) { + + // VML/HTML needs explicit attributes for flipping + if (chart.renderer.isVML) { + series[groupName].attr({ + width: series.yAxis.len, + height: series.xAxis.len + }); + } + + series[groupName].width = series.yAxis.len; + series[groupName].height = series.xAxis.len; + series[groupName].invert(inverted); + } + }); + } + + // Pie, go away (#1736) + if (!series.xAxis) { + return; + } + + // A fixed size is needed for inversion to work + remover = addEvent(chart, 'resize', setInvert); + addEvent(series, 'destroy', remover); + + // Do it now + setInvert(inverted); // do it now + + // On subsequent render and redraw, just do setInvert without setting up events again + series.invertGroups = setInvert; + }, + + /** + * General abstraction for creating plot groups like series.group, + * series.dataLabelsGroup and series.markerGroup. On subsequent calls, the + * group will only be adjusted to the updated plot size. + * + * @private + */ + plotGroup: function (prop, name, visibility, zIndex, parent) { + var group = this[prop], + isNew = !group; + + // Generate it on first call + if (isNew) { + this[prop] = group = this.chart.renderer.g() + .attr({ + zIndex: zIndex || 0.1 // IE8 and pointer logic use this + }) + .add(parent); + + } + + // Add the class names, and replace existing ones as response to + // Series.update (#6660) + group.addClass( + ( + 'highcharts-' + name + + ' highcharts-series-' + this.index + + ' highcharts-' + this.type + '-series ' + + ( + defined(this.colorIndex) ? + 'highcharts-color-' + this.colorIndex + ' ' : + '' + ) + + (this.options.className || '') + + (group.hasClass('highcharts-tracker') ? ' highcharts-tracker' : '') + ), + true + ); + + // Place it on first and subsequent (redraw) calls + group.attr({ visibility: visibility })[isNew ? 'attr' : 'animate']( + this.getPlotBox() + ); + return group; + }, + + /** + * Get the translation and scale for the plot area of this series. + */ + getPlotBox: function () { + var chart = this.chart, + xAxis = this.xAxis, + yAxis = this.yAxis; + + // Swap axes for inverted (#2339) + if (chart.inverted) { + xAxis = yAxis; + yAxis = this.xAxis; + } + return { + translateX: xAxis ? xAxis.left : chart.plotLeft, + translateY: yAxis ? yAxis.top : chart.plotTop, + scaleX: 1, // #1623 + scaleY: 1 + }; + }, + + /** + * Render the graph and markers. Called internally when first rendering and + * later when redrawing the chart. This function can be extended in plugins, + * but normally shouldn't be called directly. + */ + render: function () { + var series = this, + chart = series.chart, + group, + options = series.options, + // Animation doesn't work in IE8 quirks when the group div is + // hidden, and looks bad in other oldIE + animDuration = ( + !!series.animate && + chart.renderer.isSVG && + animObject(options.animation).duration + ), + visibility = series.visible ? 'inherit' : 'hidden', // #2597 + zIndex = options.zIndex, + hasRendered = series.hasRendered, + chartSeriesGroup = chart.seriesGroup, + inverted = chart.inverted; + + // the group + group = series.plotGroup( + 'group', + 'series', + visibility, + zIndex, + chartSeriesGroup + ); + + series.markerGroup = series.plotGroup( + 'markerGroup', + 'markers', + visibility, + zIndex, + chartSeriesGroup + ); + + // initiate the animation + if (animDuration) { + series.animate(true); + } + + // SVGRenderer needs to know this before drawing elements (#1089, #1795) + group.inverted = series.isCartesian ? inverted : false; + + // draw the graph if any + if (series.drawGraph) { + series.drawGraph(); + series.applyZones(); + } + +/* each(series.points, function (point) { + if (point.redraw) { + point.redraw(); + } + });*/ + + // draw the data labels (inn pies they go before the points) + if (series.drawDataLabels) { + series.drawDataLabels(); + } + + // draw the points + if (series.visible) { + series.drawPoints(); + } + + + // draw the mouse tracking area + if ( + series.drawTracker && + series.options.enableMouseTracking !== false + ) { + series.drawTracker(); + } + + // Handle inverted series and tracker groups + series.invertGroups(inverted); + + // Initial clipping, must be defined after inverting groups for VML. + // Applies to columns etc. (#3839). + if (options.clip !== false && !series.sharedClipKey && !hasRendered) { + group.clip(chart.clipRect); + } + + // Run the animation + if (animDuration) { + series.animate(); + } + + // Call the afterAnimate function on animation complete (but don't + // overwrite the animation.complete option which should be available to + // the user). + if (!hasRendered) { + series.animationTimeout = syncTimeout(function () { + series.afterAnimate(); + }, animDuration); + } + + series.isDirty = false; // means data is in accordance with what you see + // (See #322) series.isDirty = series.isDirtyData = false; // means + // data is in accordance with what you see + series.hasRendered = true; + }, + + /** + * Redraw the series. This function is called internally from `chart.redraw` + * and normally shouldn't be called directly. + * + * @private + */ + redraw: function () { + var series = this, + chart = series.chart, + // cache it here as it is set to false in render, but used after + wasDirty = series.isDirty || series.isDirtyData, + group = series.group, + xAxis = series.xAxis, + yAxis = series.yAxis; + + // reposition on resize + if (group) { + if (chart.inverted) { + group.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }); + } + + group.animate({ + translateX: pick(xAxis && xAxis.left, chart.plotLeft), + translateY: pick(yAxis && yAxis.top, chart.plotTop) + }); + } + + series.translate(); + series.render(); + if (wasDirty) { // #3868, #3945 + delete this.kdTree; + } + }, + + kdAxisArray: ['clientX', 'plotY'], + + searchPoint: function (e, compareX) { + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis, + inverted = series.chart.inverted; + + return this.searchKDTree({ + clientX: inverted ? + xAxis.len - e.chartY + xAxis.pos : + e.chartX - xAxis.pos, + plotY: inverted ? + yAxis.len - e.chartX + yAxis.pos : + e.chartY - yAxis.pos + }, compareX); + }, + + /** + * Build the k-d-tree that is used by mouse and touch interaction to get the + * closest point. Line-like series typically have a one-dimensional tree + * where points are searched along the X axis, while scatter-like series + * typically search in two dimensions, X and Y. + * + * @private + */ + buildKDTree: function () { + + // Prevent multiple k-d-trees from being built simultaneously (#6235) + this.buildingKdTree = true; + + var series = this, + dimensions = series.options.findNearestPointBy.indexOf('y') > -1 ? + 2 : 1; + + // Internal function + function _kdtree(points, depth, dimensions) { + var axis, + median, + length = points && points.length; + + if (length) { + + // alternate between the axis + axis = series.kdAxisArray[depth % dimensions]; + + // sort point array + points.sort(function (a, b) { + return a[axis] - b[axis]; + }); + + median = Math.floor(length / 2); + + // build and return nod + return { + point: points[median], + left: _kdtree( + points.slice(0, median), depth + 1, dimensions + ), + right: _kdtree( + points.slice(median + 1), depth + 1, dimensions + ) + }; + + } + } + + // Start the recursive build process with a clone of the points array + // and null points filtered out (#3873) + function startRecursive() { + series.kdTree = _kdtree( + series.getValidPoints( + null, + // For line-type series restrict to plot area, but + // column-type series not (#3916, #4511) + !series.directTouch + ), + dimensions, + dimensions + ); + series.buildingKdTree = false; + } + delete series.kdTree; + + // For testing tooltips, don't build async + syncTimeout(startRecursive, series.options.kdNow ? 0 : 1); + }, + + searchKDTree: function (point, compareX) { + var series = this, + kdX = this.kdAxisArray[0], + kdY = this.kdAxisArray[1], + kdComparer = compareX ? 'distX' : 'dist', + kdDimensions = series.options.findNearestPointBy.indexOf('y') > -1 ? + 2 : 1; + + // Set the one and two dimensional distance on the point object + function setDistance(p1, p2) { + var x = (defined(p1[kdX]) && defined(p2[kdX])) ? + Math.pow(p1[kdX] - p2[kdX], 2) : + null, + y = (defined(p1[kdY]) && defined(p2[kdY])) ? + Math.pow(p1[kdY] - p2[kdY], 2) : + null, + r = (x || 0) + (y || 0); + + p2.dist = defined(r) ? Math.sqrt(r) : Number.MAX_VALUE; + p2.distX = defined(x) ? Math.sqrt(x) : Number.MAX_VALUE; + } + function _search(search, tree, depth, dimensions) { + var point = tree.point, + axis = series.kdAxisArray[depth % dimensions], + tdist, + sideA, + sideB, + ret = point, + nPoint1, + nPoint2; + + setDistance(search, point); + + // Pick side based on distance to splitting point + tdist = search[axis] - point[axis]; + sideA = tdist < 0 ? 'left' : 'right'; + sideB = tdist < 0 ? 'right' : 'left'; + + // End of tree + if (tree[sideA]) { + nPoint1 = _search(search, tree[sideA], depth + 1, dimensions); + + ret = (nPoint1[kdComparer] < ret[kdComparer] ? nPoint1 : point); + } + if (tree[sideB]) { + // compare distance to current best to splitting point to decide + // wether to check side B or not + if (Math.sqrt(tdist * tdist) < ret[kdComparer]) { + nPoint2 = _search( + search, + tree[sideB], + depth + 1, + dimensions + ); + ret = nPoint2[kdComparer] < ret[kdComparer] ? + nPoint2 : + ret; + } + } + + return ret; + } + + if (!this.kdTree && !this.buildingKdTree) { + this.buildKDTree(); + } + + if (this.kdTree) { + return _search(point, this.kdTree, kdDimensions, kdDimensions); + } + } + +}); // end Series prototype + +/** + * A line series displays information as a series of data points connected by + * straight line segments. + * + * @sample {highcharts} highcharts/demo/line-basic/ Line chart + * @sample {highstock} stock/demo/basic-line/ Line chart + * + * @extends plotOptions.series + * @product highcharts highstock + * @apioption plotOptions.line + */ + +/** + * A `line` series. If the [type](#series.line.type) option is not + * specified, it is inherited from [chart.type](#chart.type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * line](#plotOptions.line). + * + * @type {Object} + * @extends series,plotOptions.line + * @excluding dataParser,dataURL + * @product highcharts highstock + * @apioption series.line + */ + +/** + * An array of data points for the series. For the `line` series type, + * points can be given in the following ways: + * + * 1. An array of numerical values. In this case, the numerical values + * will be interpreted as `y` options. The `x` values will be automatically + * calculated, either starting at 0 and incremented by 1, or from `pointStart` + * and `pointInterval` given in the series options. If the axis has + * categories, these will be used. Example: + * + * ```js + * data: [0, 5, 3, 5] + * ``` + * + * 2. An array of arrays with 2 values. In this case, the values correspond + * to `x,y`. If the first value is a string, it is applied as the name + * of the point, and the `x` value is inferred. + * + * ```js + * data: [ + * [0, 1], + * [1, 2], + * [2, 8] + * ] + * ``` + * + * 3. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.line.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * x: 1, + * y: 9, + * name: "Point2", + * color: "#00FF00" + * }, { + * x: 1, + * y: 6, + * name: "Point1", + * color: "#FF00FF" + * }] + * ``` + * + * @type {Array} + * @sample {highcharts} highcharts/chart/reflow-true/ Numerical values + * @sample {highcharts} highcharts/series/data-array-of-arrays/ Arrays of numeric x and y + * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/ Arrays of datetime x and y + * @sample {highcharts} highcharts/series/data-array-of-name-value/ Arrays of point.name and y + * @sample {highcharts} highcharts/series/data-array-of-objects/ Config objects + * @apioption series.line.data + */ + +/** + * An additional, individual class name for the data point's graphic + * representation. + * + * @type {String} + * @since 5.0.0 + * @product highcharts + * @apioption series.line.data.className + */ + +/** + * Individual color for the point. By default the color is pulled from + * the global `colors` array. + * + * In styled mode, the `color` option doesn't take effect. Instead, use + * `colorIndex`. + * + * @type {Color} + * @sample {highcharts} highcharts/point/color/ Mark the highest point + * @default undefined + * @product highcharts highstock + * @apioption series.line.data.color + */ + +/** + * Styled mode only. A specific color index to use for the point, so its + * graphic representations are given the class name + * `highcharts-color-{n}`. + * + * @type {Number} + * @since 5.0.0 + * @product highcharts + * @apioption series.line.data.colorIndex + */ + +/** + * Individual data label for each point. The options are the same as + * the ones for [plotOptions.series.dataLabels](#plotOptions.series. + * dataLabels) + * + * @type {Object} + * @sample {highcharts} highcharts/point/datalabels/ Show a label for the last value + * @sample {highstock} highcharts/point/datalabels/ Show a label for the last value + * @product highcharts highstock + * @apioption series.line.data.dataLabels + */ + +/** + * A description of the point to add to the screen reader information + * about the point. Requires the Accessibility module. + * + * @type {String} + * @default undefined + * @since 5.0.0 + * @apioption series.line.data.description + */ + +/** + * An id for the point. This can be used after render time to get a + * pointer to the point object through `chart.get()`. + * + * @type {String} + * @sample {highcharts} highcharts/point/id/ Remove an id'd point + * @default null + * @since 1.2.0 + * @product highcharts highstock + * @apioption series.line.data.id + */ + +/** + * The rank for this point's data label in case of collision. If two + * data labels are about to overlap, only the one with the highest `labelrank` + * will be drawn. + * + * @type {Number} + * @apioption series.line.data.labelrank + */ + +/** + * The name of the point as shown in the legend, tooltip, dataLabel + * etc. + * + * @type {String} + * @sample {highcharts} highcharts/series/data-array-of-objects/ Point names + * @see [xAxis.uniqueNames](#xAxis.uniqueNames) + * @apioption series.line.data.name + */ + +/** + * Whether the data point is selected initially. + * + * @type {Boolean} + * @default false + * @product highcharts highstock + * @apioption series.line.data.selected + */ + +/** + * The x value of the point. For datetime axes, the X value is the timestamp + * in milliseconds since 1970. + * + * @type {Number} + * @product highcharts highstock + * @apioption series.line.data.x + */ + +/** + * The y value of the point. + * + * @type {Number} + * @default null + * @product highcharts highstock + * @apioption series.line.data.y + */ + +/** + * Individual point events + * + * @extends plotOptions.series.point.events + * @product highcharts highstock + * @apioption series.line.data.events + */ + +/** + * @extends plotOptions.series.marker + * @product highcharts highstock + * @apioption series.line.data.marker + */ + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var Axis = H.Axis, + Chart = H.Chart, + correctFloat = H.correctFloat, + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + each = H.each, + format = H.format, + objectEach = H.objectEach, + pick = H.pick, + Series = H.Series; + +/** + * The class for stacks. Each stack, on a specific X value and either negative + * or positive, has its own stack item. + * + * @class + */ +H.StackItem = function (axis, options, isNegative, x, stackOption) { + + var inverted = axis.chart.inverted; + + this.axis = axis; + + // Tells if the stack is negative + this.isNegative = isNegative; + + // Save the options to be able to style the label + this.options = options; + + // Save the x value to be able to position the label later + this.x = x; + + // Initialize total value + this.total = null; + + // This will keep each points' extremes stored by series.index and point + // index + this.points = {}; + + // Save the stack option on the series configuration object, and whether to + // treat it as percent + this.stack = stackOption; + this.leftCliff = 0; + this.rightCliff = 0; + + // The align options and text align varies on whether the stack is negative + // and if the chart is inverted or not. + // First test the user supplied value, then use the dynamic. + this.alignOptions = { + align: options.align || + (inverted ? (isNegative ? 'left' : 'right') : 'center'), + verticalAlign: options.verticalAlign || + (inverted ? 'middle' : (isNegative ? 'bottom' : 'top')), + y: pick(options.y, inverted ? 4 : (isNegative ? 14 : -6)), + x: pick(options.x, inverted ? (isNegative ? -6 : 6) : 0) + }; + + this.textAlign = options.textAlign || + (inverted ? (isNegative ? 'right' : 'left') : 'center'); +}; + +H.StackItem.prototype = { + destroy: function () { + destroyObjectProperties(this, this.axis); + }, + + /** + * Renders the stack total label and adds it to the stack label group. + */ + render: function (group) { + var chart = this.axis.chart, + options = this.options, + formatOption = options.format, + str = formatOption ? + format(formatOption, this, chart.time) : + options.formatter.call(this); // format the text in the label + + // Change the text to reflect the new total and set visibility to hidden + // in case the serie is hidden + if (this.label) { + this.label.attr({ text: str, visibility: 'hidden' }); + // Create new label + } else { + this.label = + chart.renderer.text(str, null, null, options.useHTML) + .css(options.style) + .attr({ + align: this.textAlign, + rotation: options.rotation, + visibility: 'hidden' // hidden until setOffset is called + }) + .add(group); // add to the labels-group + } + }, + + /** + * Sets the offset that the stack has from the x value and repositions the + * label. + */ + setOffset: function (xOffset, xWidth) { + var stackItem = this, + axis = stackItem.axis, + chart = axis.chart, + // stack value translated mapped to chart coordinates + y = axis.translate( + axis.usePercentage ? 100 : stackItem.total, + 0, + 0, + 0, + 1 + ), + yZero = axis.translate(0), // stack origin + h = Math.abs(y - yZero), // stack height + x = chart.xAxis[0].translate(stackItem.x) + xOffset, // x position + stackBox = stackItem.getStackBox(chart, stackItem, x, y, xWidth, h), + label = stackItem.label, + alignAttr; + + if (label) { + // Align the label to the box + label.align(stackItem.alignOptions, null, stackBox); + + // Set visibility (#678) + alignAttr = label.alignAttr; + label[ + stackItem.options.crop === false || chart.isInsidePlot( + alignAttr.x, + alignAttr.y + ) ? 'show' : 'hide'](true); + } + }, + getStackBox: function (chart, stackItem, x, y, xWidth, h) { + var reversed = stackItem.axis.reversed, + inverted = chart.inverted, + plotHeight = chart.plotHeight, + neg = (stackItem.isNegative && !reversed) || + (!stackItem.isNegative && reversed); // #4056 + + return { // this is the box for the complete stack + x: inverted ? (neg ? y : y - h) : x, + y: inverted ? + plotHeight - x - xWidth : + (neg ? + (plotHeight - y - h) : + plotHeight - y + ), + width: inverted ? h : xWidth, + height: inverted ? xWidth : h + }; + } +}; + +/** + * Generate stacks for each series and calculate stacks total values + */ +Chart.prototype.getStacks = function () { + var chart = this; + + // reset stacks for each yAxis + each(chart.yAxis, function (axis) { + if (axis.stacks && axis.hasVisibleSeries) { + axis.oldStacks = axis.stacks; + } + }); + + each(chart.series, function (series) { + if (series.options.stacking && (series.visible === true || + chart.options.chart.ignoreHiddenSeries === false)) { + series.stackKey = series.type + pick(series.options.stack, ''); + } + }); +}; + + +// Stacking methods defined on the Axis prototype + +/** + * Build the stacks from top down + */ +Axis.prototype.buildStacks = function () { + var axisSeries = this.series, + reversedStacks = pick(this.options.reversedStacks, true), + len = axisSeries.length, + i; + if (!this.isXAxis) { + this.usePercentage = false; + i = len; + while (i--) { + axisSeries[reversedStacks ? i : len - i - 1].setStackedPoints(); + } + + // Loop up again to compute percent and stream stack + for (i = 0; i < len; i++) { + axisSeries[i].modifyStacks(); + } + } +}; + +Axis.prototype.renderStackTotals = function () { + var axis = this, + chart = axis.chart, + renderer = chart.renderer, + stacks = axis.stacks, + stackTotalGroup = axis.stackTotalGroup; + + // Create a separate group for the stack total labels + if (!stackTotalGroup) { + axis.stackTotalGroup = stackTotalGroup = + renderer.g('stack-labels') + .attr({ + visibility: 'visible', + zIndex: 6 + }) + .add(); + } + + // plotLeft/Top will change when y axis gets wider so we need to translate + // the stackTotalGroup at every render call. See bug #506 and #516 + stackTotalGroup.translate(chart.plotLeft, chart.plotTop); + + // Render each stack total + objectEach(stacks, function (type) { + objectEach(type, function (stack) { + stack.render(stackTotalGroup); + }); + }); +}; + +/** + * Set all the stacks to initial states and destroy unused ones. + */ +Axis.prototype.resetStacks = function () { + var axis = this, + stacks = axis.stacks; + if (!axis.isXAxis) { + objectEach(stacks, function (type) { + objectEach(type, function (stack, key) { + // Clean up memory after point deletion (#1044, #4320) + if (stack.touched < axis.stacksTouched) { + stack.destroy(); + delete type[key]; + + // Reset stacks + } else { + stack.total = null; + stack.cumulative = null; + } + }); + }); + } +}; + +Axis.prototype.cleanStacks = function () { + var stacks; + + if (!this.isXAxis) { + if (this.oldStacks) { + stacks = this.stacks = this.oldStacks; + } + + // reset stacks + objectEach(stacks, function (type) { + objectEach(type, function (stack) { + stack.cumulative = stack.total; + }); + }); + } +}; + + +// Stacking methods defnied for Series prototype + +/** + * Adds series' points value to corresponding stack + */ +Series.prototype.setStackedPoints = function () { + if (!this.options.stacking || (this.visible !== true && + this.chart.options.chart.ignoreHiddenSeries !== false)) { + return; + } + + var series = this, + xData = series.processedXData, + yData = series.processedYData, + stackedYData = [], + yDataLength = yData.length, + seriesOptions = series.options, + threshold = seriesOptions.threshold, + stackThreshold = pick(seriesOptions.startFromThreshold && threshold, 0), + stackOption = seriesOptions.stack, + stacking = seriesOptions.stacking, + stackKey = series.stackKey, + negKey = '-' + stackKey, + negStacks = series.negStacks, + yAxis = series.yAxis, + stacks = yAxis.stacks, + oldStacks = yAxis.oldStacks, + stackIndicator, + isNegative, + stack, + other, + key, + pointKey, + i, + x, + y; + + + yAxis.stacksTouched += 1; + + // loop over the non-null y values and read them into a local array + for (i = 0; i < yDataLength; i++) { + x = xData[i]; + y = yData[i]; + stackIndicator = series.getStackIndicator( + stackIndicator, + x, + series.index + ); + pointKey = stackIndicator.key; + // Read stacked values into a stack based on the x value, + // the sign of y and the stack key. Stacking is also handled for null + // values (#739) + isNegative = negStacks && y < (stackThreshold ? 0 : threshold); + key = isNegative ? negKey : stackKey; + + // Create empty object for this stack if it doesn't exist yet + if (!stacks[key]) { + stacks[key] = {}; + } + + // Initialize StackItem for this x + if (!stacks[key][x]) { + if (oldStacks[key] && oldStacks[key][x]) { + stacks[key][x] = oldStacks[key][x]; + stacks[key][x].total = null; + } else { + stacks[key][x] = new H.StackItem( + yAxis, + yAxis.options.stackLabels, + isNegative, + x, + stackOption + ); + } + } + + // If the StackItem doesn't exist, create it first + stack = stacks[key][x]; + if (y !== null) { + stack.points[pointKey] = stack.points[series.index] = + [pick(stack.cumulative, stackThreshold)]; + + // Record the base of the stack + if (!defined(stack.cumulative)) { + stack.base = pointKey; + } + stack.touched = yAxis.stacksTouched; + + + // In area charts, if there are multiple points on the same X value, + // let the area fill the full span of those points + if (stackIndicator.index > 0 && series.singleStacks === false) { + stack.points[pointKey][0] = + stack.points[series.index + ',' + x + ',0'][0]; + } + + // When updating to null, reset the point stack (#7493) + } else { + stack.points[pointKey] = stack.points[series.index] = null; + } + + // Add value to the stack total + if (stacking === 'percent') { + + // Percent stacked column, totals are the same for the positive and + // negative stacks + other = isNegative ? stackKey : negKey; + if (negStacks && stacks[other] && stacks[other][x]) { + other = stacks[other][x]; + stack.total = other.total = + Math.max(other.total, stack.total) + Math.abs(y) || 0; + + // Percent stacked areas + } else { + stack.total = correctFloat(stack.total + (Math.abs(y) || 0)); + } + } else { + stack.total = correctFloat(stack.total + (y || 0)); + } + + stack.cumulative = pick(stack.cumulative, stackThreshold) + (y || 0); + + if (y !== null) { + stack.points[pointKey].push(stack.cumulative); + stackedYData[i] = stack.cumulative; + } + + } + + if (stacking === 'percent') { + yAxis.usePercentage = true; + } + + this.stackedYData = stackedYData; // To be used in getExtremes + + // Reset old stacks + yAxis.oldStacks = {}; +}; + +/** + * Iterate over all stacks and compute the absolute values to percent + */ +Series.prototype.modifyStacks = function () { + var series = this, + stackKey = series.stackKey, + stacks = series.yAxis.stacks, + processedXData = series.processedXData, + stackIndicator, + stacking = series.options.stacking; + + if (series[stacking + 'Stacker']) { // Modifier function exists + each([stackKey, '-' + stackKey], function (key) { + var i = processedXData.length, + x, + stack, + pointExtremes; + + while (i--) { + x = processedXData[i]; + stackIndicator = series.getStackIndicator( + stackIndicator, + x, + series.index, + key + ); + stack = stacks[key] && stacks[key][x]; + pointExtremes = stack && stack.points[stackIndicator.key]; + if (pointExtremes) { + series[stacking + 'Stacker'](pointExtremes, stack, i); + } + } + }); + } +}; + +/** + * Modifier function for percent stacks. Blows up the stack to 100%. + */ +Series.prototype.percentStacker = function (pointExtremes, stack, i) { + var totalFactor = stack.total ? 100 / stack.total : 0; + // Y bottom value + pointExtremes[0] = correctFloat(pointExtremes[0] * totalFactor); + // Y value + pointExtremes[1] = correctFloat(pointExtremes[1] * totalFactor); + this.stackedYData[i] = pointExtremes[1]; +}; + +/** +* Get stack indicator, according to it's x-value, to determine points with the +* same x-value +*/ +Series.prototype.getStackIndicator = function (stackIndicator, x, index, key) { + // Update stack indicator, when: + // first point in a stack || x changed || stack type (negative vs positive) + // changed: + if (!defined(stackIndicator) || stackIndicator.x !== x || + (key && stackIndicator.key !== key)) { + stackIndicator = { + x: x, + index: 0, + key: key + }; + } else { + stackIndicator.index++; + } + + stackIndicator.key = [index, x, stackIndicator.index].join(','); + + return stackIndicator; +}; + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var addEvent = H.addEvent, + animate = H.animate, + Axis = H.Axis, + Chart = H.Chart, + createElement = H.createElement, + css = H.css, + defined = H.defined, + each = H.each, + erase = H.erase, + extend = H.extend, + fireEvent = H.fireEvent, + inArray = H.inArray, + isNumber = H.isNumber, + isObject = H.isObject, + isArray = H.isArray, + merge = H.merge, + objectEach = H.objectEach, + pick = H.pick, + Point = H.Point, + Series = H.Series, + seriesTypes = H.seriesTypes, + setAnimation = H.setAnimation, + splat = H.splat; + +// Extend the Chart prototype for dynamic methods +extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { + + /** + * Add a series to the chart after render time. Note that this method should + * never be used when adding data synchronously at chart render time, as it + * adds expense to the calculations and rendering. When adding data at the + * same time as the chart is initialized, add the series as a configuration + * option instead. With multiple axes, the `offset` is dynamically adjusted. + * + * @param {SeriesOptions} options + * The config options for the series. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after adding. + * @param {AnimationOptions} animation + * Whether to apply animation, and optionally animation + * configuration. + * + * @return {Highcharts.Series} + * The newly created series object. + * + * @sample highcharts/members/chart-addseries/ + * Add a series from a button + * @sample stock/members/chart-addseries/ + * Add a series in Highstock + */ + addSeries: function (options, redraw, animation) { + var series, + chart = this; + + if (options) { + redraw = pick(redraw, true); // defaults to true + + fireEvent(chart, 'addSeries', { options: options }, function () { + series = chart.initSeries(options); + + chart.isDirtyLegend = true; // the series array is out of sync with the display + chart.linkSeries(); + if (redraw) { + chart.redraw(animation); + } + }); + } + + return series; + }, + + /** + * Add an axis to the chart after render time. Note that this method should + * never be used when adding data synchronously at chart render time, as it + * adds expense to the calculations and rendering. When adding data at the + * same time as the chart is initialized, add the axis as a configuration + * option instead. + * @param {AxisOptions} options + * The axis options. + * @param {Boolean} [isX=false] + * Whether it is an X axis or a value axis. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after adding. + * @param {AnimationOptions} [animation=true] + * Whether and how to apply animation in the redraw. + * + * @sample highcharts/members/chart-addaxis/ Add and remove axes + * + * @return {Axis} + * The newly generated Axis object. + */ + addAxis: function (options, isX, redraw, animation) { + var key = isX ? 'xAxis' : 'yAxis', + chartOptions = this.options, + userOptions = merge(options, { + index: this[key].length, + isX: isX + }), + axis; + + axis = new Axis(this, userOptions); + + // Push the new axis options to the chart options + chartOptions[key] = splat(chartOptions[key] || {}); + chartOptions[key].push(userOptions); + + if (pick(redraw, true)) { + this.redraw(animation); + } + + return axis; + }, + + /** + * Dim the chart and show a loading text or symbol. Options for the loading + * screen are defined in {@link + * https://api.highcharts.com/highcharts/loading|the loading options}. + * + * @param {String} str + * An optional text to show in the loading label instead of the + * default one. The default text is set in {@link + * http://api.highcharts.com/highcharts/lang.loading|lang.loading}. + * + * @sample highcharts/members/chart-hideloading/ + * Show and hide loading from a button + * @sample highcharts/members/chart-showloading/ + * Apply different text labels + * @sample stock/members/chart-show-hide-loading/ + * Toggle loading in Highstock + */ + showLoading: function (str) { + var chart = this, + options = chart.options, + loadingDiv = chart.loadingDiv, + loadingOptions = options.loading, + setLoadingSize = function () { + if (loadingDiv) { + css(loadingDiv, { + left: chart.plotLeft + 'px', + top: chart.plotTop + 'px', + width: chart.plotWidth + 'px', + height: chart.plotHeight + 'px' + }); + } + }; + + // create the layer at the first call + if (!loadingDiv) { + chart.loadingDiv = loadingDiv = createElement('div', { + className: 'highcharts-loading highcharts-loading-hidden' + }, null, chart.container); + + chart.loadingSpan = createElement( + 'span', + { className: 'highcharts-loading-inner' }, + null, + loadingDiv + ); + addEvent(chart, 'redraw', setLoadingSize); // #1080 + } + + loadingDiv.className = 'highcharts-loading'; + + // Update text + chart.loadingSpan.innerHTML = str || options.lang.loading; + + + // Update visuals + css(loadingDiv, extend(loadingOptions.style, { + zIndex: 10 + })); + css(chart.loadingSpan, loadingOptions.labelStyle); + + // Show it + if (!chart.loadingShown) { + css(loadingDiv, { + opacity: 0, + display: '' + }); + animate(loadingDiv, { + opacity: loadingOptions.style.opacity || 0.5 + }, { + duration: loadingOptions.showDuration || 0 + }); + } + + + chart.loadingShown = true; + setLoadingSize(); + }, + + /** + * Hide the loading layer. + * + * @see Highcharts.Chart#showLoading + * @sample highcharts/members/chart-hideloading/ + * Show and hide loading from a button + * @sample stock/members/chart-show-hide-loading/ + * Toggle loading in Highstock + */ + hideLoading: function () { + var options = this.options, + loadingDiv = this.loadingDiv; + + if (loadingDiv) { + loadingDiv.className = 'highcharts-loading highcharts-loading-hidden'; + + animate(loadingDiv, { + opacity: 0 + }, { + duration: options.loading.hideDuration || 100, + complete: function () { + css(loadingDiv, { display: 'none' }); + } + }); + + } + this.loadingShown = false; + }, + + /** + * These properties cause isDirtyBox to be set to true when updating. Can be extended from plugins. + */ + propsRequireDirtyBox: ['backgroundColor', 'borderColor', 'borderWidth', 'margin', 'marginTop', 'marginRight', + 'marginBottom', 'marginLeft', 'spacing', 'spacingTop', 'spacingRight', 'spacingBottom', 'spacingLeft', + 'borderRadius', 'plotBackgroundColor', 'plotBackgroundImage', 'plotBorderColor', 'plotBorderWidth', + 'plotShadow', 'shadow'], + + /** + * These properties cause all series to be updated when updating. Can be + * extended from plugins. + */ + propsRequireUpdateSeries: ['chart.inverted', 'chart.polar', + 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions', + 'time', 'tooltip'], + + /** + * A generic function to update any element of the chart. Elements can be + * enabled and disabled, moved, re-styled, re-formatted etc. + * + * A special case is configuration objects that take arrays, for example + * {@link https://api.highcharts.com/highcharts/xAxis|xAxis}, + * {@link https://api.highcharts.com/highcharts/yAxis|yAxis} or + * {@link https://api.highcharts.com/highcharts/series|series}. For these + * collections, an `id` option is used to map the new option set to an + * existing object. If an existing object of the same id is not found, the + * corresponding item is updated. So for example, running `chart.update` + * with a series item without an id, will cause the existing chart's series + * with the same index in the series array to be updated. When the + * `oneToOne` parameter is true, `chart.update` will also take care of + * adding and removing items from the collection. Read more under the + * parameter description below. + * + * See also the {@link https://api.highcharts.com/highcharts/responsive| + * responsive option set}. Switching between `responsive.rules` basically + * runs `chart.update` under the hood. + * + * @param {Options} options + * A configuration object for the new chart options. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart. + * @param {Boolean} [oneToOne=false] + * When `true`, the `series`, `xAxis` and `yAxis` collections will + * be updated one to one, and items will be either added or removed + * to match the new updated options. For example, if the chart has + * two series and we call `chart.update` with a configuration + * containing three series, one will be added. If we call + * `chart.update` with one series, one will be removed. Setting an + * empty `series` array will remove all series, but leaving out the + * `series` property will leave all series untouched. If the series + * have id's, the new series options will be matched by id, and the + * remaining ones removed. + * + * @sample highcharts/members/chart-update/ + * Update chart geometry + */ + update: function (options, redraw, oneToOne) { + var chart = this, + adders = { + credits: 'addCredits', + title: 'setTitle', + subtitle: 'setSubtitle' + }, + optionsChart = options.chart, + updateAllAxes, + updateAllSeries, + newWidth, + newHeight, + itemsForRemoval = []; + + // If the top-level chart option is present, some special updates are required + if (optionsChart) { + merge(true, chart.options.chart, optionsChart); + + // Setter function + if ('className' in optionsChart) { + chart.setClassName(optionsChart.className); + } + + if ('inverted' in optionsChart || 'polar' in optionsChart) { + // Parse options.chart.inverted and options.chart.polar together + // with the available series. + chart.propFromSeries(); + updateAllAxes = true; + } + + if ('alignTicks' in optionsChart) { // #6452 + updateAllAxes = true; + } + + objectEach(optionsChart, function (val, key) { + if (inArray('chart.' + key, chart.propsRequireUpdateSeries) !== -1) { + updateAllSeries = true; + } + // Only dirty box + if (inArray(key, chart.propsRequireDirtyBox) !== -1) { + chart.isDirtyBox = true; + } + }); + + + if ('style' in optionsChart) { + chart.renderer.setStyle(optionsChart.style); + } + + } + + // Moved up, because tooltip needs updated plotOptions (#6218) + + if (options.colors) { + this.options.colors = options.colors; + } + + + if (options.plotOptions) { + merge(true, this.options.plotOptions, options.plotOptions); + } + + // Some option stuctures correspond one-to-one to chart objects that + // have update methods, for example + // options.credits => chart.credits + // options.legend => chart.legend + // options.title => chart.title + // options.tooltip => chart.tooltip + // options.subtitle => chart.subtitle + // options.mapNavigation => chart.mapNavigation + // options.navigator => chart.navigator + // options.scrollbar => chart.scrollbar + objectEach(options, function (val, key) { + if (chart[key] && typeof chart[key].update === 'function') { + chart[key].update(val, false); + + // If a one-to-one object does not exist, look for an adder function + } else if (typeof chart[adders[key]] === 'function') { + chart[adders[key]](val); + } + + if ( + key !== 'chart' && + inArray(key, chart.propsRequireUpdateSeries) !== -1 + ) { + updateAllSeries = true; + } + }); + + // Setters for collections. For axes and series, each item is referred + // by an id. If the id is not found, it defaults to the corresponding + // item in the collection, so setting one series without an id, will + // update the first series in the chart. Setting two series without + // an id will update the first and the second respectively (#6019) + // chart.update and responsive. + each([ + 'xAxis', + 'yAxis', + 'zAxis', + 'series', + 'colorAxis', + 'pane' + ], function (coll) { + if (options[coll]) { + each(splat(options[coll]), function (newOptions, i) { + var item = ( + defined(newOptions.id) && + chart.get(newOptions.id) + ) || chart[coll][i]; + if (item && item.coll === coll) { + item.update(newOptions, false); + + if (oneToOne) { + item.touched = true; + } + } + + // If oneToOne and no matching item is found, add one + if (!item && oneToOne) { + if (coll === 'series') { + chart.addSeries(newOptions, false) + .touched = true; + } else if (coll === 'xAxis' || coll === 'yAxis') { + chart.addAxis(newOptions, coll === 'xAxis', false) + .touched = true; + } + } + + }); + + // Add items for removal + if (oneToOne) { + each(chart[coll], function (item) { + if (!item.touched) { + itemsForRemoval.push(item); + } else { + delete item.touched; + } + }); + } + + + } + }); + + each(itemsForRemoval, function (item) { + item.remove(false); + }); + + if (updateAllAxes) { + each(chart.axes, function (axis) { + axis.update({}, false); + }); + } + + // Certain options require the whole series structure to be thrown away + // and rebuilt + if (updateAllSeries) { + each(chart.series, function (series) { + series.update({}, false); + }); + } + + // For loading, just update the options, do not redraw + if (options.loading) { + merge(true, chart.options.loading, options.loading); + } + + // Update size. Redraw is forced. + newWidth = optionsChart && optionsChart.width; + newHeight = optionsChart && optionsChart.height; + if ((isNumber(newWidth) && newWidth !== chart.chartWidth) || + (isNumber(newHeight) && newHeight !== chart.chartHeight)) { + chart.setSize(newWidth, newHeight); + } else if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Shortcut to set the subtitle options. This can also be done from {@link + * Chart#update} or {@link Chart#setTitle}. + * + * @param {SubtitleOptions} options + * New subtitle options. The subtitle text itself is set by the + * `options.text` property. + */ + setSubtitle: function (options) { + this.setTitle(undefined, options); + } + + +}); + +// extend the Point prototype for dynamic methods +extend(Point.prototype, /** @lends Highcharts.Point.prototype */ { + /** + * Update point with new options (typically x/y data) and optionally redraw + * the series. + * + * @param {Object} options + * The point options. Point options are handled as described under + * the `series.type.data` item for each series type. For example + * for a line series, if options is a single number, the point will + * be given that number as the main y value. If it is an array, it + * will be interpreted as x and y values respectively. If it is an + * object, advanced options are applied. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the point is updated. If doing + * more operations on the chart, it is best practice to set + * `redraw` to false and call `chart.redraw()` after. + * @param {AnimationOptions} [animation=true] + * Whether to apply animation, and optionally animation + * configuration. + * + * @sample highcharts/members/point-update-column/ + * Update column value + * @sample highcharts/members/point-update-pie/ + * Update pie slice + * @sample maps/members/point-update/ + * Update map area value in Highmaps + */ + update: function (options, redraw, animation, runEvent) { + var point = this, + series = point.series, + graphic = point.graphic, + i, + chart = series.chart, + seriesOptions = series.options; + + redraw = pick(redraw, true); + + function update() { + + point.applyOptions(options); + + // Update visuals + if (point.y === null && graphic) { // #4146 + point.graphic = graphic.destroy(); + } + if (isObject(options, true)) { + // Destroy so we can get new elements + if (graphic && graphic.element) { + // "null" is also a valid symbol + if (options && options.marker && options.marker.symbol !== undefined) { + point.graphic = graphic.destroy(); + } + } + if (options && options.dataLabels && point.dataLabel) { // #2468 + point.dataLabel = point.dataLabel.destroy(); + } + if (point.connector) { + point.connector = point.connector.destroy(); // #7243 + } + } + + // record changes in the parallel arrays + i = point.index; + series.updateParallelArrays(point, i); + + // Record the options to options.data. If the old or the new config + // is an object, use point options, otherwise use raw options + // (#4701, #4916). + seriesOptions.data[i] = ( + isObject(seriesOptions.data[i], true) || + isObject(options, true) + ) ? + point.options : + options; + + // redraw + series.isDirty = series.isDirtyData = true; + if (!series.fixedBox && series.hasCartesianSeries) { // #1906, #2320 + chart.isDirtyBox = true; + } + + if (seriesOptions.legendType === 'point') { // #1831, #1885 + chart.isDirtyLegend = true; + } + if (redraw) { + chart.redraw(animation); + } + } + + // Fire the event with a default handler of doing the update + if (runEvent === false) { // When called from setData + update(); + } else { + point.firePointEvent('update', { options: options }, update); + } + }, + + /** + * Remove a point and optionally redraw the series and if necessary the axes + * @param {Boolean} redraw + * Whether to redraw the chart or wait for an explicit call. When + * doing more operations on the chart, for example running + * `point.remove()` in a loop, it is best practice to set `redraw` + * to false and call `chart.redraw()` after. + * @param {AnimationOptions} [animation=false] + * Whether to apply animation, and optionally animation + * configuration. + * + * @sample highcharts/plotoptions/series-point-events-remove/ + * Remove point and confirm + * @sample highcharts/members/point-remove/ + * Remove pie slice + * @sample maps/members/point-remove/ + * Remove selected points in Highmaps + */ + remove: function (redraw, animation) { + this.series.removePoint(inArray(this, this.series.data), redraw, animation); + } +}); + +// Extend the series prototype for dynamic methods +extend(Series.prototype, /** @lends Series.prototype */ { + /** + * Add a point to the series after render time. The point can be added at + * the end, or by giving it an X value, to the start or in the middle of the + * series. + * + * @param {Number|Array|Object} options + * The point options. If options is a single number, a point with + * that y value is appended to the series.If it is an array, it will + * be interpreted as x and y values respectively. If it is an + * object, advanced options as outlined under `series.data` are + * applied. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the point is added. When adding + * more than one point, it is highly recommended that the redraw + * option be set to false, and instead {@link Chart#redraw} + * is explicitly called after the adding of points is finished. + * Otherwise, the chart will redraw after adding each point. + * @param {Boolean} [shift=false] + * If true, a point is shifted off the start of the series as one is + * appended to the end. + * @param {AnimationOptions} [animation] + * Whether to apply animation, and optionally animation + * configuration. + * + * @sample highcharts/members/series-addpoint-append/ + * Append point + * @sample highcharts/members/series-addpoint-append-and-shift/ + * Append and shift + * @sample highcharts/members/series-addpoint-x-and-y/ + * Both X and Y values given + * @sample highcharts/members/series-addpoint-pie/ + * Append pie slice + * @sample stock/members/series-addpoint/ + * Append 100 points in Highstock + * @sample stock/members/series-addpoint-shift/ + * Append and shift in Highstock + * @sample maps/members/series-addpoint/ + * Add a point in Highmaps + */ + addPoint: function (options, redraw, shift, animation) { + var series = this, + seriesOptions = series.options, + data = series.data, + chart = series.chart, + xAxis = series.xAxis, + names = xAxis && xAxis.hasNames && xAxis.names, + dataOptions = seriesOptions.data, + point, + isInTheMiddle, + xData = series.xData, + i, + x; + + // Optional redraw, defaults to true + redraw = pick(redraw, true); + + // Get options and push the point to xData, yData and series.options. In series.generatePoints + // the Point instance will be created on demand and pushed to the series.data array. + point = { series: series }; + series.pointClass.prototype.applyOptions.apply(point, [options]); + x = point.x; + + // Get the insertion point + i = xData.length; + if (series.requireSorting && x < xData[i - 1]) { + isInTheMiddle = true; + while (i && xData[i - 1] > x) { + i--; + } + } + + series.updateParallelArrays(point, 'splice', i, 0, 0); // insert undefined item + series.updateParallelArrays(point, i); // update it + + if (names && point.name) { + names[x] = point.name; + } + dataOptions.splice(i, 0, options); + + if (isInTheMiddle) { + series.data.splice(i, 0, null); + series.processData(); + } + + // Generate points to be added to the legend (#1329) + if (seriesOptions.legendType === 'point') { + series.generatePoints(); + } + + // Shift the first point off the parallel arrays + if (shift) { + if (data[0] && data[0].remove) { + data[0].remove(false); + } else { + data.shift(); + series.updateParallelArrays(point, 'shift'); + + dataOptions.shift(); + } + } + + // redraw + series.isDirty = true; + series.isDirtyData = true; + + if (redraw) { + chart.redraw(animation); // Animation is set anyway on redraw, #5665 + } + }, + + /** + * Remove a point from the series. Unlike the {@link Highcharts.Point#remove} + * method, this can also be done on a point that is not instanciated because + * it is outside the view or subject to Highstock data grouping. + * + * @param {Number} i + * The index of the point in the {@link Highcharts.Series.data|data} + * array. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the point is added. When + * removing more than one point, it is highly recommended that the + * `redraw` option be set to `false`, and instead {@link + * Highcharts.Chart#redraw} is explicitly called after the adding of + * points is finished. + * @param {AnimationOptions} [animation] + * Whether and optionally how the series should be animated. + * + * @sample highcharts/members/series-removepoint/ + * Remove cropped point + */ + removePoint: function (i, redraw, animation) { + + var series = this, + data = series.data, + point = data[i], + points = series.points, + chart = series.chart, + remove = function () { + + if (points && points.length === data.length) { // #4935 + points.splice(i, 1); + } + data.splice(i, 1); + series.options.data.splice(i, 1); + series.updateParallelArrays(point || { series: series }, 'splice', i, 1); + + if (point) { + point.destroy(); + } + + // redraw + series.isDirty = true; + series.isDirtyData = true; + if (redraw) { + chart.redraw(); + } + }; + + setAnimation(animation, chart); + redraw = pick(redraw, true); + + // Fire the event with a default handler of removing the point + if (point) { + point.firePointEvent('remove', null, remove); + } else { + remove(); + } + }, + + /** + * Remove a series and optionally redraw the chart. + * + * @param {Boolean} [redraw=true] + * Whether to redraw the chart or wait for an explicit call to + * {@link Highcharts.Chart#redraw}. + * @param {AnimationOptions} [animation] + * Whether to apply animation, and optionally animation + * configuration + * @param {Boolean} [withEvent=true] + * Used internally, whether to fire the series `remove` event. + * + * @sample highcharts/members/series-remove/ + * Remove first series from a button + */ + remove: function (redraw, animation, withEvent) { + var series = this, + chart = series.chart; + + function remove() { + + // Destroy elements + series.destroy(); + + // Redraw + chart.isDirtyLegend = chart.isDirtyBox = true; + chart.linkSeries(); + + if (pick(redraw, true)) { + chart.redraw(animation); + } + } + + // Fire the event with a default handler of removing the point + if (withEvent !== false) { + fireEvent(series, 'remove', null, remove); + } else { + remove(); + } + }, + + /** + * Update the series with a new set of options. For a clean and precise + * handling of new options, all methods and elements from the series are + * removed, and it is initiated from scratch. Therefore, this method is more + * performance expensive than some other utility methods like {@link + * Series#setData} or {@link Series#setVisible}. + * + * @param {SeriesOptions} options + * New options that will be merged with the series' existing + * options. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the series is altered. If doing + * more operations on the chart, it is a good idea to set redraw to + * false and call {@link Chart#redraw} after. + * + * @sample highcharts/members/series-update/ + * Updating series options + * @sample maps/members/series-update/ + * Update series options in Highmaps + */ + update: function (newOptions, redraw) { + var series = this, + chart = series.chart, + // must use user options when changing type because series.options + // is merged in with type specific plotOptions + oldOptions = series.userOptions, + oldType = series.oldType || series.type, + newType = newOptions.type || oldOptions.type || chart.options.chart.type, + proto = seriesTypes[oldType].prototype, + n, + groups = [ + 'group', + 'markerGroup', + 'dataLabelsGroup' + ], + preserve = [ + 'navigatorSeries', + 'baseSeries' + ], + + // Animation must be enabled when calling update before the initial + // animation has first run. This happens when calling update + // directly after chart initialization, or when applying responsive + // rules (#6912). + animation = series.finishedAnimating && { animation: false }; + + // Running Series.update to update the data only is an intuitive usage, + // so we want to make sure that when used like this, we run the + // cheaper setData function and allow animation instead of completely + // recreating the series instance. + if (Object.keys && Object.keys(newOptions).toString() === 'data') { + return this.setData(newOptions.data, redraw); + } + + // Make sure preserved properties are not destroyed (#3094) + preserve = groups.concat(preserve); + each(preserve, function (prop) { + preserve[prop] = series[prop]; + delete series[prop]; + }); + + // Do the merge, with some forced options + newOptions = merge(oldOptions, animation, { + index: series.index, + pointStart: series.xData[0] // when updating after addPoint + }, { data: series.options.data }, newOptions); + + // Destroy the series and delete all properties. Reinsert all methods + // and properties from the new type prototype (#2270, #3719) + series.remove(false, null, false); + for (n in proto) { + series[n] = undefined; + } + extend(series, seriesTypes[newType || oldType].prototype); + + // Re-register groups (#3094) and other preserved properties + each(preserve, function (prop) { + series[prop] = preserve[prop]; + }); + + series.init(chart, newOptions); + + // Update the Z index of groups (#3380, #7397) + if (newOptions.zIndex !== oldOptions.zIndex) { + each(groups, function (groupName) { + if (series[groupName]) { + series[groupName].attr({ + zIndex: newOptions.zIndex + }); + } + }); + } + + + series.oldType = oldType; + chart.linkSeries(); // Links are lost in series.remove (#3028) + if (pick(redraw, true)) { + chart.redraw(false); + } + } +}); + +// Extend the Axis.prototype for dynamic methods +extend(Axis.prototype, /** @lends Highcharts.Axis.prototype */ { + + /** + * Update an axis object with a new set of options. The options are merged + * with the existing options, so only new or altered options need to be + * specified. + * + * @param {Object} options + * The new options that will be merged in with existing options on + * the axis. + * @sample highcharts/members/axis-update/ Axis update demo + */ + update: function (options, redraw) { + var chart = this.chart; + + options = chart.options[this.coll][this.options.index] = + merge(this.userOptions, options); + + this.destroy(true); + + this.init(chart, extend(options, { events: undefined })); + + chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Remove the axis from the chart. + * + * @param {Boolean} [redraw=true] Whether to redraw the chart following the + * remove. + * + * @sample highcharts/members/chart-addaxis/ Add and remove axes + */ + remove: function (redraw) { + var chart = this.chart, + key = this.coll, // xAxis or yAxis + axisSeries = this.series, + i = axisSeries.length; + + // Remove associated series (#2687) + while (i--) { + if (axisSeries[i]) { + axisSeries[i].remove(false); + } + } + + // Remove the axis + erase(chart.axes, this); + erase(chart[key], this); + + if (isArray(chart.options[key])) { + chart.options[key].splice(this.options.index, 1); + } else { // color axis, #6488 + delete chart.options[key]; + } + + each(chart[key], function (axis, i) { // Re-index, #1706 + axis.options.index = i; + }); + this.destroy(); + chart.isDirtyBox = true; + + if (pick(redraw, true)) { + chart.redraw(); + } + }, + + /** + * Update the axis title by options after render time. + * + * @param {TitleOptions} titleOptions + * The additional title options. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after setting the title. + * @sample highcharts/members/axis-settitle/ Set a new Y axis title + */ + setTitle: function (titleOptions, redraw) { + this.update({ title: titleOptions }, redraw); + }, + + /** + * Set new axis categories and optionally redraw. + * @param {Array.} categories - The new categories. + * @param {Boolean} [redraw=true] - Whether to redraw the chart. + * @sample highcharts/members/axis-setcategories/ Set categories by click on + * a button + */ + setCategories: function (categories, redraw) { + this.update({ categories: categories }, redraw); + } + +}); + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var color = H.color, + each = H.each, + LegendSymbolMixin = H.LegendSymbolMixin, + map = H.map, + pick = H.pick, + Series = H.Series, + seriesType = H.seriesType; + +/** + * Area series type. + * @constructor seriesTypes.area + * @extends {Series} + */ +/** + * The area series type. + * @extends {plotOptions.line} + * @product highcharts highstock + * @sample {highcharts} highcharts/demo/area-basic/ + * Area chart + * @sample {highstock} stock/demo/area/ + * Area chart + * @optionparent plotOptions.area + */ +seriesType('area', 'line', { + + /** + * Fill color or gradient for the area. When `null`, the series' `color` + * is used with the series' `fillOpacity`. + * + * @type {Color} + * @see In styled mode, the fill color can be set with the `.highcharts-area` class name. + * @sample {highcharts} highcharts/plotoptions/area-fillcolor-default/ Null by default + * @sample {highcharts} highcharts/plotoptions/area-fillcolor-gradient/ Gradient + * @default null + * @product highcharts highstock + * @apioption plotOptions.area.fillColor + */ + + /** + * Fill opacity for the area. When you set an explicit `fillColor`, + * the `fillOpacity` is not applied. Instead, you should define the + * opacity in the `fillColor` with an rgba color definition. The `fillOpacity` + * setting, also the default setting, overrides the alpha component + * of the `color` setting. + * + * @type {Number} + * @see In styled mode, the fill opacity can be set with the `.highcharts-area` class name. + * @sample {highcharts} highcharts/plotoptions/area-fillopacity/ Automatic fill color and fill opacity of 0.1 + * @default {highcharts} 0.75 + * @default {highstock} .75 + * @product highcharts highstock + * @apioption plotOptions.area.fillOpacity + */ + + /** + * A separate color for the graph line. By default the line takes the + * `color` of the series, but the lineColor setting allows setting a + * separate color for the line without altering the `fillColor`. + * + * @type {Color} + * @see In styled mode, the line stroke can be set with the `.highcharts-graph` class name. + * @sample {highcharts} highcharts/plotoptions/area-linecolor/ Dark gray line + * @default null + * @product highcharts highstock + * @apioption plotOptions.area.lineColor + */ + + /** + * A separate color for the negative part of the area. + * + * @type {Color} + * @see [negativeColor](#plotOptions.area.negativeColor). In styled mode, a negative + * color is set with the `.highcharts-negative` class name ([view live + * demo](http://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/css/series- + * negative-color/)). + * @since 3.0 + * @product highcharts + * @apioption plotOptions.area.negativeFillColor + */ + + /** + * When this is true, the series will not cause the Y axis to cross + * the zero plane (or [threshold](#plotOptions.series.threshold) option) + * unless the data actually crosses the plane. + * + * For example, if `softThreshold` is `false`, a series of 0, 1, 2, + * 3 will make the Y axis show negative values according to the `minPadding` + * option. If `softThreshold` is `true`, the Y axis starts at 0. + * + * @type {Boolean} + * @default false + * @since 4.1.9 + * @product highcharts highstock + */ + softThreshold: false, + + /** + * The Y axis value to serve as the base for the area, for distinguishing + * between values above and below a threshold. If `null`, the area + * behaves like a line series with fill between the graph and the Y + * axis minimum. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/area-threshold/ A threshold of 100 + * @default 0 + * @since 2.0 + * @product highcharts highstock + */ + threshold: 0 + + /** + * Whether the whole area or just the line should respond to mouseover + * tooltips and other mouse or touch events. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/area-trackbyarea/ Display the tooltip when the area is hovered + * @sample {highstock} highcharts/plotoptions/area-trackbyarea/ Display the tooltip when the area is hovered + * @default false + * @since 1.1.6 + * @product highcharts highstock + * @apioption plotOptions.area.trackByArea + */ + + +}, /** @lends seriesTypes.area.prototype */ { + singleStacks: false, + /** + * Return an array of stacked points, where null and missing points are replaced by + * dummy points in order for gaps to be drawn correctly in stacks. + */ + getStackPoints: function (points) { + var series = this, + segment = [], + keys = [], + xAxis = this.xAxis, + yAxis = this.yAxis, + stack = yAxis.stacks[this.stackKey], + pointMap = {}, + seriesIndex = series.index, + yAxisSeries = yAxis.series, + seriesLength = yAxisSeries.length, + visibleSeries, + upOrDown = pick(yAxis.options.reversedStacks, true) ? 1 : -1, + i; + + + points = points || this.points; + + if (this.options.stacking) { + + for (i = 0; i < points.length; i++) { + // Reset after point update (#7326) + points[i].leftNull = points[i].rightNull = null; + + // Create a map where we can quickly look up the points by their + // X values. + pointMap[points[i].x] = points[i]; + } + + // Sort the keys (#1651) + H.objectEach(stack, function (stackX, x) { + if (stackX.total !== null) { // nulled after switching between grouping and not (#1651, #2336) + keys.push(x); + } + }); + keys.sort(function (a, b) { + return a - b; + }); + + visibleSeries = map(yAxisSeries, function () { + return this.visible; + }); + + each(keys, function (x, idx) { + var y = 0, + stackPoint, + stackedValues; + + if (pointMap[x] && !pointMap[x].isNull) { + segment.push(pointMap[x]); + + // Find left and right cliff. -1 goes left, 1 goes right. + each([-1, 1], function (direction) { + var nullName = direction === 1 ? 'rightNull' : 'leftNull', + cliffName = direction === 1 ? 'rightCliff' : 'leftCliff', + cliff = 0, + otherStack = stack[keys[idx + direction]]; + + // If there is a stack next to this one, to the left or to the right... + if (otherStack) { + i = seriesIndex; + while (i >= 0 && i < seriesLength) { // Can go either up or down, depending on reversedStacks + stackPoint = otherStack.points[i]; + if (!stackPoint) { + // If the next point in this series is missing, mark the point + // with point.leftNull or point.rightNull = true. + if (i === seriesIndex) { + pointMap[x][nullName] = true; + + // If there are missing points in the next stack in any of the + // series below this one, we need to substract the missing values + // and add a hiatus to the left or right. + } else if (visibleSeries[i]) { + stackedValues = stack[x].points[i]; + if (stackedValues) { + cliff -= stackedValues[1] - stackedValues[0]; + } + } + } + // When reversedStacks is true, loop up, else loop down + i += upOrDown; + } + } + pointMap[x][cliffName] = cliff; + }); + + + // There is no point for this X value in this series, so we + // insert a dummy point in order for the areas to be drawn + // correctly. + } else { + + // Loop down the stack to find the series below this one that has + // a value (#1991) + i = seriesIndex; + while (i >= 0 && i < seriesLength) { + stackPoint = stack[x].points[i]; + if (stackPoint) { + y = stackPoint[1]; + break; + } + // When reversedStacks is true, loop up, else loop down + i += upOrDown; + } + y = yAxis.translate(y, 0, 1, 0, 1); // #6272 + segment.push({ + isNull: true, + plotX: xAxis.translate(x, 0, 0, 0, 1), // #6272 + x: x, + plotY: y, + yBottom: y + }); + } + }); + + } + + return segment; + }, + + getGraphPath: function (points) { + var getGraphPath = Series.prototype.getGraphPath, + graphPath, + options = this.options, + stacking = options.stacking, + yAxis = this.yAxis, + topPath, + bottomPath, + bottomPoints = [], + graphPoints = [], + seriesIndex = this.index, + i, + areaPath, + plotX, + stacks = yAxis.stacks[this.stackKey], + threshold = options.threshold, + translatedThreshold = yAxis.getThreshold(options.threshold), + isNull, + yBottom, + connectNulls = options.connectNulls || stacking === 'percent', + /** + * To display null points in underlying stacked series, this series graph must be + * broken, and the area also fall down to fill the gap left by the null point. #2069 + */ + addDummyPoints = function (i, otherI, side) { + var point = points[i], + stackedValues = stacking && stacks[point.x].points[seriesIndex], + nullVal = point[side + 'Null'] || 0, + cliffVal = point[side + 'Cliff'] || 0, + top, + bottom, + isNull = true; + + if (cliffVal || nullVal) { + + top = (nullVal ? stackedValues[0] : stackedValues[1]) + cliffVal; + bottom = stackedValues[0] + cliffVal; + isNull = !!nullVal; + + } else if (!stacking && points[otherI] && points[otherI].isNull) { + top = bottom = threshold; + } + + // Add to the top and bottom line of the area + if (top !== undefined) { + graphPoints.push({ + plotX: plotX, + plotY: top === null ? translatedThreshold : yAxis.getThreshold(top), + isNull: isNull, + isCliff: true + }); + bottomPoints.push({ + plotX: plotX, + plotY: bottom === null ? translatedThreshold : yAxis.getThreshold(bottom), + doCurve: false // #1041, gaps in areaspline areas + }); + } + }; + + // Find what points to use + points = points || this.points; + + // Fill in missing points + if (stacking) { + points = this.getStackPoints(points); + } + + for (i = 0; i < points.length; i++) { + isNull = points[i].isNull; + plotX = pick(points[i].rectPlotX, points[i].plotX); + yBottom = pick(points[i].yBottom, translatedThreshold); + + if (!isNull || connectNulls) { + + if (!connectNulls) { + addDummyPoints(i, i - 1, 'left'); + } + + if (!(isNull && !stacking && connectNulls)) { // Skip null point when stacking is false and connectNulls true + graphPoints.push(points[i]); + bottomPoints.push({ + x: i, + plotX: plotX, + plotY: yBottom + }); + } + + if (!connectNulls) { + addDummyPoints(i, i + 1, 'right'); + } + } + } + + topPath = getGraphPath.call(this, graphPoints, true, true); + + bottomPoints.reversed = true; + bottomPath = getGraphPath.call(this, bottomPoints, true, true); + if (bottomPath.length) { + bottomPath[0] = 'L'; + } + + areaPath = topPath.concat(bottomPath); + graphPath = getGraphPath.call(this, graphPoints, false, connectNulls); // TODO: don't set leftCliff and rightCliff when connectNulls? + + areaPath.xMap = topPath.xMap; + this.areaPath = areaPath; + + return graphPath; + }, + + /** + * Draw the graph and the underlying area. This method calls the Series base + * function and adds the area. The areaPath is calculated in the getSegmentPath + * method called from Series.prototype.drawGraph. + */ + drawGraph: function () { + + // Define or reset areaPath + this.areaPath = []; + + // Call the base method + Series.prototype.drawGraph.apply(this); + + // Define local variables + var series = this, + areaPath = this.areaPath, + options = this.options, + zones = this.zones, + props = [[ + 'area', + 'highcharts-area', + + this.color, + options.fillColor + + ]]; // area name, main color, fill color + + each(zones, function (zone, i) { + props.push([ + 'zone-area-' + i, + 'highcharts-area highcharts-zone-area-' + i + ' ' + zone.className, + + zone.color || series.color, + zone.fillColor || options.fillColor + + ]); + }); + + each(props, function (prop) { + var areaKey = prop[0], + area = series[areaKey]; + + // Create or update the area + if (area) { // update + area.endX = series.preventGraphAnimation ? null : areaPath.xMap; + area.animate({ d: areaPath }); + + } else { // create + area = series[areaKey] = series.chart.renderer.path(areaPath) + .addClass(prop[1]) + .attr({ + + fill: pick( + prop[3], + color(prop[2]).setOpacity(pick(options.fillOpacity, 0.75)).get() + ), + + zIndex: 0 // #1069 + }).add(series.group); + area.isArea = true; + } + area.startX = areaPath.xMap; + area.shiftUnit = options.step ? 2 : 1; + }); + }, + + drawLegendSymbol: LegendSymbolMixin.drawRectangle +}); + +/** + * A `area` series. If the [type](#series.area.type) option is not + * specified, it is inherited from [chart.type](#chart.type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * area](#plotOptions.area). + * + * @type {Object} + * @extends series,plotOptions.area + * @excluding dataParser,dataURL + * @product highcharts highstock + * @apioption series.area + */ + +/** + * An array of data points for the series. For the `area` series type, + * points can be given in the following ways: + * + * 1. An array of numerical values. In this case, the numerical values + * will be interpreted as `y` options. The `x` values will be automatically + * calculated, either starting at 0 and incremented by 1, or from `pointStart` + * and `pointInterval` given in the series options. If the axis has + * categories, these will be used. Example: + * + * ```js + * data: [0, 5, 3, 5] + * ``` + * + * 2. An array of arrays with 2 values. In this case, the values correspond + * to `x,y`. If the first value is a string, it is applied as the name + * of the point, and the `x` value is inferred. + * + * ```js + * data: [ + * [0, 9], + * [1, 7], + * [2, 6] + * ] + * ``` + * + * 3. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.area.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * x: 1, + * y: 9, + * name: "Point2", + * color: "#00FF00" + * }, { + * x: 1, + * y: 6, + * name: "Point1", + * color: "#FF00FF" + * }] + * ``` + * + * @type {Array} + * @extends series.line.data + * @sample {highcharts} highcharts/chart/reflow-true/ Numerical values + * @sample {highcharts} highcharts/series/data-array-of-arrays/ Arrays of numeric x and y + * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/ Arrays of datetime x and y + * @sample {highcharts} highcharts/series/data-array-of-name-value/ Arrays of point.name and y + * @sample {highcharts} highcharts/series/data-array-of-objects/ Config objects + * @product highcharts highstock + * @apioption series.area.data + */ + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var pick = H.pick, + seriesType = H.seriesType; + +/** + * A spline series is a special type of line series, where the segments between + * the data points are smoothed. + * + * @sample {highcharts} highcharts/demo/spline-irregular-time/ Spline chart + * @sample {highstock} stock/demo/spline/ Spline chart + * + * @extends plotOptions.series + * @excluding step + * @product highcharts highstock + * @apioption plotOptions.spline + */ + +/** + * Spline series type. + * @constructor seriesTypes.spline + * @extends {Series} + */ +seriesType('spline', 'line', {}, /** @lends seriesTypes.spline.prototype */ { + /** + * Get the spline segment from a given point's previous neighbour to the + * given point + */ + getPointSpline: function (points, point, i) { + var + // 1 means control points midway between points, 2 means 1/3 from + // the point, 3 is 1/4 etc + smoothing = 1.5, + denom = smoothing + 1, + plotX = point.plotX, + plotY = point.plotY, + lastPoint = points[i - 1], + nextPoint = points[i + 1], + leftContX, + leftContY, + rightContX, + rightContY, + ret; + + function doCurve(otherPoint) { + return otherPoint && + !otherPoint.isNull && + otherPoint.doCurve !== false && + !point.isCliff; // #6387, area splines next to null + } + + // Find control points + if (doCurve(lastPoint) && doCurve(nextPoint)) { + var lastX = lastPoint.plotX, + lastY = lastPoint.plotY, + nextX = nextPoint.plotX, + nextY = nextPoint.plotY, + correction = 0; + + leftContX = (smoothing * plotX + lastX) / denom; + leftContY = (smoothing * plotY + lastY) / denom; + rightContX = (smoothing * plotX + nextX) / denom; + rightContY = (smoothing * plotY + nextY) / denom; + + // Have the two control points make a straight line through main + // point + if (rightContX !== leftContX) { // #5016, division by zero + correction = ((rightContY - leftContY) * (rightContX - plotX)) / + (rightContX - leftContX) + plotY - rightContY; + } + + leftContY += correction; + rightContY += correction; + + // to prevent false extremes, check that control points are between + // neighbouring points' y values + if (leftContY > lastY && leftContY > plotY) { + leftContY = Math.max(lastY, plotY); + // mirror of left control point + rightContY = 2 * plotY - leftContY; + } else if (leftContY < lastY && leftContY < plotY) { + leftContY = Math.min(lastY, plotY); + rightContY = 2 * plotY - leftContY; + } + if (rightContY > nextY && rightContY > plotY) { + rightContY = Math.max(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } else if (rightContY < nextY && rightContY < plotY) { + rightContY = Math.min(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } + + // record for drawing in next point + point.rightContX = rightContX; + point.rightContY = rightContY; + + + } + + // Visualize control points for debugging + /* + if (leftContX) { + this.chart.renderer.circle( + leftContX + this.chart.plotLeft, + leftContY + this.chart.plotTop, + 2 + ) + .attr({ + stroke: 'red', + 'stroke-width': 2, + fill: 'none', + zIndex: 9 + }) + .add(); + this.chart.renderer.path(['M', leftContX + this.chart.plotLeft, + leftContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'red', + 'stroke-width': 2, + zIndex: 9 + }) + .add(); + } + if (rightContX) { + this.chart.renderer.circle( + rightContX + this.chart.plotLeft, + rightContY + this.chart.plotTop, + 2 + ) + .attr({ + stroke: 'green', + 'stroke-width': 2, + fill: 'none', + zIndex: 9 + }) + .add(); + this.chart.renderer.path(['M', rightContX + this.chart.plotLeft, + rightContY + this.chart.plotTop, + 'L', plotX + this.chart.plotLeft, plotY + this.chart.plotTop]) + .attr({ + stroke: 'green', + 'stroke-width': 2, + zIndex: 9 + }) + .add(); + } + // */ + ret = [ + 'C', + pick(lastPoint.rightContX, lastPoint.plotX), + pick(lastPoint.rightContY, lastPoint.plotY), + pick(leftContX, plotX), + pick(leftContY, plotY), + plotX, + plotY + ]; + // reset for updating series later + lastPoint.rightContX = lastPoint.rightContY = null; + return ret; + } +}); + +/** + * A `spline` series. If the [type](#series.spline.type) option is + * not specified, it is inherited from [chart.type](#chart.type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * spline](#plotOptions.spline). + * + * @type {Object} + * @extends series,plotOptions.spline + * @excluding dataParser,dataURL + * @product highcharts highstock + * @apioption series.spline + */ + +/** + * An array of data points for the series. For the `spline` series type, + * points can be given in the following ways: + * + * 1. An array of numerical values. In this case, the numerical values + * will be interpreted as `y` options. The `x` values will be automatically + * calculated, either starting at 0 and incremented by 1, or from `pointStart` + * and `pointInterval` given in the series options. If the axis has + * categories, these will be used. Example: + * + * ```js + * data: [0, 5, 3, 5] + * ``` + * + * 2. An array of arrays with 2 values. In this case, the values correspond + * to `x,y`. If the first value is a string, it is applied as the name + * of the point, and the `x` value is inferred. + * + * ```js + * data: [ + * [0, 9], + * [1, 2], + * [2, 8] + * ] + * ``` + * + * 3. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.spline.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * x: 1, + * y: 9, + * name: "Point2", + * color: "#00FF00" + * }, { + * x: 1, + * y: 0, + * name: "Point1", + * color: "#FF00FF" + * }] + * ``` + * + * @type {Array} + * @extends series.line.data + * @sample {highcharts} highcharts/chart/reflow-true/ + * Numerical values + * @sample {highcharts} highcharts/series/data-array-of-arrays/ + * Arrays of numeric x and y + * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/ + * Arrays of datetime x and y + * @sample {highcharts} highcharts/series/data-array-of-name-value/ + * Arrays of point.name and y + * @sample {highcharts} highcharts/series/data-array-of-objects/ + * Config objects + * @product highcharts highstock + * @apioption series.spline.data + */ + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var areaProto = H.seriesTypes.area.prototype, + defaultPlotOptions = H.defaultPlotOptions, + LegendSymbolMixin = H.LegendSymbolMixin, + seriesType = H.seriesType; +/** + * AreaSplineSeries object + */ +/** + * The area spline series is an area series where the graph between the points + * is smoothed into a spline. + * + * @extends plotOptions.area + * @excluding step + * @sample {highcharts} highcharts/demo/areaspline/ Area spline chart + * @sample {highstock} stock/demo/areaspline/ Area spline chart + * @product highcharts highstock + * @apioption plotOptions.areaspline + */ +seriesType('areaspline', 'spline', defaultPlotOptions.area, { + getStackPoints: areaProto.getStackPoints, + getGraphPath: areaProto.getGraphPath, + drawGraph: areaProto.drawGraph, + drawLegendSymbol: LegendSymbolMixin.drawRectangle +}); +/** + * A `areaspline` series. If the [type](#series.areaspline.type) option + * is not specified, it is inherited from [chart.type](#chart.type). + * + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * areaspline](#plotOptions.areaspline). + * + * @type {Object} + * @extends series,plotOptions.areaspline + * @excluding dataParser,dataURL + * @product highcharts highstock + * @apioption series.areaspline + */ + + +/** + * An array of data points for the series. For the `areaspline` series + * type, points can be given in the following ways: + * + * 1. An array of numerical values. In this case, the numerical values + * will be interpreted as `y` options. The `x` values will be automatically + * calculated, either starting at 0 and incremented by 1, or from `pointStart` + * and `pointInterval` given in the series options. If the axis has + * categories, these will be used. Example: + * + * ```js + * data: [0, 5, 3, 5] + * ``` + * + * 2. An array of arrays with 2 values. In this case, the values correspond + * to `x,y`. If the first value is a string, it is applied as the name + * of the point, and the `x` value is inferred. + * + * ```js + * data: [ + * [0, 10], + * [1, 9], + * [2, 3] + * ] + * ``` + * + * 3. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.areaspline.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * x: 1, + * y: 4, + * name: "Point2", + * color: "#00FF00" + * }, { + * x: 1, + * y: 4, + * name: "Point1", + * color: "#FF00FF" + * }] + * ``` + * + * @type {Array} + * @extends series.line.data + * @sample {highcharts} highcharts/chart/reflow-true/ Numerical values + * @sample {highcharts} highcharts/series/data-array-of-arrays/ Arrays of numeric x and y + * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/ Arrays of datetime x and y + * @sample {highcharts} highcharts/series/data-array-of-name-value/ Arrays of point.name and y + * @sample {highcharts} highcharts/series/data-array-of-objects/ Config objects + * @product highcharts highstock + * @apioption series.areaspline.data + */ + + + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var animObject = H.animObject, + color = H.color, + each = H.each, + extend = H.extend, + isNumber = H.isNumber, + LegendSymbolMixin = H.LegendSymbolMixin, + merge = H.merge, + noop = H.noop, + pick = H.pick, + Series = H.Series, + seriesType = H.seriesType, + svg = H.svg; +/** + * The column series type. + * + * @constructor seriesTypes.column + * @augments Series + */ + +/** + * Column series display one column per value along an X axis. + * + * @sample {highcharts} highcharts/demo/column-basic/ Column chart + * @sample {highstock} stock/demo/column/ Column chart + * + * @extends {plotOptions.line} + * @product highcharts highstock + * @excluding connectNulls,dashStyle,gapSize,gapUnit,linecap,lineWidth,marker, + * connectEnds,step + * @optionparent plotOptions.column + */ +seriesType('column', 'line', { + + /** + * The corner radius of the border surrounding each column or bar. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/column-borderradius/ + * Rounded columns + * @default 0 + * @product highcharts highstock + */ + borderRadius: 0, + + /** + * The width of the border surrounding each column or bar. + * + * In styled mode, the stroke width can be set with the `.highcharts-point` + * rule. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/column-borderwidth/ + * 2px black border + * @default 1 + * @product highcharts highstock + * @apioption plotOptions.column.borderWidth + */ + + /** + * When using automatic point colors pulled from the `options.colors` + * collection, this option determines whether the chart should receive + * one color per series or one color per point. + * + * @type {Boolean} + * @see [series colors](#plotOptions.column.colors) + * @sample {highcharts} highcharts/plotoptions/column-colorbypoint-false/ + * False by default + * @sample {highcharts} highcharts/plotoptions/column-colorbypoint-true/ + * True + * @default false + * @since 2.0 + * @product highcharts highstock + * @apioption plotOptions.column.colorByPoint + */ + + /** + * A series specific or series type specific color set to apply instead + * of the global [colors](#colors) when [colorByPoint](#plotOptions. + * column.colorByPoint) is true. + * + * @type {Array} + * @since 3.0 + * @product highcharts highstock + * @apioption plotOptions.column.colors + */ + + /** + * When true, each column edge is rounded to its nearest pixel in order + * to render sharp on screen. In some cases, when there are a lot of + * densely packed columns, this leads to visible difference in column + * widths or distance between columns. In these cases, setting `crisp` + * to `false` may look better, even though each column is rendered + * blurry. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/column-crisp-false/ + * Crisp is false + * @default true + * @since 5.0.10 + * @product highcharts highstock + */ + crisp: true, + + /** + * Padding between each value groups, in x axis units. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/column-grouppadding-default/ + * 0.2 by default + * @sample {highcharts} highcharts/plotoptions/column-grouppadding-none/ + * No group padding - all columns are evenly spaced + * @default 0.2 + * @product highcharts highstock + */ + groupPadding: 0.2, + + /** + * Whether to group non-stacked columns or to let them render independent + * of each other. Non-grouped columns will be laid out individually + * and overlap each other. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/column-grouping-false/ + * Grouping disabled + * @sample {highstock} highcharts/plotoptions/column-grouping-false/ + * Grouping disabled + * @default true + * @since 2.3.0 + * @product highcharts highstock + * @apioption plotOptions.column.grouping + */ + + marker: null, // point options are specified in the base options + + /** + * The maximum allowed pixel width for a column, translated to the height + * of a bar in a bar chart. This prevents the columns from becoming + * too wide when there is a small number of points in the chart. + * + * @type {Number} + * @see [pointWidth](#plotOptions.column.pointWidth) + * @sample {highcharts} highcharts/plotoptions/column-maxpointwidth-20/ + * Limited to 50 + * @sample {highstock} highcharts/plotoptions/column-maxpointwidth-20/ + * Limited to 50 + * @default null + * @since 4.1.8 + * @product highcharts highstock + * @apioption plotOptions.column.maxPointWidth + */ + + /** + * Padding between each column or bar, in x axis units. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/column-pointpadding-default/ + * 0.1 by default + * @sample {highcharts} highcharts/plotoptions/column-pointpadding-025/ + * 0.25 + * @sample {highcharts} highcharts/plotoptions/column-pointpadding-none/ + * 0 for tightly packed columns + * @default 0.1 + * @product highcharts highstock + */ + pointPadding: 0.1, + + /** + * A pixel value specifying a fixed width for each column or bar. When + * `null`, the width is calculated from the `pointPadding` and + * `groupPadding`. + * + * @type {Number} + * @see [maxPointWidth](#plotOptions.column.maxPointWidth) + * @sample {highcharts} highcharts/plotoptions/column-pointwidth-20/ + * 20px wide columns regardless of chart width or the amount of data + * points + * @default null + * @since 1.2.5 + * @product highcharts highstock + * @apioption plotOptions.column.pointWidth + */ + + /** + * The minimal height for a column or width for a bar. By default, + * 0 values are not shown. To visualize a 0 (or close to zero) point, + * set the minimal point length to a pixel value like 3\. In stacked + * column charts, minPointLength might not be respected for tightly + * packed values. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/column-minpointlength/ + * Zero base value + * @sample {highcharts} highcharts/plotoptions/column-minpointlength-pos-and-neg/ + * Positive and negative close to zero values + * @default 0 + * @product highcharts highstock + */ + minPointLength: 0, + + /** + * When the series contains less points than the crop threshold, all + * points are drawn, event if the points fall outside the visible plot + * area at the current zoom. The advantage of drawing all points (including + * markers and columns), is that animation is performed on updates. + * On the other hand, when the series contains more points than the + * crop threshold, the series data is cropped to only contain points + * that fall within the plot area. The advantage of cropping away invisible + * points is to increase performance on large series. . + * + * @type {Number} + * @default 50 + * @product highcharts highstock + */ + cropThreshold: 50, + + /** + * The X axis range that each point is valid for. This determines the + * width of the column. On a categorized axis, the range will be 1 + * by default (one category unit). On linear and datetime axes, the + * range will be computed as the distance between the two closest data + * points. + * + * The default `null` means it is computed automatically, but this option + * can be used to override the automatic value. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/column-pointrange/ + * Set the point range to one day on a data set with one week + * between the points + * @default null + * @since 2.3 + * @product highcharts highstock + */ + pointRange: null, + + states: { + + /** + * Options for the hovered point. These settings override the normal + * state options when a point is moused over or touched. + * + * @extends plotOptions.series.states.hover + * @excluding halo,lineWidth,lineWidthPlus,marker + * @product highcharts highstock + */ + hover: { + + /** + * @ignore-option + */ + halo: false, + /** + * A specific border color for the hovered point. Defaults to + * inherit the normal state border color. + * + * @type {Color} + * @product highcharts + * @apioption plotOptions.column.states.hover.borderColor + */ + + /** + * A specific color for the hovered point. + * + * @type {Color} + * @default undefined + * @product highcharts + * @apioption plotOptions.column.states.hover.color + */ + + + + /** + * How much to brighten the point on interaction. Requires the main + * color to be defined in hex or rgb(a) format. + * + * In styled mode, the hover brightening is by default replaced + * with a fill-opacity set in the `.highcharts-point:hover` rule. + * + * @sample {highcharts} + * highcharts/plotoptions/column-states-hover-brightness/ + * Brighten by 0.5 + * @product highcharts highstock + */ + brightness: 0.1 + + + }, + + + /** + * Options for the selected point. These settings override the normal + * state options when a point is selected. + * + * @excluding halo,lineWidth,lineWidthPlus,marker + * @product highcharts highstock + */ + select: { + /** + * A specific color for the selected point. + * + * @type {Color} + * @default #cccccc + * @product highcharts highstock + */ + color: '#cccccc', + + /** + * A specific border color for the selected point. + * + * @type {Color} + * @default #000000 + * @product highcharts highstock + */ + borderColor: '#000000' + } + + }, + + dataLabels: { + align: null, // auto + verticalAlign: null, // auto + y: null + }, + + /** + * When this is true, the series will not cause the Y axis to cross + * the zero plane (or [threshold](#plotOptions.series.threshold) option) + * unless the data actually crosses the plane. + * + * For example, if `softThreshold` is `false`, a series of 0, 1, 2, + * 3 will make the Y axis show negative values according to the `minPadding` + * option. If `softThreshold` is `true`, the Y axis starts at 0. + * + * @type {Boolean} + * @default {highcharts} true + * @default {highstock} false + * @since 4.1.9 + * @product highcharts highstock + */ + softThreshold: false, + + // false doesn't work well: http://jsfiddle.net/highcharts/hz8fopan/14/ + /** @ignore */ + startFromThreshold: true, + + stickyTracking: false, + + tooltip: { + distance: 6 + }, + + /** + * The Y axis value to serve as the base for the columns, for distinguishing + * between values above and below a threshold. If `null`, the columns + * extend from the padding Y axis minimum. + * + * @type {Number} + * @default 0 + * @since 2.0 + * @product highcharts + */ + threshold: 0, + + + /** + * The color of the border surrounding each column or bar. + * + * In styled mode, the border stroke can be set with the `.highcharts-point` + * rule. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/column-bordercolor/ + * Dark gray border + * @default #ffffff + * @product highcharts highstock + */ + borderColor: '#ffffff' + // borderWidth: 1 + + +}, /** @lends seriesTypes.column.prototype */ { + cropShoulder: 0, + // When tooltip is not shared, this series (and derivatives) requires direct + // touch/hover. KD-tree does not apply. + directTouch: true, + trackerGroups: ['group', 'dataLabelsGroup'], + // use separate negative stacks, unlike area stacks where a negative point + // is substracted from previous (#1910) + negStacks: true, + + /** + * Initialize the series. Extends the basic Series.init method by + * marking other series of the same type as dirty. + * + * @function #init + * @memberOf seriesTypes.column + * + */ + init: function () { + Series.prototype.init.apply(this, arguments); + + var series = this, + chart = series.chart; + + // if the series is added dynamically, force redraw of other + // series affected by a new column + if (chart.hasRendered) { + each(chart.series, function (otherSeries) { + if (otherSeries.type === series.type) { + otherSeries.isDirty = true; + } + }); + } + }, + + /** + * Return the width and x offset of the columns adjusted for grouping, + * groupPadding, pointPadding, pointWidth etc. + */ + getColumnMetrics: function () { + + var series = this, + options = series.options, + xAxis = series.xAxis, + yAxis = series.yAxis, + reversedXAxis = xAxis.reversed, + stackKey, + stackGroups = {}, + columnCount = 0; + + // Get the total number of column type series. This is called on every + // series. Consider moving this logic to a chart.orderStacks() function + // and call it on init, addSeries and removeSeries + if (options.grouping === false) { + columnCount = 1; + } else { + each(series.chart.series, function (otherSeries) { + var otherOptions = otherSeries.options, + otherYAxis = otherSeries.yAxis, + columnIndex; + if ( + otherSeries.type === series.type && + ( + otherSeries.visible || + !series.chart.options.chart.ignoreHiddenSeries + ) && + yAxis.len === otherYAxis.len && + yAxis.pos === otherYAxis.pos + ) { // #642, #2086 + if (otherOptions.stacking) { + stackKey = otherSeries.stackKey; + if (stackGroups[stackKey] === undefined) { + stackGroups[stackKey] = columnCount++; + } + columnIndex = stackGroups[stackKey]; + } else if (otherOptions.grouping !== false) { // #1162 + columnIndex = columnCount++; + } + otherSeries.columnIndex = columnIndex; + } + }); + } + + var categoryWidth = Math.min( + Math.abs(xAxis.transA) * ( + xAxis.ordinalSlope || + options.pointRange || + xAxis.closestPointRange || + xAxis.tickInterval || + 1 + ), // #2610 + xAxis.len // #1535 + ), + groupPadding = categoryWidth * options.groupPadding, + groupWidth = categoryWidth - 2 * groupPadding, + pointOffsetWidth = groupWidth / (columnCount || 1), + pointWidth = Math.min( + options.maxPointWidth || xAxis.len, + pick( + options.pointWidth, + pointOffsetWidth * (1 - 2 * options.pointPadding) + ) + ), + pointPadding = (pointOffsetWidth - pointWidth) / 2, + // #1251, #3737 + colIndex = (series.columnIndex || 0) + (reversedXAxis ? 1 : 0), + pointXOffset = + pointPadding + + ( + groupPadding + + colIndex * pointOffsetWidth - + (categoryWidth / 2) + ) * (reversedXAxis ? -1 : 1); + + // Save it for reading in linked series (Error bars particularly) + series.columnMetrics = { + width: pointWidth, + offset: pointXOffset + }; + return series.columnMetrics; + + }, + + /** + * Make the columns crisp. The edges are rounded to the nearest full pixel. + */ + crispCol: function (x, y, w, h) { + var chart = this.chart, + borderWidth = this.borderWidth, + xCrisp = -(borderWidth % 2 ? 0.5 : 0), + yCrisp = borderWidth % 2 ? 0.5 : 1, + right, + bottom, + fromTop; + + if (chart.inverted && chart.renderer.isVML) { + yCrisp += 1; + } + + // Horizontal. We need to first compute the exact right edge, then round + // it and compute the width from there. + if (this.options.crisp) { + right = Math.round(x + w) + xCrisp; + x = Math.round(x) + xCrisp; + w = right - x; + } + + // Vertical + bottom = Math.round(y + h) + yCrisp; + fromTop = Math.abs(y) <= 0.5 && bottom > 0.5; // #4504, #4656 + y = Math.round(y) + yCrisp; + h = bottom - y; + + // Top edges are exceptions + if (fromTop && h) { // #5146 + y -= 1; + h += 1; + } + + return { + x: x, + y: y, + width: w, + height: h + }; + }, + + /** + * Translate each point to the plot area coordinate system and find shape + * positions + */ + translate: function () { + var series = this, + chart = series.chart, + options = series.options, + dense = series.dense = + series.closestPointRange * series.xAxis.transA < 2, + borderWidth = series.borderWidth = pick( + options.borderWidth, + dense ? 0 : 1 // #3635 + ), + yAxis = series.yAxis, + threshold = options.threshold, + translatedThreshold = series.translatedThreshold = + yAxis.getThreshold(threshold), + minPointLength = pick(options.minPointLength, 5), + metrics = series.getColumnMetrics(), + pointWidth = metrics.width, + // postprocessed for border width + seriesBarW = series.barW = + Math.max(pointWidth, 1 + 2 * borderWidth), + pointXOffset = series.pointXOffset = metrics.offset; + + if (chart.inverted) { + translatedThreshold -= 0.5; // #3355 + } + + // When the pointPadding is 0, we want the columns to be packed tightly, + // so we allow individual columns to have individual sizes. When + // pointPadding is greater, we strive for equal-width columns (#2694). + if (options.pointPadding) { + seriesBarW = Math.ceil(seriesBarW); + } + + Series.prototype.translate.apply(series); + + // Record the new values + each(series.points, function (point) { + var yBottom = pick(point.yBottom, translatedThreshold), + safeDistance = 999 + Math.abs(yBottom), + plotY = Math.min( + Math.max(-safeDistance, point.plotY), + yAxis.len + safeDistance + ), // Don't draw too far outside plot area (#1303, #2241, #4264) + barX = point.plotX + pointXOffset, + barW = seriesBarW, + barY = Math.min(plotY, yBottom), + up, + barH = Math.max(plotY, yBottom) - barY; + + // Handle options.minPointLength + if (minPointLength && Math.abs(barH) < minPointLength) { + barH = minPointLength; + up = (!yAxis.reversed && !point.negative) || + (yAxis.reversed && point.negative); + + // Reverse zeros if there's no positive value in the series + // in visible range (#7046) + if ( + point.y === threshold && + series.dataMax <= threshold && + yAxis.min < threshold // and if there's room for it (#7311) + ) { + up = !up; + } + + // If stacked... + barY = Math.abs(barY - translatedThreshold) > minPointLength ? + // ...keep position + yBottom - minPointLength : + // #1485, #4051 + translatedThreshold - (up ? minPointLength : 0); + } + + // Cache for access in polar + point.barX = barX; + point.pointWidth = pointWidth; + + // Fix the tooltip on center of grouped columns (#1216, #424, #3648) + point.tooltipPos = chart.inverted ? + [ + yAxis.len + yAxis.pos - chart.plotLeft - plotY, + series.xAxis.len - barX - barW / 2, barH + ] : + [barX + barW / 2, plotY + yAxis.pos - chart.plotTop, barH]; + + // Register shape type and arguments to be used in drawPoints + point.shapeType = 'rect'; + point.shapeArgs = series.crispCol.apply( + series, + point.isNull ? + // #3169, drilldown from null must have a position to work + // from #6585, dataLabel should be placed on xAxis, not + // floating in the middle of the chart + [barX, translatedThreshold, barW, 0] : + [barX, barY, barW, barH] + ); + }); + + }, + + getSymbol: noop, + + /** + * Use a solid rectangle like the area series types + */ + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + + + /** + * Columns have no graph + */ + drawGraph: function () { + this.group[ + this.dense ? 'addClass' : 'removeClass' + ]('highcharts-dense-data'); + }, + + + /** + * Get presentational attributes + */ + pointAttribs: function (point, state) { + var options = this.options, + stateOptions, + ret, + p2o = this.pointAttrToOptions || {}, + strokeOption = p2o.stroke || 'borderColor', + strokeWidthOption = p2o['stroke-width'] || 'borderWidth', + fill = (point && point.color) || this.color, + stroke = (point && point[strokeOption]) || options[strokeOption] || + this.color || fill, // set to fill when borderColor null + strokeWidth = (point && point[strokeWidthOption]) || + options[strokeWidthOption] || this[strokeWidthOption] || 0, + dashstyle = options.dashStyle, + zone, + brightness; + + // Handle zone colors + if (point && this.zones.length) { + zone = point.getZone(); + // When zones are present, don't use point.color (#4267). Changed + // order (#6527) + fill = point.options.color || (zone && zone.color) || this.color; + } + + // Select or hover states + if (state) { + stateOptions = merge( + options.states[state], + // #6401 + point.options.states && point.options.states[state] || {} + ); + brightness = stateOptions.brightness; + fill = stateOptions.color || + ( + brightness !== undefined && + color(fill).brighten(stateOptions.brightness).get() + ) || + fill; + stroke = stateOptions[strokeOption] || stroke; + strokeWidth = stateOptions[strokeWidthOption] || strokeWidth; + dashstyle = stateOptions.dashStyle || dashstyle; + } + + ret = { + 'fill': fill, + 'stroke': stroke, + 'stroke-width': strokeWidth + }; + + if (dashstyle) { + ret.dashstyle = dashstyle; + } + + return ret; + }, + + + /** + * Draw the columns. For bars, the series.group is rotated, so the same + * coordinates apply for columns and bars. This method is inherited by + * scatter series. + */ + drawPoints: function () { + var series = this, + chart = this.chart, + options = series.options, + renderer = chart.renderer, + animationLimit = options.animationLimit || 250, + shapeArgs; + + // draw the columns + each(series.points, function (point) { + var plotY = point.plotY, + graphic = point.graphic; + + if (isNumber(plotY) && point.y !== null) { + shapeArgs = point.shapeArgs; + + if (graphic) { // update + graphic[ + chart.pointCount < animationLimit ? 'animate' : 'attr' + ]( + merge(shapeArgs) + ); + + } else { + point.graphic = graphic = + renderer[point.shapeType](shapeArgs) + .add(point.group || series.group); + } + + // Border radius is not stylable (#6900) + if (options.borderRadius) { + graphic.attr({ + r: options.borderRadius + }); + } + + + // Presentational + graphic + .attr(series.pointAttribs( + point, + point.selected && 'select' + )) + .shadow( + options.shadow, + null, + options.stacking && !options.borderRadius + ); + + + graphic.addClass(point.getClassName(), true); + + + } else if (graphic) { + point.graphic = graphic.destroy(); // #1269 + } + }); + }, + + /** + * Animate the column heights one by one from zero + * @param {Boolean} init Whether to initialize the animation or run it + */ + animate: function (init) { + var series = this, + yAxis = this.yAxis, + options = series.options, + inverted = this.chart.inverted, + attr = {}, + translateProp = inverted ? 'translateX' : 'translateY', + translateStart, + translatedThreshold; + + if (svg) { // VML is too slow anyway + if (init) { + attr.scaleY = 0.001; + translatedThreshold = Math.min( + yAxis.pos + yAxis.len, + Math.max(yAxis.pos, yAxis.toPixels(options.threshold)) + ); + if (inverted) { + attr.translateX = translatedThreshold - yAxis.len; + } else { + attr.translateY = translatedThreshold; + } + series.group.attr(attr); + + } else { // run the animation + translateStart = series.group.attr(translateProp); + series.group.animate( + { scaleY: 1 }, + extend(animObject(series.options.animation + ), { + // Do the scale synchronously to ensure smooth updating + // (#5030, #7228) + step: function (val, fx) { + + attr[translateProp] = + translateStart + + fx.pos * (yAxis.pos - translateStart); + series.group.attr(attr); + } + })); + + // delete this function to allow it only once + series.animate = null; + } + } + }, + + /** + * Remove this series from the chart + */ + remove: function () { + var series = this, + chart = series.chart; + + // column and bar series affects other series of the same type + // as they are either stacked or grouped + if (chart.hasRendered) { + each(chart.series, function (otherSeries) { + if (otherSeries.type === series.type) { + otherSeries.isDirty = true; + } + }); + } + + Series.prototype.remove.apply(series, arguments); + } +}); + + +/** + * A `column` series. If the [type](#series.column.type) option is + * not specified, it is inherited from [chart.type](#chart.type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * column](#plotOptions.column). + * + * @type {Object} + * @extends series,plotOptions.column + * @excluding dataParser,dataURL,marker + * + * @product highcharts highstock + * @apioption series.column + */ + +/** + * @excluding halo,lineWidth,lineWidthPlus,marker + * @product highcharts highstock + * @apioption series.column.states.hover + */ + +/** + * @excluding halo,lineWidth,lineWidthPlus,marker + * @product highcharts highstock + * @apioption series.column.states.select + */ + +/** + * An array of data points for the series. For the `column` series type, + * points can be given in the following ways: + * + * 1. An array of numerical values. In this case, the numerical values + * will be interpreted as `y` options. The `x` values will be automatically + * calculated, either starting at 0 and incremented by 1, or from `pointStart` + * and `pointInterval` given in the series options. If the axis has + * categories, these will be used. Example: + * + * ```js + * data: [0, 5, 3, 5] + * ``` + * + * 2. An array of arrays with 2 values. In this case, the values correspond + * to `x,y`. If the first value is a string, it is applied as the name + * of the point, and the `x` value is inferred. + * + * ```js + * data: [ + * [0, 6], + * [1, 2], + * [2, 6] + * ] + * ``` + * + * 3. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.column.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * x: 1, + * y: 9, + * name: "Point2", + * color: "#00FF00" + * }, { + * x: 1, + * y: 6, + * name: "Point1", + * color: "#FF00FF" + * }] + * ``` + * + * @type {Array} + * @extends series.line.data + * @excluding marker + * @sample {highcharts} highcharts/chart/reflow-true/ Numerical values + * @sample {highcharts} highcharts/series/data-array-of-arrays/ + * Arrays of numeric x and y + * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/ + * Arrays of datetime x and y + * @sample {highcharts} highcharts/series/data-array-of-name-value/ + * Arrays of point.name and y + * @sample {highcharts} highcharts/series/data-array-of-objects/ + * Config objects + * @product highcharts highstock + * @apioption series.column.data + */ + + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ + +var seriesType = H.seriesType; + +/** + * The Bar series class + */ +seriesType('bar', 'column', null, { + inverted: true +}); +/** + * A bar series is a special type of column series where the columns are + * horizontal. + * + * @sample highcharts/demo/bar-basic/ Bar chart + * @extends {plotOptions.column} + * @product highcharts + * @optionparent plotOptions.bar + */ + + +/** + * A `bar` series. If the [type](#series.bar.type) option is not specified, + * it is inherited from [chart.type](#chart.type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * bar](#plotOptions.bar). + * + * @type {Object} + * @extends series,plotOptions.bar + * @excluding dataParser,dataURL + * @product highcharts + * @apioption series.bar + */ + +/** + * An array of data points for the series. For the `bar` series type, + * points can be given in the following ways: + * + * 1. An array of numerical values. In this case, the numerical values + * will be interpreted as `y` options. The `x` values will be automatically + * calculated, either starting at 0 and incremented by 1, or from `pointStart` + * and `pointInterval` given in the series options. If the axis has + * categories, these will be used. Example: + * + * ```js + * data: [0, 5, 3, 5] + * ``` + * + * 2. An array of arrays with 2 values. In this case, the values correspond + * to `x,y`. If the first value is a string, it is applied as the name + * of the point, and the `x` value is inferred. + * + * ```js + * data: [ + * [0, 5], + * [1, 10], + * [2, 3] + * ] + * ``` + * + * 3. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.bar.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * x: 1, + * y: 1, + * name: "Point2", + * color: "#00FF00" + * }, { + * x: 1, + * y: 10, + * name: "Point1", + * color: "#FF00FF" + * }] + * ``` + * + * @type {Array} + * @extends series.column.data + * @sample {highcharts} highcharts/chart/reflow-true/ Numerical values + * @sample {highcharts} highcharts/series/data-array-of-arrays/ Arrays of numeric x and y + * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/ Arrays of datetime x and y + * @sample {highcharts} highcharts/series/data-array-of-name-value/ Arrays of point.name and y + * @sample {highcharts} highcharts/series/data-array-of-objects/ Config objects + * @product highcharts + * @apioption series.bar.data + */ + +/** + * Alignment of the data label relative to the data point. + * + * @type {String} + * @sample {highcharts} highcharts/plotoptions/bar-datalabels-align-inside-bar/ + * Data labels inside the bar + * @default left + * @product highcharts + * @apioption plotOptions.bar.dataLabels.align + */ + +/** + * The x position of the data label relative to the data point. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/bar-datalabels-align-inside-bar/ + * Data labels inside the bar + * @default 5 + * @product highcharts + * @apioption plotOptions.bar.dataLabels.x + */ + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var Series = H.Series, + seriesType = H.seriesType; + +/** + * A scatter plot uses cartesian coordinates to display values for two variables + * for a set of data. + * + * @sample {highcharts} highcharts/demo/scatter/ Scatter plot + * + * @extends {plotOptions.line} + * @product highcharts highstock + * @optionparent plotOptions.scatter + */ +seriesType('scatter', 'line', { + + /** + * The width of the line connecting the data points. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/scatter-linewidth-none/ + * 0 by default + * @sample {highcharts} highcharts/plotoptions/scatter-linewidth-1/ + * 1px + * @default 0 + * @product highcharts highstock + */ + lineWidth: 0, + + findNearestPointBy: 'xy', + marker: { + enabled: true // Overrides auto-enabling in line series (#3647) + }, + + /** + * Sticky tracking of mouse events. When true, the `mouseOut` event + * on a series isn't triggered until the mouse moves over another series, + * or out of the plot area. When false, the `mouseOut` event on a series + * is triggered when the mouse leaves the area around the series' graph + * or markers. This also implies the tooltip. When `stickyTracking` + * is false and `tooltip.shared` is false, the tooltip will be hidden + * when moving the mouse between series. + * + * @type {Boolean} + * @default false + * @product highcharts highstock + * @apioption plotOptions.scatter.stickyTracking + */ + + /** + * A configuration object for the tooltip rendering of each single + * series. Properties are inherited from #tooltip. + * Overridable properties are `headerFormat`, `pointFormat`, `yDecimals`, + * `xDateFormat`, `yPrefix` and `ySuffix`. Unlike other series, in + * a scatter plot the series.name by default shows in the headerFormat + * and point.x and point.y in the pointFormat. + * + * @product highcharts highstock + */ + tooltip: { + + headerFormat: + '\u25CF ' + + ' {series.name}
', + + + pointFormat: 'x: {point.x}
y: {point.y}
' + } + +// Prototype members +}, { + sorted: false, + requireSorting: false, + noSharedTooltip: true, + trackerGroups: ['group', 'markerGroup', 'dataLabelsGroup'], + takeOrdinalPosition: false, // #2342 + drawGraph: function () { + if (this.options.lineWidth) { + Series.prototype.drawGraph.call(this); + } + } +}); + +/** + * A `scatter` series. If the [type](#series.scatter.type) option is + * not specified, it is inherited from [chart.type](#chart.type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * scatter](#plotOptions.scatter). + * + * @type {Object} + * @extends series,plotOptions.scatter + * @excluding dataParser,dataURL,stack + * @product highcharts highstock + * @apioption series.scatter + */ + +/** + * An array of data points for the series. For the `scatter` series + * type, points can be given in the following ways: + * + * 1. An array of numerical values. In this case, the numerical values + * will be interpreted as `y` options. The `x` values will be automatically + * calculated, either starting at 0 and incremented by 1, or from `pointStart` + * and `pointInterval` given in the series options. If the axis has + * categories, these will be used. Example: + * + * ```js + * data: [0, 5, 3, 5] + * ``` + * + * 2. An array of arrays with 2 values. In this case, the values correspond + * to `x,y`. If the first value is a string, it is applied as the name + * of the point, and the `x` value is inferred. + * + * ```js + * data: [ + * [0, 0], + * [1, 8], + * [2, 9] + * ] + * ``` + * + * 3. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.scatter.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * x: 1, + * y: 2, + * name: "Point2", + * color: "#00FF00" + * }, { + * x: 1, + * y: 4, + * name: "Point1", + * color: "#FF00FF" + * }] + * ``` + * + * @type {Array} + * @extends series.line.data + * @sample {highcharts} highcharts/chart/reflow-true/ + * Numerical values + * @sample {highcharts} highcharts/series/data-array-of-arrays/ + * Arrays of numeric x and y + * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/ + * Arrays of datetime x and y + * @sample {highcharts} highcharts/series/data-array-of-name-value/ + * Arrays of point.name and y + * @sample {highcharts} highcharts/series/data-array-of-objects/ + * Config objects + * @product highcharts highstock + * @apioption series.scatter.data + */ + + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var deg2rad = H.deg2rad, + isNumber = H.isNumber, + pick = H.pick, + relativeLength = H.relativeLength; +H.CenteredSeriesMixin = { + /** + * Get the center of the pie based on the size and center options relative to the + * plot area. Borrowed by the polar and gauge series types. + */ + getCenter: function () { + + var options = this.options, + chart = this.chart, + slicingRoom = 2 * (options.slicedOffset || 0), + handleSlicingRoom, + plotWidth = chart.plotWidth - 2 * slicingRoom, + plotHeight = chart.plotHeight - 2 * slicingRoom, + centerOption = options.center, + positions = [pick(centerOption[0], '50%'), pick(centerOption[1], '50%'), options.size || '100%', options.innerSize || 0], + smallestSize = Math.min(plotWidth, plotHeight), + i, + value; + + for (i = 0; i < 4; ++i) { + value = positions[i]; + handleSlicingRoom = i < 2 || (i === 2 && /%$/.test(value)); + + // i == 0: centerX, relative to width + // i == 1: centerY, relative to height + // i == 2: size, relative to smallestSize + // i == 3: innerSize, relative to size + positions[i] = relativeLength(value, [plotWidth, plotHeight, smallestSize, positions[2]][i]) + + (handleSlicingRoom ? slicingRoom : 0); + + } + // innerSize cannot be larger than size (#3632) + if (positions[3] > positions[2]) { + positions[3] = positions[2]; + } + return positions; + }, + /** + * getStartAndEndRadians - Calculates start and end angles in radians. + * Used in series types such as pie and sunburst. + * + * @param {Number} start Start angle in degrees. + * @param {Number} end Start angle in degrees. + * @return {object} Returns an object containing start and end angles as + * radians. + */ + getStartAndEndRadians: function getStartAndEndRadians(start, end) { + var startAngle = isNumber(start) ? start : 0, // must be a number + endAngle = ( + ( + isNumber(end) && // must be a number + end > startAngle && // must be larger than the start angle + // difference must be less than 360 degrees + (end - startAngle) < 360 + ) ? + end : + startAngle + 360 + ), + correction = -90; + return { + start: deg2rad * (startAngle + correction), + end: deg2rad * (endAngle + correction) + }; + } +}; + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var addEvent = H.addEvent, + CenteredSeriesMixin = H.CenteredSeriesMixin, + defined = H.defined, + each = H.each, + extend = H.extend, + getStartAndEndRadians = CenteredSeriesMixin.getStartAndEndRadians, + inArray = H.inArray, + LegendSymbolMixin = H.LegendSymbolMixin, + noop = H.noop, + pick = H.pick, + Point = H.Point, + Series = H.Series, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes, + setAnimation = H.setAnimation; + +/** + * The pie series type. + * + * @constructor seriesTypes.pie + * @augments Series + */ + +/** + * A pie chart is a circular graphic which is divided into slices to illustrate + * numerical proportion. + * + * @sample highcharts/demo/pie-basic/ Pie chart + * + * @extends {plotOptions.line} + * @excluding animationLimit,boostThreshold,connectEnds,connectNulls, + * cropThreshold,dashStyle,findNearestPointBy,getExtremesFromAll, + * lineWidth,marker,negativeColor,pointInterval,pointIntervalUnit, + * pointPlacement,pointStart,softThreshold,stacking,step,threshold, + * turboThreshold,zoneAxis,zones + * @product highcharts + * @optionparent plotOptions.pie + */ +seriesType('pie', 'line', { + + /** + * The center of the pie chart relative to the plot area. Can be percentages + * or pixel values. The default behaviour (as of 3.0) is to center + * the pie so that all slices and data labels are within the plot area. + * As a consequence, the pie may actually jump around in a chart with + * dynamic values, as the data labels move. In that case, the center + * should be explicitly set, for example to `["50%", "50%"]`. + * + * @type {Array} + * @sample {highcharts} highcharts/plotoptions/pie-center/ Centered at 100, 100 + * @default [null, null] + * @product highcharts + */ + center: [null, null], + + clip: false, + + /** @ignore */ + colorByPoint: true, // always true for pies + + /** + * A series specific or series type specific color set to use instead + * of the global [colors](#colors). + * + * @type {Array} + * @sample {highcharts} highcharts/demo/pie-monochrome/ Set default colors for all pies + * @since 3.0 + * @product highcharts + * @apioption plotOptions.pie.colors + */ + + /** + * @extends plotOptions.series.dataLabels + * @excluding align,allowOverlap,staggerLines,step + * @product highcharts + */ + dataLabels: { + /** + * The color of the line connecting the data label to the pie slice. + * The default color is the same as the point's color. + * + * In styled mode, the connector stroke is given in the + * `.highcharts-data-label-connector` class. + * + * @type {String} + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorcolor/ Blue connectors + * @sample {highcharts} highcharts/css/pie-point/ Styled connectors + * @default {point.color} + * @since 2.1 + * @product highcharts + * @apioption plotOptions.pie.dataLabels.connectorColor + */ + + /** + * The distance from the data label to the connector. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorpadding/ No padding + * @default 5 + * @since 2.1 + * @product highcharts + * @apioption plotOptions.pie.dataLabels.connectorPadding + */ + + /** + * The width of the line connecting the data label to the pie slice. + * + * + * In styled mode, the connector stroke width is given in the + * `.highcharts-data-label-connector` class. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorwidth-disabled/ Disable the connector + * @sample {highcharts} highcharts/css/pie-point/ Styled connectors + * @default 1 + * @since 2.1 + * @product highcharts + * @apioption plotOptions.pie.dataLabels.connectorWidth + */ + + /** + * The distance of the data label from the pie's edge. Negative numbers + * put the data label on top of the pie slices. Connectors are only + * shown for data labels outside the pie. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-distance/ Data labels on top of the pie + * @default 30 + * @since 2.1 + * @product highcharts + */ + distance: 30, + + /** + * Enable or disable the data labels. + * + * @type {Boolean} + * @since 2.1 + * @product highcharts + */ + enabled: true, + + formatter: function () { // #2945 + return this.point.isNull ? undefined : this.point.name; + }, + + /** + * Whether to render the connector as a soft arc or a line with sharp + * break. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-softconnector-true/ Soft + * @sample {highcharts} highcharts/plotoptions/pie-datalabels-softconnector-false/ Non soft + * @since 2.1.7 + * @product highcharts + * @apioption plotOptions.pie.dataLabels.softConnector + */ + + x: 0 + // y: 0 + }, + + /** + * The end angle of the pie in degrees where 0 is top and 90 is right. + * Defaults to `startAngle` plus 360. + * + * @type {Number} + * @sample {highcharts} highcharts/demo/pie-semi-circle/ Semi-circle donut + * @default null + * @since 1.3.6 + * @product highcharts + * @apioption plotOptions.pie.endAngle + */ + + /** + * Equivalent to [chart.ignoreHiddenSeries](#chart.ignoreHiddenSeries), + * this option tells whether the series shall be redrawn as if the + * hidden point were `null`. + * + * The default value changed from `false` to `true` with Highcharts + * 3.0. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/pie-ignorehiddenpoint/ True, the hiddden point is ignored + * @default true + * @since 2.3.0 + * @product highcharts + */ + ignoreHiddenPoint: true, + + /** + * The size of the inner diameter for the pie. A size greater than 0 + * renders a donut chart. Can be a percentage or pixel value. Percentages + * are relative to the pie size. Pixel values are given as integers. + * + * + * Note: in Highcharts < 4.1.2, the percentage was relative to the plot + * area, not the pie size. + * + * @type {String|Number} + * @sample {highcharts} highcharts/plotoptions/pie-innersize-80px/ 80px inner size + * @sample {highcharts} highcharts/plotoptions/pie-innersize-50percent/ 50% of the plot area + * @sample {highcharts} highcharts/demo/3d-pie-donut/ 3D donut + * @default 0 + * @since 2.0 + * @product highcharts + * @apioption plotOptions.pie.innerSize + */ + + /** @ignore */ + legendType: 'point', + + /** @ignore */ + marker: null, // point options are specified in the base options + + /** + * The minimum size for a pie in response to auto margins. The pie will + * try to shrink to make room for data labels in side the plot area, + * but only to this size. + * + * @type {Number} + * @default 80 + * @since 3.0 + * @product highcharts + * @apioption plotOptions.pie.minSize + */ + + /** + * The diameter of the pie relative to the plot area. Can be a percentage + * or pixel value. Pixel values are given as integers. The default + * behaviour (as of 3.0) is to scale to the plot area and give room + * for data labels within the plot area. As a consequence, the size + * of the pie may vary when points are updated and data labels more + * around. In that case it is best to set a fixed value, for example + * `"75%"`. + * + * @type {String|Number} + * @sample {highcharts} highcharts/plotoptions/pie-size/ Smaller pie + * @default + * @product highcharts + */ + size: null, + + /** + * Whether to display this particular series or series type in the + * legend. Since 2.1, pies are not shown in the legend by default. + * + * @type {Boolean} + * @sample {highcharts} highcharts/plotoptions/series-showinlegend/ One series in the legend, one hidden + * @product highcharts + */ + showInLegend: false, + + /** + * If a point is sliced, moved out from the center, how many pixels + * should it be moved?. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-slicedoffset-20/ 20px offset + * @default 10 + * @product highcharts + */ + slicedOffset: 10, + + /** + * The start angle of the pie slices in degrees where 0 is top and 90 + * right. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-startangle-90/ Start from right + * @default 0 + * @since 2.3.4 + * @product highcharts + * @apioption plotOptions.pie.startAngle + */ + + /** + * Sticky tracking of mouse events. When true, the `mouseOut` event + * on a series isn't triggered until the mouse moves over another series, + * or out of the plot area. When false, the `mouseOut` event on a + * series is triggered when the mouse leaves the area around the series' + * graph or markers. This also implies the tooltip. When `stickyTracking` + * is false and `tooltip.shared` is false, the tooltip will be hidden + * when moving the mouse between series. + * + * @product highcharts + */ + stickyTracking: false, + + tooltip: { + followPointer: true + }, + + + /** + * The color of the border surrounding each slice. When `null`, the + * border takes the same color as the slice fill. This can be used + * together with a `borderWidth` to fill drawing gaps created by antialiazing + * artefacts in borderless pies. + * + * In styled mode, the border stroke is given in the `.highcharts-point` class. + * + * @type {Color} + * @sample {highcharts} highcharts/plotoptions/pie-bordercolor-black/ Black border + * @default #ffffff + * @product highcharts + */ + borderColor: '#ffffff', + + /** + * The width of the border surrounding each slice. + * + * When setting the border width to 0, there may be small gaps between + * the slices due to SVG antialiasing artefacts. To work around this, + * keep the border width at 0.5 or 1, but set the `borderColor` to + * `null` instead. + * + * In styled mode, the border stroke width is given in the `.highcharts-point` class. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-borderwidth/ 3px border + * @default 1 + * @product highcharts + */ + borderWidth: 1, + + states: { + + /** + * @extends plotOptions.series.states.hover + * @product highcharts + */ + hover: { + + /** + * How much to brighten the point on interaction. Requires the main + * color to be defined in hex or rgb(a) format. + * + * In styled mode, the hover brightness is by default replaced + * by a fill-opacity given in the `.highcharts-point-hover` class. + * + * @type {Number} + * @sample {highcharts} highcharts/plotoptions/pie-states-hover-brightness/ Brightened by 0.5 + * @default 0.1 + * @product highcharts + */ + brightness: 0.1, + + shadow: false + } + } + + +}, /** @lends seriesTypes.pie.prototype */ { + isCartesian: false, + requireSorting: false, + directTouch: true, + noSharedTooltip: true, + trackerGroups: ['group', 'dataLabelsGroup'], + axisTypes: [], + pointAttribs: seriesTypes.column.prototype.pointAttribs, + /** + * Animate the pies in + */ + animate: function (init) { + var series = this, + points = series.points, + startAngleRad = series.startAngleRad; + + if (!init) { + each(points, function (point) { + var graphic = point.graphic, + args = point.shapeArgs; + + if (graphic) { + // start values + graphic.attr({ + r: point.startR || (series.center[3] / 2), // animate from inner radius (#779) + start: startAngleRad, + end: startAngleRad + }); + + // animate + graphic.animate({ + r: args.r, + start: args.start, + end: args.end + }, series.options.animation); + } + }); + + // delete this function to allow it only once + series.animate = null; + } + }, + + /** + * Recompute total chart sum and update percentages of points. + */ + updateTotals: function () { + var i, + total = 0, + points = this.points, + len = points.length, + point, + ignoreHiddenPoint = this.options.ignoreHiddenPoint; + + // Get the total sum + for (i = 0; i < len; i++) { + point = points[i]; + total += (ignoreHiddenPoint && !point.visible) ? + 0 : + point.isNull ? 0 : point.y; + } + this.total = total; + + // Set each point's properties + for (i = 0; i < len; i++) { + point = points[i]; + point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0; + point.total = total; + } + }, + + /** + * Extend the generatePoints method by adding total and percentage properties to each point + */ + generatePoints: function () { + Series.prototype.generatePoints.call(this); + this.updateTotals(); + }, + + /** + * Do translation for pie slices + */ + translate: function (positions) { + this.generatePoints(); + + var series = this, + cumulative = 0, + precision = 1000, // issue #172 + options = series.options, + slicedOffset = options.slicedOffset, + connectorOffset = slicedOffset + (options.borderWidth || 0), + finalConnectorOffset, + start, + end, + angle, + radians = getStartAndEndRadians(options.startAngle, options.endAngle), + startAngleRad = series.startAngleRad = radians.start, + endAngleRad = series.endAngleRad = radians.end, + circ = endAngleRad - startAngleRad, // 2 * Math.PI, + points = series.points, + radiusX, // the x component of the radius vector for a given point + radiusY, + labelDistance = options.dataLabels.distance, + ignoreHiddenPoint = options.ignoreHiddenPoint, + i, + len = points.length, + point; + + // Get positions - either an integer or a percentage string must be given. + // If positions are passed as a parameter, we're in a recursive loop for adjusting + // space for data labels. + if (!positions) { + series.center = positions = series.getCenter(); + } + + // Utility for getting the x value from a given y, used for anticollision + // logic in data labels. + // Added point for using specific points' label distance. + series.getX = function (y, left, point) { + angle = Math.asin(Math.min((y - positions[1]) / (positions[2] / 2 + point.labelDistance), 1)); + return positions[0] + + (left ? -1 : 1) * + (Math.cos(angle) * (positions[2] / 2 + point.labelDistance)); + }; + + // Calculate the geometry for each point + for (i = 0; i < len; i++) { + + point = points[i]; + + // Used for distance calculation for specific point. + point.labelDistance = pick( + point.options.dataLabels && point.options.dataLabels.distance, + labelDistance + ); + + // Saved for later dataLabels distance calculation. + series.maxLabelDistance = Math.max(series.maxLabelDistance || 0, point.labelDistance); + + // set start and end angle + start = startAngleRad + (cumulative * circ); + if (!ignoreHiddenPoint || point.visible) { + cumulative += point.percentage / 100; + } + end = startAngleRad + (cumulative * circ); + + // set the shape + point.shapeType = 'arc'; + point.shapeArgs = { + x: positions[0], + y: positions[1], + r: positions[2] / 2, + innerR: positions[3] / 2, + start: Math.round(start * precision) / precision, + end: Math.round(end * precision) / precision + }; + + // The angle must stay within -90 and 270 (#2645) + angle = (end + start) / 2; + if (angle > 1.5 * Math.PI) { + angle -= 2 * Math.PI; + } else if (angle < -Math.PI / 2) { + angle += 2 * Math.PI; + } + + // Center for the sliced out slice + point.slicedTranslation = { + translateX: Math.round(Math.cos(angle) * slicedOffset), + translateY: Math.round(Math.sin(angle) * slicedOffset) + }; + + // set the anchor point for tooltips + radiusX = Math.cos(angle) * positions[2] / 2; + radiusY = Math.sin(angle) * positions[2] / 2; + point.tooltipPos = [ + positions[0] + radiusX * 0.7, + positions[1] + radiusY * 0.7 + ]; + + point.half = angle < -Math.PI / 2 || angle > Math.PI / 2 ? 1 : 0; + point.angle = angle; + + // Set the anchor point for data labels. Use point.labelDistance + // instead of labelDistance // #1174 + // finalConnectorOffset - not override connectorOffset value. + finalConnectorOffset = Math.min(connectorOffset, point.labelDistance / 5); // #1678 + point.labelPos = [ + positions[0] + radiusX + Math.cos(angle) * point.labelDistance, // first break of connector + positions[1] + radiusY + Math.sin(angle) * point.labelDistance, // a/a + positions[0] + radiusX + Math.cos(angle) * finalConnectorOffset, // second break, right outside pie + positions[1] + radiusY + Math.sin(angle) * finalConnectorOffset, // a/a + positions[0] + radiusX, // landing point for connector + positions[1] + radiusY, // a/a + point.labelDistance < 0 ? // alignment + 'center' : + point.half ? 'right' : 'left', // alignment + angle // center angle + ]; + + } + }, + + drawGraph: null, + + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, + chart = series.chart, + renderer = chart.renderer, + groupTranslation, + graphic, + pointAttr, + shapeArgs; + + + var shadow = series.options.shadow; + if (shadow && !series.shadowGroup) { + series.shadowGroup = renderer.g('shadow') + .add(series.group); + } + + + // draw the slices + each(series.points, function (point) { + graphic = point.graphic; + if (!point.isNull) { + shapeArgs = point.shapeArgs; + + + // If the point is sliced, use special translation, else use + // plot area traslation + groupTranslation = point.getTranslate(); + + + // Put the shadow behind all points + var shadowGroup = point.shadowGroup; + if (shadow && !shadowGroup) { + shadowGroup = point.shadowGroup = renderer.g('shadow') + .add(series.shadowGroup); + } + + if (shadowGroup) { + shadowGroup.attr(groupTranslation); + } + pointAttr = series.pointAttribs(point, point.selected && 'select'); + + + // Draw the slice + if (graphic) { + graphic + .setRadialReference(series.center) + + .attr(pointAttr) + + .animate(extend(shapeArgs, groupTranslation)); + } else { + + point.graphic = graphic = renderer[point.shapeType](shapeArgs) + .setRadialReference(series.center) + .attr(groupTranslation) + .add(series.group); + + if (!point.visible) { + graphic.attr({ visibility: 'hidden' }); + } + + + graphic + .attr(pointAttr) + .attr({ 'stroke-linejoin': 'round' }) + .shadow(shadow, shadowGroup); + + } + + graphic.addClass(point.getClassName()); + + } else if (graphic) { + point.graphic = graphic.destroy(); + } + }); + + }, + + + searchPoint: noop, + + /** + * Utility for sorting data labels + */ + sortByAngle: function (points, sign) { + points.sort(function (a, b) { + return a.angle !== undefined && (b.angle - a.angle) * sign; + }); + }, + + /** + * Use a simple symbol from LegendSymbolMixin + */ + drawLegendSymbol: LegendSymbolMixin.drawRectangle, + + /** + * Use the getCenter method from drawLegendSymbol + */ + getCenter: CenteredSeriesMixin.getCenter, + + /** + * Pies don't have point marker symbols + */ + getSymbol: noop + + +/** + * @constructor seriesTypes.pie.prototype.pointClass + * @extends {Point} + */ +}, /** @lends seriesTypes.pie.prototype.pointClass.prototype */ { + /** + * Initiate the pie slice + */ + init: function () { + + Point.prototype.init.apply(this, arguments); + + var point = this, + toggleSlice; + + point.name = pick(point.name, 'Slice'); + + // add event listener for select + toggleSlice = function (e) { + point.slice(e.type === 'select'); + }; + addEvent(point, 'select', toggleSlice); + addEvent(point, 'unselect', toggleSlice); + + return point; + }, + + /** + * Negative points are not valid (#1530, #3623, #5322) + */ + isValid: function () { + return H.isNumber(this.y, true) && this.y >= 0; + }, + + /** + * Toggle the visibility of the pie slice + * @param {Boolean} vis Whether to show the slice or not. If undefined, the + * visibility is toggled + */ + setVisible: function (vis, redraw) { + var point = this, + series = point.series, + chart = series.chart, + ignoreHiddenPoint = series.options.ignoreHiddenPoint; + + redraw = pick(redraw, ignoreHiddenPoint); + + if (vis !== point.visible) { + + // If called without an argument, toggle visibility + point.visible = point.options.visible = vis = vis === undefined ? !point.visible : vis; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + + // Show and hide associated elements. This is performed regardless of redraw or not, + // because chart.redraw only handles full series. + each(['graphic', 'dataLabel', 'connector', 'shadowGroup'], function (key) { + if (point[key]) { + point[key][vis ? 'show' : 'hide'](true); + } + }); + + if (point.legendItem) { + chart.legend.colorizeItem(point, vis); + } + + // #4170, hide halo after hiding point + if (!vis && point.state === 'hover') { + point.setState(''); + } + + // Handle ignore hidden slices + if (ignoreHiddenPoint) { + series.isDirty = true; + } + + if (redraw) { + chart.redraw(); + } + } + }, + + /** + * Set or toggle whether the slice is cut out from the pie + * @param {Boolean} sliced When undefined, the slice state is toggled + * @param {Boolean} redraw Whether to redraw the chart. True by default. + */ + slice: function (sliced, redraw, animation) { + var point = this, + series = point.series, + chart = series.chart; + + setAnimation(animation, chart); + + // redraw is true by default + redraw = pick(redraw, true); + + // if called without an argument, toggle + point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced; + series.options.data[inArray(point, series.data)] = point.options; // update userOptions.data + + point.graphic.animate(this.getTranslate()); + + + if (point.shadowGroup) { + point.shadowGroup.animate(this.getTranslate()); + } + + }, + + getTranslate: function () { + return this.sliced ? this.slicedTranslation : { + translateX: 0, + translateY: 0 + }; + }, + + haloPath: function (size) { + var shapeArgs = this.shapeArgs; + + return this.sliced || !this.visible ? + [] : + this.series.chart.renderer.symbols.arc( + shapeArgs.x, + shapeArgs.y, + shapeArgs.r + size, + shapeArgs.r + size, { + // Substract 1px to ensure the background is not bleeding + // through between the halo and the slice (#7495). + innerR: this.shapeArgs.r - 1, + start: shapeArgs.start, + end: shapeArgs.end + } + ); + } +}); + +/** + * A `pie` series. If the [type](#series.pie.type) option is not specified, + * it is inherited from [chart.type](#chart.type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * pie](#plotOptions.pie). + * + * @type {Object} + * @extends series,plotOptions.pie + * @excluding dataParser,dataURL,stack,xAxis,yAxis + * @product highcharts + * @apioption series.pie + */ + +/** + * An array of data points for the series. For the `pie` series type, + * points can be given in the following ways: + * + * 1. An array of numerical values. In this case, the numerical values + * will be interpreted as `y` options. Example: + * + * ```js + * data: [0, 5, 3, 5] + * ``` + * + * 2. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.pie.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * y: 1, + * name: "Point2", + * color: "#00FF00" + * }, { + * y: 7, + * name: "Point1", + * color: "#FF00FF" + * }] + * + * @type {Array} + * @extends series.line.data + * @excluding marker,x + * @sample {highcharts} highcharts/chart/reflow-true/ Numerical values + * @sample {highcharts} highcharts/series/data-array-of-arrays/ Arrays of numeric x and y + * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/ Arrays of datetime x and y + * @sample {highcharts} highcharts/series/data-array-of-name-value/ Arrays of point.name and y + * @sample {highcharts} highcharts/series/data-array-of-objects/ Config objects + * @product highcharts + * @apioption series.pie.data + */ + +/** + * The sequential index of the data point in the legend. + * + * @type {Number} + * @product highcharts + * @apioption series.pie.data.legendIndex + */ + +/** + * Whether to display a slice offset from the center. + * + * @type {Boolean} + * @sample {highcharts} highcharts/point/sliced/ One sliced point + * @product highcharts + * @apioption series.pie.data.sliced + */ + +/** + * Fires when the checkbox next to the point name in the legend is clicked. + * One parameter, event, is passed to the function. The state of the + * checkbox is found by event.checked. The checked item is found by + * event.item. Return false to prevent the default action which is to + * toggle the select state of the series. + * + * @type {Function} + * @context Point + * @sample {highcharts} highcharts/plotoptions/series-events-checkboxclick/ + * Alert checkbox status + * @since 1.2.0 + * @product highcharts + * @apioption plotOptions.pie.events.checkboxClick + */ + +/** + * Not applicable to pies, as the legend item is per point. See point. + * events. + * + * @type {Function} + * @since 1.2.0 + * @product highcharts + * @apioption plotOptions.pie.events.legendItemClick + */ + +/** + * Fires when the legend item belonging to the pie point (slice) is + * clicked. The `this` keyword refers to the point itself. One parameter, + * `event`, is passed to the function, containing common event information. The + * default action is to toggle the visibility of the point. This can be + * prevented by calling `event.preventDefault()`. + * + * @type {Function} + * @sample {highcharts} highcharts/plotoptions/pie-point-events-legenditemclick/ + * Confirm toggle visibility + * @since 1.2.0 + * @product highcharts + * @apioption plotOptions.pie.point.events.legendItemClick + */ + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var addEvent = H.addEvent, + arrayMax = H.arrayMax, + defined = H.defined, + each = H.each, + extend = H.extend, + format = H.format, + map = H.map, + merge = H.merge, + noop = H.noop, + pick = H.pick, + relativeLength = H.relativeLength, + Series = H.Series, + seriesTypes = H.seriesTypes, + stableSort = H.stableSort; + +/* eslint max-len: ["warn", 80, 4] */ +/** + * General distribution algorithm for distributing labels of differing size + * along a confined length in two dimensions. The algorithm takes an array of + * objects containing a size, a target and a rank. It will place the labels as + * close as possible to their targets, skipping the lowest ranked labels if + * necessary. + */ +H.distribute = function (boxes, len) { + + var i, + overlapping = true, + origBoxes = boxes, // Original array will be altered with added .pos + restBoxes = [], // The outranked overshoot + box, + target, + total = 0; + + function sortByTarget(a, b) { + return a.target - b.target; + } + + // If the total size exceeds the len, remove those boxes with the lowest + // rank + i = boxes.length; + while (i--) { + total += boxes[i].size; + } + + // Sort by rank, then slice away overshoot + if (total > len) { + stableSort(boxes, function (a, b) { + return (b.rank || 0) - (a.rank || 0); + }); + i = 0; + total = 0; + while (total <= len) { + total += boxes[i].size; + i++; + } + restBoxes = boxes.splice(i - 1, boxes.length); + } + + // Order by target + stableSort(boxes, sortByTarget); + + + // So far we have been mutating the original array. Now + // create a copy with target arrays + boxes = map(boxes, function (box) { + return { + size: box.size, + targets: [box.target], + align: pick(box.align, 0.5) + }; + }); + + while (overlapping) { + // Initial positions: target centered in box + i = boxes.length; + while (i--) { + box = boxes[i]; + // Composite box, average of targets + target = ( + Math.min.apply(0, box.targets) + + Math.max.apply(0, box.targets) + ) / 2; + box.pos = Math.min( + Math.max(0, target - box.size * box.align), + len - box.size + ); + } + + // Detect overlap and join boxes + i = boxes.length; + overlapping = false; + while (i--) { + // Overlap + if (i > 0 && boxes[i - 1].pos + boxes[i - 1].size > boxes[i].pos) { + // Add this size to the previous box + boxes[i - 1].size += boxes[i].size; + boxes[i - 1].targets = boxes[i - 1] + .targets + .concat(boxes[i].targets); + boxes[i - 1].align = 0.5; + + // Overlapping right, push left + if (boxes[i - 1].pos + boxes[i - 1].size > len) { + boxes[i - 1].pos = len - boxes[i - 1].size; + } + boxes.splice(i, 1); // Remove this item + overlapping = true; + } + } + } + + // Now the composite boxes are placed, we need to put the original boxes + // within them + i = 0; + each(boxes, function (box) { + var posInCompositeBox = 0; + each(box.targets, function () { + origBoxes[i].pos = box.pos + posInCompositeBox; + posInCompositeBox += origBoxes[i].size; + i++; + }); + }); + + // Add the rest (hidden) boxes and sort by target + origBoxes.push.apply(origBoxes, restBoxes); + stableSort(origBoxes, sortByTarget); +}; + + +/** + * Draw the data labels + */ +Series.prototype.drawDataLabels = function () { + var series = this, + chart = series.chart, + seriesOptions = series.options, + options = seriesOptions.dataLabels, + points = series.points, + pointOptions, + generalOptions, + hasRendered = series.hasRendered || 0, + str, + dataLabelsGroup, + defer = pick(options.defer, !!seriesOptions.animation), + renderer = chart.renderer; + + /* + * Handle the dataLabels.filter option. + */ + function applyFilter(point, options) { + var filter = options.filter, + op, + prop, + val; + if (filter) { + op = filter.operator; + prop = point[filter.property]; + val = filter.value; + if ( + (op === '>' && prop > val) || + (op === '<' && prop < val) || + (op === '>=' && prop >= val) || + (op === '<=' && prop <= val) || + (op === '==' && prop == val) || // eslint-disable-line eqeqeq + (op === '===' && prop === val) + ) { + return true; + } + return false; + } + return true; + } + + if (options.enabled || series._hasPointLabels) { + + // Process default alignment of data labels for columns + if (series.dlProcessOptions) { + series.dlProcessOptions(options); + } + + // Create a separate group for the data labels to avoid rotation + dataLabelsGroup = series.plotGroup( + 'dataLabelsGroup', + 'data-labels', + defer && !hasRendered ? 'hidden' : 'visible', // #5133 + options.zIndex || 6 + ); + + if (defer) { + dataLabelsGroup.attr({ opacity: +hasRendered }); // #3300 + if (!hasRendered) { + addEvent(series, 'afterAnimate', function () { + if (series.visible) { // #2597, #3023, #3024 + dataLabelsGroup.show(true); + } + dataLabelsGroup[ + seriesOptions.animation ? 'animate' : 'attr' + ]({ opacity: 1 }, { duration: 200 }); + }); + } + } + + // Make the labels for each point + generalOptions = options; + each(points, function (point) { + var enabled, + dataLabel = point.dataLabel, + labelConfig, + attr, + rotation, + connector = point.connector, + isNew = !dataLabel, + style, + formatString; + + // Determine if each data label is enabled + // @note dataLabelAttribs (like pointAttribs) would eradicate + // the need for dlOptions, and simplify the section below. + pointOptions = point.dlOptions || // dlOptions is used in treemaps + (point.options && point.options.dataLabels); + enabled = pick( + pointOptions && pointOptions.enabled, + generalOptions.enabled + ) && !point.isNull; // #2282, #4641, #7112 + + if (enabled) { + enabled = applyFilter(point, pointOptions || options) === true; + } + + if (enabled) { + // Create individual options structure that can be extended + // without affecting others + options = merge(generalOptions, pointOptions); + labelConfig = point.getLabelConfig(); + formatString = ( + options[point.formatPrefix + 'Format'] || + options.format + ); + + str = defined(formatString) ? + format(formatString, labelConfig, chart.time) : + ( + options[point.formatPrefix + 'Formatter'] || + options.formatter + ).call(labelConfig, options); + + style = options.style; + rotation = options.rotation; + + // Determine the color + style.color = pick( + options.color, + style.color, + series.color, + '#000000' + ); + // Get automated contrast color + if (style.color === 'contrast') { + point.contrastColor = + renderer.getContrast(point.color || series.color); + style.color = options.inside || + pick(point.labelDistance, options.distance) < 0 || + !!seriesOptions.stacking ? + point.contrastColor : + '#000000'; + } + if (seriesOptions.cursor) { + style.cursor = seriesOptions.cursor; + } + + + attr = { + + fill: options.backgroundColor, + stroke: options.borderColor, + 'stroke-width': options.borderWidth, + + r: options.borderRadius || 0, + rotation: rotation, + padding: options.padding, + zIndex: 1 + }; + + // Remove unused attributes (#947) + H.objectEach(attr, function (val, name) { + if (val === undefined) { + delete attr[name]; + } + }); + } + // If the point is outside the plot area, destroy it. #678, #820 + if (dataLabel && (!enabled || !defined(str))) { + point.dataLabel = dataLabel = dataLabel.destroy(); + if (connector) { + point.connector = connector.destroy(); + } + // Individual labels are disabled if the are explicitly disabled + // in the point options, or if they fall outside the plot area. + } else if (enabled && defined(str)) { + // create new label + if (!dataLabel) { + dataLabel = point.dataLabel = rotation ? + + renderer.text(str, 0, -9999) // labels don't rotate + .addClass('highcharts-data-label') : + + renderer.label( + str, + 0, + -9999, + options.shape, + null, + null, + options.useHTML, + null, + 'data-label' + ); + + dataLabel.addClass( + ' highcharts-data-label-color-' + point.colorIndex + + ' ' + (options.className || '') + + (options.useHTML ? 'highcharts-tracker' : '') // #3398 + ); + } else { + attr.text = str; + } + dataLabel.attr(attr); + + // Styles must be applied before add in order to read text + // bounding box + dataLabel.css(style).shadow(options.shadow); + + + if (!dataLabel.added) { + dataLabel.add(dataLabelsGroup); + } + // Now the data label is created and placed at 0,0, so we need + // to align it + series.alignDataLabel(point, dataLabel, options, null, isNew); + } + }); + } +}; + +/** + * Align each individual data label + */ +Series.prototype.alignDataLabel = function ( + point, + dataLabel, + options, + alignTo, + isNew +) { + var chart = this.chart, + inverted = chart.inverted, + plotX = pick(point.dlBox && point.dlBox.centerX, point.plotX, -9999), + plotY = pick(point.plotY, -9999), + bBox = dataLabel.getBBox(), + fontSize, + baseline, + rotation = options.rotation, + normRotation, + negRotation, + align = options.align, + rotCorr, // rotation correction + // Math.round for rounding errors (#2683), alignTo to allow column + // labels (#2700) + visible = + this.visible && + ( + point.series.forceDL || + chart.isInsidePlot(plotX, Math.round(plotY), inverted) || + ( + alignTo && chart.isInsidePlot( + plotX, + inverted ? + alignTo.x + 1 : + alignTo.y + alignTo.height - 1, + inverted + ) + ) + ), + alignAttr, // the final position; + justify = pick(options.overflow, 'justify') === 'justify'; + + if (visible) { + + + fontSize = options.style.fontSize; + + + baseline = chart.renderer.fontMetrics(fontSize, dataLabel).b; + + // The alignment box is a singular point + alignTo = extend({ + x: inverted ? this.yAxis.len - plotY : plotX, + y: Math.round(inverted ? this.xAxis.len - plotX : plotY), + width: 0, + height: 0 + }, alignTo); + + // Add the text size for alignment calculation + extend(options, { + width: bBox.width, + height: bBox.height + }); + + // Allow a hook for changing alignment in the last moment, then do the + // alignment + if (rotation) { + justify = false; // Not supported for rotated text + rotCorr = chart.renderer.rotCorr(baseline, rotation); // #3723 + alignAttr = { + x: alignTo.x + options.x + alignTo.width / 2 + rotCorr.x, + y: ( + alignTo.y + + options.y + + { top: 0, middle: 0.5, bottom: 1 }[options.verticalAlign] * + alignTo.height + ) + }; + dataLabel[isNew ? 'attr' : 'animate'](alignAttr) + .attr({ // #3003 + align: align + }); + + // Compensate for the rotated label sticking out on the sides + normRotation = (rotation + 720) % 360; + negRotation = normRotation > 180 && normRotation < 360; + + if (align === 'left') { + alignAttr.y -= negRotation ? bBox.height : 0; + } else if (align === 'center') { + alignAttr.x -= bBox.width / 2; + alignAttr.y -= bBox.height / 2; + } else if (align === 'right') { + alignAttr.x -= bBox.width; + alignAttr.y -= negRotation ? 0 : bBox.height; + } + + + } else { + dataLabel.align(options, null, alignTo); + alignAttr = dataLabel.alignAttr; + } + + // Handle justify or crop + if (justify) { + point.isLabelJustified = this.justifyDataLabel( + dataLabel, + options, + alignAttr, + bBox, + alignTo, + isNew + ); + + // Now check that the data label is within the plot area + } else if (pick(options.crop, true)) { + visible = + chart.isInsidePlot( + alignAttr.x, + alignAttr.y + ) && + chart.isInsidePlot( + alignAttr.x + bBox.width, + alignAttr.y + bBox.height + ); + } + + // When we're using a shape, make it possible with a connector or an + // arrow pointing to thie point + if (options.shape && !rotation) { + dataLabel[isNew ? 'attr' : 'animate']({ + anchorX: inverted ? chart.plotWidth - point.plotY : point.plotX, + anchorY: inverted ? chart.plotHeight - point.plotX : point.plotY + }); + } + } + + // Show or hide based on the final aligned position + if (!visible) { + dataLabel.attr({ y: -9999 }); + dataLabel.placed = false; // don't animate back in + } + +}; + +/** + * If data labels fall partly outside the plot area, align them back in, in a + * way that doesn't hide the point. + */ +Series.prototype.justifyDataLabel = function ( + dataLabel, + options, + alignAttr, + bBox, + alignTo, + isNew +) { + var chart = this.chart, + align = options.align, + verticalAlign = options.verticalAlign, + off, + justified, + padding = dataLabel.box ? 0 : (dataLabel.padding || 0); + + // Off left + off = alignAttr.x + padding; + if (off < 0) { + if (align === 'right') { + options.align = 'left'; + } else { + options.x = -off; + } + justified = true; + } + + // Off right + off = alignAttr.x + bBox.width - padding; + if (off > chart.plotWidth) { + if (align === 'left') { + options.align = 'right'; + } else { + options.x = chart.plotWidth - off; + } + justified = true; + } + + // Off top + off = alignAttr.y + padding; + if (off < 0) { + if (verticalAlign === 'bottom') { + options.verticalAlign = 'top'; + } else { + options.y = -off; + } + justified = true; + } + + // Off bottom + off = alignAttr.y + bBox.height - padding; + if (off > chart.plotHeight) { + if (verticalAlign === 'top') { + options.verticalAlign = 'bottom'; + } else { + options.y = chart.plotHeight - off; + } + justified = true; + } + + if (justified) { + dataLabel.placed = !isNew; + dataLabel.align(options, null, alignTo); + } + + return justified; +}; + +/** + * Override the base drawDataLabels method by pie specific functionality + */ +if (seriesTypes.pie) { + seriesTypes.pie.prototype.drawDataLabels = function () { + var series = this, + data = series.data, + point, + chart = series.chart, + options = series.options.dataLabels, + connectorPadding = pick(options.connectorPadding, 10), + connectorWidth = pick(options.connectorWidth, 1), + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + connector, + seriesCenter = series.center, + radius = seriesCenter[2] / 2, + centerY = seriesCenter[1], + dataLabel, + dataLabelWidth, + labelPos, + labelHeight, + // divide the points into right and left halves for anti collision + halves = [ + [], // right + [] // left + ], + x, + y, + visibility, + j, + overflow = [0, 0, 0, 0]; // top, right, bottom, left + + // get out if not enabled + if (!series.visible || (!options.enabled && !series._hasPointLabels)) { + return; + } + + // Reset all labels that have been shortened + each(data, function (point) { + if (point.dataLabel && point.visible && point.dataLabel.shortened) { + point.dataLabel + .attr({ + width: 'auto' + }).css({ + width: 'auto', + textOverflow: 'clip' + }); + point.dataLabel.shortened = false; + } + }); + + + // run parent method + Series.prototype.drawDataLabels.apply(series); + + each(data, function (point) { + if (point.dataLabel && point.visible) { // #407, #2510 + + // Arrange points for detection collision + halves[point.half].push(point); + + // Reset positions (#4905) + point.dataLabel._pos = null; + } + }); + + /* Loop over the points in each half, starting from the top and bottom + * of the pie to detect overlapping labels. + */ + each(halves, function (points, i) { + + var top, + bottom, + length = points.length, + positions = [], + naturalY, + sideOverflow, + positionsIndex, // Point index in positions array. + size; + + if (!length) { + return; + } + + // Sort by angle + series.sortByAngle(points, i - 0.5); + // Only do anti-collision when we have dataLabels outside the pie + // and have connectors. (#856) + if (series.maxLabelDistance > 0) { + top = Math.max( + 0, + centerY - radius - series.maxLabelDistance + ); + bottom = Math.min( + centerY + radius + series.maxLabelDistance, + chart.plotHeight + ); + each(points, function (point) { + // check if specific points' label is outside the pie + if (point.labelDistance > 0 && point.dataLabel) { + // point.top depends on point.labelDistance value + // Used for calculation of y value in getX method + point.top = Math.max( + 0, + centerY - radius - point.labelDistance + ); + point.bottom = Math.min( + centerY + radius + point.labelDistance, + chart.plotHeight + ); + size = point.dataLabel.getBBox().height || 21; + + // point.positionsIndex is needed for getting index of + // parameter related to specific point inside positions + // array - not every point is in positions array. + point.positionsIndex = positions.push({ + target: point.labelPos[1] - point.top + size / 2, + size: size, + rank: point.y + }) - 1; + } + }); + H.distribute(positions, bottom + size - top); + } + + // Now the used slots are sorted, fill them up sequentially + for (j = 0; j < length; j++) { + + point = points[j]; + positionsIndex = point.positionsIndex; + labelPos = point.labelPos; + dataLabel = point.dataLabel; + visibility = point.visible === false ? 'hidden' : 'inherit'; + naturalY = labelPos[1]; + y = naturalY; + + if (positions && defined(positions[positionsIndex])) { + if (positions[positionsIndex].pos === undefined) { + visibility = 'hidden'; + } else { + labelHeight = positions[positionsIndex].size; + y = point.top + positions[positionsIndex].pos; + } + } + + // It is needed to delete point.positionIndex for + // dynamically added points etc. + + delete point.positionIndex; + + // get the x - use the natural x position for labels near the + // top and bottom, to prevent the top and botton slice + // connectors from touching each other on either side + if (options.justify) { + x = seriesCenter[0] + + (i ? -1 : 1) * (radius + point.labelDistance); + } else { + x = series.getX( + y < point.top + 2 || y > point.bottom - 2 ? + naturalY : + y, + i, + point + ); + } + + + // Record the placement and visibility + dataLabel._attr = { + visibility: visibility, + align: labelPos[6] + }; + dataLabel._pos = { + x: ( + x + + options.x + + ({ + left: connectorPadding, + right: -connectorPadding + }[labelPos[6]] || 0) + ), + + // 10 is for the baseline (label vs text) + y: y + options.y - 10 + }; + labelPos.x = x; + labelPos.y = y; + + + // Detect overflowing data labels + if (pick(options.crop, true)) { + dataLabelWidth = dataLabel.getBBox().width; + + sideOverflow = null; + // Overflow left + if (x - dataLabelWidth < connectorPadding) { + sideOverflow = Math.round( + dataLabelWidth - x + connectorPadding + ); + overflow[3] = Math.max(sideOverflow, overflow[3]); + + // Overflow right + } else if ( + x + dataLabelWidth > + plotWidth - connectorPadding + ) { + sideOverflow = Math.round( + x + dataLabelWidth - plotWidth + connectorPadding + ); + overflow[1] = Math.max(sideOverflow, overflow[1]); + } + + // Overflow top + if (y - labelHeight / 2 < 0) { + overflow[0] = Math.max( + Math.round(-y + labelHeight / 2), + overflow[0] + ); + + // Overflow left + } else if (y + labelHeight / 2 > plotHeight) { + overflow[2] = Math.max( + Math.round(y + labelHeight / 2 - plotHeight), + overflow[2] + ); + } + dataLabel.sideOverflow = sideOverflow; + } + } // for each point + }); // for each half + + // Do not apply the final placement and draw the connectors until we + // have verified that labels are not spilling over. + if ( + arrayMax(overflow) === 0 || + this.verifyDataLabelOverflow(overflow) + ) { + + // Place the labels in the final position + this.placeDataLabels(); + + // Draw the connectors + if (connectorWidth) { + each(this.points, function (point) { + var isNew; + + connector = point.connector; + dataLabel = point.dataLabel; + + if ( + dataLabel && + dataLabel._pos && + point.visible && + point.labelDistance > 0 + ) { + visibility = dataLabel._attr.visibility; + + isNew = !connector; + + if (isNew) { + point.connector = connector = chart.renderer.path() + .addClass('highcharts-data-label-connector ' + + ' highcharts-color-' + point.colorIndex) + .add(series.dataLabelsGroup); + + + connector.attr({ + 'stroke-width': connectorWidth, + 'stroke': ( + options.connectorColor || + point.color || + '#666666' + ) + }); + + } + connector[isNew ? 'attr' : 'animate']({ + d: series.connectorPath(point.labelPos) + }); + connector.attr('visibility', visibility); + + } else if (connector) { + point.connector = connector.destroy(); + } + }); + } + } + }; + + /** + * Extendable method for getting the path of the connector between the data + * label and the pie slice. + */ + seriesTypes.pie.prototype.connectorPath = function (labelPos) { + var x = labelPos.x, + y = labelPos.y; + return pick(this.options.dataLabels.softConnector, true) ? [ + 'M', + // end of the string at the label + x + (labelPos[6] === 'left' ? 5 : -5), y, + 'C', + x, y, // first break, next to the label + 2 * labelPos[2] - labelPos[4], 2 * labelPos[3] - labelPos[5], + labelPos[2], labelPos[3], // second break + 'L', + labelPos[4], labelPos[5] // base + ] : [ + 'M', + // end of the string at the label + x + (labelPos[6] === 'left' ? 5 : -5), y, + 'L', + labelPos[2], labelPos[3], // second break + 'L', + labelPos[4], labelPos[5] // base + ]; + }; + + /** + * Perform the final placement of the data labels after we have verified + * that they fall within the plot area. + */ + seriesTypes.pie.prototype.placeDataLabels = function () { + each(this.points, function (point) { + var dataLabel = point.dataLabel, + _pos; + if (dataLabel && point.visible) { + _pos = dataLabel._pos; + if (_pos) { + + // Shorten data labels with ellipsis if they still overflow + // after the pie has reached minSize (#223). + if (dataLabel.sideOverflow) { + dataLabel._attr.width = + dataLabel.getBBox().width - dataLabel.sideOverflow; + dataLabel.css({ + width: dataLabel._attr.width + 'px', + textOverflow: 'ellipsis' + }); + dataLabel.shortened = true; + } + + dataLabel.attr(dataLabel._attr); + dataLabel[dataLabel.moved ? 'animate' : 'attr'](_pos); + dataLabel.moved = true; + } else if (dataLabel) { + dataLabel.attr({ y: -9999 }); + } + } + }, this); + }; + + seriesTypes.pie.prototype.alignDataLabel = noop; + + /** + * Verify whether the data labels are allowed to draw, or we should run more + * translation and data label positioning to keep them inside the plot area. + * Returns true when data labels are ready to draw. + */ + seriesTypes.pie.prototype.verifyDataLabelOverflow = function (overflow) { + + var center = this.center, + options = this.options, + centerOption = options.center, + minSize = options.minSize || 80, + newSize = minSize, + // If a size is set, return true and don't try to shrink the pie + // to fit the labels. + ret = options.size !== null; + + if (!ret) { + // Handle horizontal size and center + if (centerOption[0] !== null) { // Fixed center + newSize = Math.max(center[2] - + Math.max(overflow[1], overflow[3]), minSize); + + } else { // Auto center + newSize = Math.max( + // horizontal overflow + center[2] - overflow[1] - overflow[3], + minSize + ); + // horizontal center + center[0] += (overflow[3] - overflow[1]) / 2; + } + + // Handle vertical size and center + if (centerOption[1] !== null) { // Fixed center + newSize = Math.max(Math.min(newSize, center[2] - + Math.max(overflow[0], overflow[2])), minSize); + + } else { // Auto center + newSize = Math.max( + Math.min( + newSize, + // vertical overflow + center[2] - overflow[0] - overflow[2] + ), + minSize + ); + // vertical center + center[1] += (overflow[0] - overflow[2]) / 2; + } + + // If the size must be decreased, we need to run translate and + // drawDataLabels again + if (newSize < center[2]) { + center[2] = newSize; + center[3] = Math.min( // #3632 + relativeLength(options.innerSize || 0, newSize), + newSize + ); + this.translate(center); + + if (this.drawDataLabels) { + this.drawDataLabels(); + } + // Else, return true to indicate that the pie and its labels is + // within the plot area + } else { + ret = true; + } + } + return ret; + }; +} + +if (seriesTypes.column) { + + /** + * Override the basic data label alignment by adjusting for the position of + * the column + */ + seriesTypes.column.prototype.alignDataLabel = function ( + point, + dataLabel, + options, + alignTo, + isNew + ) { + var inverted = this.chart.inverted, + series = point.series, + // data label box for alignment + dlBox = point.dlBox || point.shapeArgs, + below = pick( + point.below, // range series + point.plotY > pick(this.translatedThreshold, series.yAxis.len) + ), + // draw it inside the box? + inside = pick(options.inside, !!this.options.stacking), + overshoot; + + // Align to the column itself, or the top of it + if (dlBox) { // Area range uses this method but not alignTo + alignTo = merge(dlBox); + + if (alignTo.y < 0) { + alignTo.height += alignTo.y; + alignTo.y = 0; + } + overshoot = alignTo.y + alignTo.height - series.yAxis.len; + if (overshoot > 0) { + alignTo.height -= overshoot; + } + + if (inverted) { + alignTo = { + x: series.yAxis.len - alignTo.y - alignTo.height, + y: series.xAxis.len - alignTo.x - alignTo.width, + width: alignTo.height, + height: alignTo.width + }; + } + + // Compute the alignment box + if (!inside) { + if (inverted) { + alignTo.x += below ? 0 : alignTo.width; + alignTo.width = 0; + } else { + alignTo.y += below ? alignTo.height : 0; + alignTo.height = 0; + } + } + } + + + // When alignment is undefined (typically columns and bars), display the + // individual point below or above the point depending on the threshold + options.align = pick( + options.align, + !inverted || inside ? 'center' : below ? 'right' : 'left' + ); + options.verticalAlign = pick( + options.verticalAlign, + inverted || inside ? 'middle' : below ? 'top' : 'bottom' + ); + + // Call the parent method + Series.prototype.alignDataLabel.call( + this, + point, + dataLabel, + options, + alignTo, + isNew + ); + + // If label was justified and we have contrast, set it: + if (point.isLabelJustified && point.contrastColor) { + point.dataLabel.css({ + color: point.contrastColor + }); + } + }; +} + +}(Highcharts)); +(function (H) { +/** + * (c) 2009-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +/** + * Highcharts module to hide overlapping data labels. This module is included in + * Highcharts. + */ +var Chart = H.Chart, + each = H.each, + objectEach = H.objectEach, + pick = H.pick, + addEvent = H.addEvent; + +// Collect potensial overlapping data labels. Stack labels probably don't need +// to be considered because they are usually accompanied by data labels that lie +// inside the columns. +addEvent(Chart.prototype, 'render', function collectAndHide() { + var labels = []; + + // Consider external label collectors + each(this.labelCollectors || [], function (collector) { + labels = labels.concat(collector()); + }); + + each(this.yAxis || [], function (yAxis) { + if ( + yAxis.options.stackLabels && + !yAxis.options.stackLabels.allowOverlap + ) { + objectEach(yAxis.stacks, function (stack) { + objectEach(stack, function (stackItem) { + labels.push(stackItem.label); + }); + }); + } + }); + + each(this.series || [], function (series) { + var dlOptions = series.options.dataLabels, + // Range series have two collections + collections = series.dataLabelCollections || ['dataLabel']; + + if ( + (dlOptions.enabled || series._hasPointLabels) && + !dlOptions.allowOverlap && + series.visible + ) { // #3866 + each(collections, function (coll) { + each(series.points, function (point) { + if (point[coll]) { + point[coll].labelrank = pick( + point.labelrank, + point.shapeArgs && point.shapeArgs.height + ); // #4118 + labels.push(point[coll]); + } + }); + }); + } + }); + this.hideOverlappingLabels(labels); +}); + +/** + * Hide overlapping labels. Labels are moved and faded in and out on zoom to + * provide a smooth visual imression. + */ +Chart.prototype.hideOverlappingLabels = function (labels) { + + var len = labels.length, + label, + i, + j, + label1, + label2, + isIntersecting, + pos1, + pos2, + parent1, + parent2, + padding, + bBox, + intersectRect = function (x1, y1, w1, h1, x2, y2, w2, h2) { + return !( + x2 > x1 + w1 || + x2 + w2 < x1 || + y2 > y1 + h1 || + y2 + h2 < y1 + ); + }; + + for (i = 0; i < len; i++) { + label = labels[i]; + if (label) { + + // Mark with initial opacity + label.oldOpacity = label.opacity; + label.newOpacity = 1; + + // Get width and height if pure text nodes (stack labels) + if (!label.width) { + bBox = label.getBBox(); + label.width = bBox.width; + label.height = bBox.height; + } + } + } + + // Prevent a situation in a gradually rising slope, that each label will + // hide the previous one because the previous one always has lower rank. + labels.sort(function (a, b) { + return (b.labelrank || 0) - (a.labelrank || 0); + }); + + // Detect overlapping labels + for (i = 0; i < len; i++) { + label1 = labels[i]; + + for (j = i + 1; j < len; ++j) { + label2 = labels[j]; + if ( + label1 && label2 && + label1 !== label2 && // #6465, polar chart with connectEnds + label1.placed && label2.placed && + label1.newOpacity !== 0 && label2.newOpacity !== 0 + ) { + pos1 = label1.alignAttr; + pos2 = label2.alignAttr; + // Different panes have different positions + parent1 = label1.parentGroup; + parent2 = label2.parentGroup; + // Substract the padding if no background or border (#4333) + padding = 2 * (label1.box ? 0 : (label1.padding || 0)); + isIntersecting = intersectRect( + pos1.x + parent1.translateX, + pos1.y + parent1.translateY, + label1.width - padding, + label1.height - padding, + pos2.x + parent2.translateX, + pos2.y + parent2.translateY, + label2.width - padding, + label2.height - padding + ); + + if (isIntersecting) { + (label1.labelrank < label2.labelrank ? label1 : label2) + .newOpacity = 0; + } + } + } + } + + // Hide or show + each(labels, function (label) { + var complete, + newOpacity; + + if (label) { + newOpacity = label.newOpacity; + + if (label.oldOpacity !== newOpacity && label.placed) { + + // Make sure the label is completely hidden to avoid catching + // clicks (#4362) + if (newOpacity) { + label.show(true); + } else { + complete = function () { + label.hide(); + }; + } + + // Animate or set the opacity + label.alignAttr.opacity = newOpacity; + label[label.isOld ? 'animate' : 'attr']( + label.alignAttr, + null, + complete + ); + + } + label.isOld = true; + } + }); +}; + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var addEvent = H.addEvent, + Chart = H.Chart, + createElement = H.createElement, + css = H.css, + defaultOptions = H.defaultOptions, + defaultPlotOptions = H.defaultPlotOptions, + each = H.each, + extend = H.extend, + fireEvent = H.fireEvent, + hasTouch = H.hasTouch, + inArray = H.inArray, + isObject = H.isObject, + Legend = H.Legend, + merge = H.merge, + pick = H.pick, + Point = H.Point, + Series = H.Series, + seriesTypes = H.seriesTypes, + svg = H.svg, + TrackerMixin; + +/** + * TrackerMixin for points and graphs. + */ +TrackerMixin = H.TrackerMixin = { + + /** + * Draw the tracker for a point. + */ + drawTrackerPoint: function () { + var series = this, + chart = series.chart, + pointer = chart.pointer, + onMouseOver = function (e) { + var point = pointer.getPointFromEvent(e); + // undefined on graph in scatterchart + if (point !== undefined) { + pointer.isDirectTouch = true; + point.onMouseOver(e); + } + }; + + // Add reference to the point + each(series.points, function (point) { + if (point.graphic) { + point.graphic.element.point = point; + } + if (point.dataLabel) { + if (point.dataLabel.div) { + point.dataLabel.div.point = point; + } else { + point.dataLabel.element.point = point; + } + } + }); + + // Add the event listeners, we need to do this only once + if (!series._hasTracking) { + each(series.trackerGroups, function (key) { + if (series[key]) { // we don't always have dataLabelsGroup + series[key] + .addClass('highcharts-tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { + pointer.onTrackerMouseOut(e); + }); + if (hasTouch) { + series[key].on('touchstart', onMouseOver); + } + + + if (series.options.cursor) { + series[key] + .css(css) + .css({ cursor: series.options.cursor }); + } + + } + }); + series._hasTracking = true; + } + }, + + /** + * Draw the tracker object that sits above all data labels and markers to + * track mouse events on the graph or points. For the line type charts + * the tracker uses the same graphPath, but with a greater stroke width + * for better control. + */ + drawTrackerGraph: function () { + var series = this, + options = series.options, + trackByArea = options.trackByArea, + trackerPath = [].concat(trackByArea ? series.areaPath : series.graphPath), + trackerPathLength = trackerPath.length, + chart = series.chart, + pointer = chart.pointer, + renderer = chart.renderer, + snap = chart.options.tooltip.snap, + tracker = series.tracker, + i, + onMouseOver = function () { + if (chart.hoverSeries !== series) { + series.onMouseOver(); + } + }, + /* + * Empirical lowest possible opacities for TRACKER_FILL for an element to stay invisible but clickable + * IE6: 0.002 + * IE7: 0.002 + * IE8: 0.002 + * IE9: 0.00000000001 (unlimited) + * IE10: 0.0001 (exporting only) + * FF: 0.00000000001 (unlimited) + * Chrome: 0.000001 + * Safari: 0.000001 + * Opera: 0.00000000001 (unlimited) + */ + TRACKER_FILL = 'rgba(192,192,192,' + (svg ? 0.0001 : 0.002) + ')'; + + // Extend end points. A better way would be to use round linecaps, + // but those are not clickable in VML. + if (trackerPathLength && !trackByArea) { + i = trackerPathLength + 1; + while (i--) { + if (trackerPath[i] === 'M') { // extend left side + trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], 'L'); + } + if ((i && trackerPath[i] === 'M') || i === trackerPathLength) { // extend right side + trackerPath.splice(i, 0, 'L', trackerPath[i - 2] + snap, trackerPath[i - 1]); + } + } + } + + // draw the tracker + if (tracker) { + tracker.attr({ d: trackerPath }); + } else if (series.graph) { // create + + series.tracker = renderer.path(trackerPath) + .attr({ + 'stroke-linejoin': 'round', // #1225 + visibility: series.visible ? 'visible' : 'hidden', + stroke: TRACKER_FILL, + fill: trackByArea ? TRACKER_FILL : 'none', + 'stroke-width': series.graph.strokeWidth() + (trackByArea ? 0 : 2 * snap), + zIndex: 2 + }) + .add(series.group); + + // The tracker is added to the series group, which is clipped, but is covered + // by the marker group. So the marker group also needs to capture events. + each([series.tracker, series.markerGroup], function (tracker) { + tracker.addClass('highcharts-tracker') + .on('mouseover', onMouseOver) + .on('mouseout', function (e) { + pointer.onTrackerMouseOut(e); + }); + + + if (options.cursor) { + tracker.css({ cursor: options.cursor }); + } + + + if (hasTouch) { + tracker.on('touchstart', onMouseOver); + } + }); + } + } +}; +/* End TrackerMixin */ + + +/** + * Add tracking event listener to the series group, so the point graphics + * themselves act as trackers + */ + +if (seriesTypes.column) { + seriesTypes.column.prototype.drawTracker = TrackerMixin.drawTrackerPoint; +} + +if (seriesTypes.pie) { + seriesTypes.pie.prototype.drawTracker = TrackerMixin.drawTrackerPoint; +} + +if (seriesTypes.scatter) { + seriesTypes.scatter.prototype.drawTracker = TrackerMixin.drawTrackerPoint; +} + +/* + * Extend Legend for item events + */ +extend(Legend.prototype, { + + setItemEvents: function (item, legendItem, useHTML) { + var legend = this, + boxWrapper = legend.chart.renderer.boxWrapper, + activeClass = 'highcharts-legend-' + + (item instanceof Point ? 'point' : 'series') + '-active'; + + // Set the events on the item group, or in case of useHTML, the item itself (#1249) + (useHTML ? legendItem : item.legendGroup).on('mouseover', function () { + item.setState('hover'); + + // A CSS class to dim or hide other than the hovered series + boxWrapper.addClass(activeClass); + + + legendItem.css(legend.options.itemHoverStyle); + + }) + .on('mouseout', function () { + + legendItem.css(merge(item.visible ? legend.itemStyle : legend.itemHiddenStyle)); + + + // A CSS class to dim or hide other than the hovered series + boxWrapper.removeClass(activeClass); + + item.setState(); + }) + .on('click', function (event) { + var strLegendItemClick = 'legendItemClick', + fnLegendItemClick = function () { + if (item.setVisible) { + item.setVisible(); + } + }; + + // A CSS class to dim or hide other than the hovered series. Event + // handling in iOS causes the activeClass to be added prior to click + // in some cases (#7418). + boxWrapper.removeClass(activeClass); + + // Pass over the click/touch event. #4. + event = { + browserEvent: event + }; + + // click the name or symbol + if (item.firePointEvent) { // point + item.firePointEvent(strLegendItemClick, event, fnLegendItemClick); + } else { + fireEvent(item, strLegendItemClick, event, fnLegendItemClick); + } + }); + }, + + createCheckboxForItem: function (item) { + var legend = this; + + item.checkbox = createElement('input', { + type: 'checkbox', + checked: item.selected, + defaultChecked: item.selected // required by IE7 + }, legend.options.itemCheckboxStyle, legend.chart.container); + + addEvent(item.checkbox, 'click', function (event) { + var target = event.target; + fireEvent( + item.series || item, + 'checkboxClick', + { // #3712 + checked: target.checked, + item: item + }, + function () { + item.select(); + } + ); + }); + } +}); + + + +// Add pointer cursor to legend itemstyle in defaultOptions +defaultOptions.legend.itemStyle.cursor = 'pointer'; + + + +/* + * Extend the Chart object with interaction + */ + +extend(Chart.prototype, /** @lends Chart.prototype */ { + /** + * Display the zoom button. + * + * @private + */ + showResetZoom: function () { + var chart = this, + lang = defaultOptions.lang, + btnOptions = chart.options.chart.resetZoomButton, + theme = btnOptions.theme, + states = theme.states, + alignTo = btnOptions.relativeTo === 'chart' ? null : 'plotBox'; + + function zoomOut() { + chart.zoomOut(); + } + + this.resetZoomButton = chart.renderer.button(lang.resetZoom, null, null, zoomOut, theme, states && states.hover) + .attr({ + align: btnOptions.position.align, + title: lang.resetZoomTitle + }) + .addClass('highcharts-reset-zoom') + .add() + .align(btnOptions.position, false, alignTo); + + }, + + /** + * Zoom out to 1:1. + * + * @private + */ + zoomOut: function () { + var chart = this; + fireEvent(chart, 'selection', { resetSelection: true }, function () { + chart.zoom(); + }); + }, + + /** + * Zoom into a given portion of the chart given by axis coordinates. + * @param {Object} event + * + * @private + */ + zoom: function (event) { + var chart = this, + hasZoomed, + pointer = chart.pointer, + displayButton = false, + resetZoomButton; + + // If zoom is called with no arguments, reset the axes + if (!event || event.resetSelection) { + each(chart.axes, function (axis) { + hasZoomed = axis.zoom(); + }); + pointer.initiated = false; // #6804 + + } else { // else, zoom in on all axes + each(event.xAxis.concat(event.yAxis), function (axisData) { + var axis = axisData.axis, + isXAxis = axis.isXAxis; + + // don't zoom more than minRange + if (pointer[isXAxis ? 'zoomX' : 'zoomY']) { + hasZoomed = axis.zoom(axisData.min, axisData.max); + if (axis.displayBtn) { + displayButton = true; + } + } + }); + } + + // Show or hide the Reset zoom button + resetZoomButton = chart.resetZoomButton; + if (displayButton && !resetZoomButton) { + chart.showResetZoom(); + } else if (!displayButton && isObject(resetZoomButton)) { + chart.resetZoomButton = resetZoomButton.destroy(); + } + + + // Redraw + if (hasZoomed) { + chart.redraw( + pick(chart.options.chart.animation, event && event.animation, chart.pointCount < 100) // animation + ); + } + }, + + /** + * Pan the chart by dragging the mouse across the pane. This function is + * called on mouse move, and the distance to pan is computed from chartX + * compared to the first chartX position in the dragging operation. + * + * @private + */ + pan: function (e, panning) { + + var chart = this, + hoverPoints = chart.hoverPoints, + doRedraw; + + // remove active points for shared tooltip + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + each(panning === 'xy' ? [1, 0] : [1], function (isX) { // xy is used in maps + var axis = chart[isX ? 'xAxis' : 'yAxis'][0], + horiz = axis.horiz, + mousePos = e[horiz ? 'chartX' : 'chartY'], + mouseDown = horiz ? 'mouseDownX' : 'mouseDownY', + startPos = chart[mouseDown], + halfPointRange = (axis.pointRange || 0) / 2, + extremes = axis.getExtremes(), + panMin = axis.toValue(startPos - mousePos, true) + + halfPointRange, + panMax = axis.toValue(startPos + axis.len - mousePos, true) - + halfPointRange, + flipped = panMax < panMin, + newMin = flipped ? panMax : panMin, + newMax = flipped ? panMin : panMax, + paddedMin = Math.min( + extremes.dataMin, + halfPointRange ? + extremes.min : + axis.toValue( + axis.toPixels(extremes.min) - axis.minPixelPadding + ) + ), + paddedMax = Math.max( + extremes.dataMax, + halfPointRange ? + extremes.max : + axis.toValue( + axis.toPixels(extremes.max) + axis.minPixelPadding + ) + ), + spill; + + // If the new range spills over, either to the min or max, adjust + // the new range. + spill = paddedMin - newMin; + if (spill > 0) { + newMax += spill; + newMin = paddedMin; + } + spill = newMax - paddedMax; + if (spill > 0) { + newMax = paddedMax; + newMin -= spill; + } + + // Set new extremes if they are actually new + if (axis.series.length && newMin !== extremes.min && newMax !== extremes.max) { + axis.setExtremes( + newMin, + newMax, + false, + false, + { trigger: 'pan' } + ); + doRedraw = true; + } + + chart[mouseDown] = mousePos; // set new reference for next run + }); + + if (doRedraw) { + chart.redraw(false); + } + css(chart.container, { cursor: 'move' }); + } +}); + +/* + * Extend the Point object with interaction + */ +extend(Point.prototype, /** @lends Highcharts.Point.prototype */ { + /** + * Toggle the selection status of a point. + * @param {Boolean} [selected] + * When `true`, the point is selected. When `false`, the point is + * unselected. When `null` or `undefined`, the selection state is + * toggled. + * @param {Boolean} [accumulate=false] + * When `true`, the selection is added to other selected points. + * When `false`, other selected points are deselected. Internally in + * Highcharts, when {@link http://api.highcharts.com/highcharts/plotOptions.series.allowPointSelect|allowPointSelect} + * is `true`, selected points are accumulated on Control, Shift or + * Cmd clicking the point. + * + * @see Highcharts.Chart#getSelectedPoints + * + * @sample highcharts/members/point-select/ + * Select a point from a button + * @sample highcharts/chart/events-selection-points/ + * Select a range of points through a drag selection + * @sample maps/series/data-id/ + * Select a point in Highmaps + */ + select: function (selected, accumulate) { + var point = this, + series = point.series, + chart = series.chart; + + selected = pick(selected, !point.selected); + + // fire the event with the default handler + point.firePointEvent(selected ? 'select' : 'unselect', { accumulate: accumulate }, function () { + + /** + * Whether the point is selected or not. + * @see Point#select + * @see Chart#getSelectedPoints + * @memberof Point + * @name selected + * @type {Boolean} + */ + point.selected = point.options.selected = selected; + series.options.data[inArray(point, series.data)] = point.options; + + point.setState(selected && 'select'); + + // unselect all other points unless Ctrl or Cmd + click + if (!accumulate) { + each(chart.getSelectedPoints(), function (loopPoint) { + if (loopPoint.selected && loopPoint !== point) { + loopPoint.selected = loopPoint.options.selected = false; + series.options.data[inArray(loopPoint, series.data)] = loopPoint.options; + loopPoint.setState(''); + loopPoint.firePointEvent('unselect'); + } + }); + } + }); + }, + + /** + * Runs on mouse over the point. Called internally from mouse and touch + * events. + * + * @param {Object} e The event arguments + */ + onMouseOver: function (e) { + var point = this, + series = point.series, + chart = series.chart, + pointer = chart.pointer; + e = e ? + pointer.normalize(e) : + // In cases where onMouseOver is called directly without an event + pointer.getChartCoordinatesFromPoint(point, chart.inverted); + pointer.runPointActions(e, point); + }, + + /** + * Runs on mouse out from the point. Called internally from mouse and touch + * events. + */ + onMouseOut: function () { + var point = this, + chart = point.series.chart; + point.firePointEvent('mouseOut'); + each(chart.hoverPoints || [], function (p) { + p.setState(); + }); + chart.hoverPoints = chart.hoverPoint = null; + }, + + /** + * Import events from the series' and point's options. Only do it on + * demand, to save processing time on hovering. + * + * @private + */ + importEvents: function () { + if (!this.hasImportedEvents) { + var point = this, + options = merge(point.series.options.point, point.options), + events = options.events; + + point.events = events; + + H.objectEach(events, function (event, eventType) { + addEvent(point, eventType, event); + }); + this.hasImportedEvents = true; + + } + }, + + /** + * Set the point's state. + * @param {String} [state] + * The new state, can be one of `''` (an empty string), `hover` or + * `select`. + */ + setState: function (state, move) { + var point = this, + plotX = Math.floor(point.plotX), // #4586 + plotY = point.plotY, + series = point.series, + stateOptions = series.options.states[state] || {}, + markerOptions = defaultPlotOptions[series.type].marker && + series.options.marker, + normalDisabled = markerOptions && markerOptions.enabled === false, + markerStateOptions = (markerOptions && markerOptions.states && + markerOptions.states[state]) || {}, + stateDisabled = markerStateOptions.enabled === false, + stateMarkerGraphic = series.stateMarkerGraphic, + pointMarker = point.marker || {}, + chart = series.chart, + halo = series.halo, + haloOptions, + markerAttribs, + hasMarkers = markerOptions && series.markerAttribs, + newSymbol; + + state = state || ''; // empty string + + if ( + // already has this state + (state === point.state && !move) || + + // selected points don't respond to hover + (point.selected && state !== 'select') || + + // series' state options is disabled + (stateOptions.enabled === false) || + + // general point marker's state options is disabled + (state && ( + stateDisabled || + (normalDisabled && markerStateOptions.enabled === false) + )) || + + // individual point marker's state options is disabled + ( + state && + pointMarker.states && + pointMarker.states[state] && + pointMarker.states[state].enabled === false + ) // #1610 + + ) { + return; + } + + if (hasMarkers) { + markerAttribs = series.markerAttribs(point, state); + } + + // Apply hover styles to the existing point + if (point.graphic) { + + if (point.state) { + point.graphic.removeClass('highcharts-point-' + point.state); + } + if (state) { + point.graphic.addClass('highcharts-point-' + state); + } + + + point.graphic.animate( + series.pointAttribs(point, state), + pick( + chart.options.chart.animation, + stateOptions.animation + ) + ); + + + if (markerAttribs) { + point.graphic.animate( + markerAttribs, + pick( + chart.options.chart.animation, // Turn off globally + markerStateOptions.animation, + markerOptions.animation + ) + ); + } + + // Zooming in from a range with no markers to a range with markers + if (stateMarkerGraphic) { + stateMarkerGraphic.hide(); + } + } else { + // if a graphic is not applied to each point in the normal state, create a shared + // graphic for the hover state + if (state && markerStateOptions) { + newSymbol = pointMarker.symbol || series.symbol; + + // If the point has another symbol than the previous one, throw away the + // state marker graphic and force a new one (#1459) + if (stateMarkerGraphic && stateMarkerGraphic.currentSymbol !== newSymbol) { + stateMarkerGraphic = stateMarkerGraphic.destroy(); + } + + // Add a new state marker graphic + if (!stateMarkerGraphic) { + if (newSymbol) { + series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.symbol( + newSymbol, + markerAttribs.x, + markerAttribs.y, + markerAttribs.width, + markerAttribs.height + ) + .add(series.markerGroup); + stateMarkerGraphic.currentSymbol = newSymbol; + } + + // Move the existing graphic + } else { + stateMarkerGraphic[move ? 'animate' : 'attr']({ // #1054 + x: markerAttribs.x, + y: markerAttribs.y + }); + } + + if (stateMarkerGraphic) { + stateMarkerGraphic.attr(series.pointAttribs(point, state)); + } + + } + + if (stateMarkerGraphic) { + stateMarkerGraphic[state && chart.isInsidePlot(plotX, plotY, chart.inverted) ? 'show' : 'hide'](); // #2450 + stateMarkerGraphic.element.point = point; // #4310 + } + } + + // Show me your halo + haloOptions = stateOptions.halo; + if (haloOptions && haloOptions.size) { + if (!halo) { + series.halo = halo = chart.renderer.path() + // #5818, #5903, #6705 + .add((point.graphic || stateMarkerGraphic).parentGroup); + } + halo[move ? 'animate' : 'attr']({ + d: point.haloPath(haloOptions.size) + }); + halo.attr({ + 'class': 'highcharts-halo highcharts-color-' + + pick(point.colorIndex, series.colorIndex) + }); + halo.point = point; // #6055 + + + halo.attr(extend({ + 'fill': point.color || series.color, + 'fill-opacity': haloOptions.opacity, + 'zIndex': -1 // #4929, IE8 added halo above everything + }, haloOptions.attributes)); + + + } else if (halo && halo.point && halo.point.haloPath) { + // Animate back to 0 on the current halo point (#6055) + halo.animate({ d: halo.point.haloPath(0) }); + } + + point.state = state; + }, + + /** + * Get the path definition for the halo, which is usually a shadow-like + * circle around the currently hovered point. + * @param {Number} size + * The radius of the circular halo. + * @return {Array} The path definition + */ + haloPath: function (size) { + var series = this.series, + chart = series.chart; + + return chart.renderer.symbols.circle( + Math.floor(this.plotX) - size, + this.plotY - size, + size * 2, + size * 2 + ); + } +}); + +/* + * Extend the Series object with interaction + */ + +extend(Series.prototype, /** @lends Highcharts.Series.prototype */ { + /** + * Runs on mouse over the series graphical items. + */ + onMouseOver: function () { + var series = this, + chart = series.chart, + hoverSeries = chart.hoverSeries; + + // set normal state to previous series + if (hoverSeries && hoverSeries !== series) { + hoverSeries.onMouseOut(); + } + + // trigger the event, but to save processing time, + // only if defined + if (series.options.events.mouseOver) { + fireEvent(series, 'mouseOver'); + } + + // hover this + series.setState('hover'); + chart.hoverSeries = series; + }, + + /** + * Runs on mouse out of the series graphical items. + */ + onMouseOut: function () { + // trigger the event only if listeners exist + var series = this, + options = series.options, + chart = series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + chart.hoverSeries = null; // #182, set to null before the mouseOut event fires + + // trigger mouse out on the point, which must be in this series + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + // fire the mouse out event + if (series && options.events.mouseOut) { + fireEvent(series, 'mouseOut'); + } + + + // hide the tooltip + if (tooltip && !series.stickyTracking && (!tooltip.shared || series.noSharedTooltip)) { + tooltip.hide(); + } + + // set normal state + series.setState(); + }, + + /** + * Set the state of the series. Called internally on mouse interaction + * operations, but it can also be called directly to visually + * highlight a series. + * + * @param {String} [state] + * Can be either `hover` or undefined to set to normal + * state. + */ + setState: function (state) { + var series = this, + options = series.options, + graph = series.graph, + stateOptions = options.states, + lineWidth = options.lineWidth, + attribs, + i = 0; + + state = state || ''; + + if (series.state !== state) { + + // Toggle class names + each([ + series.group, + series.markerGroup, + series.dataLabelsGroup + ], function (group) { + if (group) { + // Old state + if (series.state) { + group.removeClass('highcharts-series-' + series.state); + } + // New state + if (state) { + group.addClass('highcharts-series-' + state); + } + } + }); + + series.state = state; + + + + if (stateOptions[state] && stateOptions[state].enabled === false) { + return; + } + + if (state) { + lineWidth = stateOptions[state].lineWidth || lineWidth + (stateOptions[state].lineWidthPlus || 0); // #4035 + } + + if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML + attribs = { + 'stroke-width': lineWidth + }; + + // Animate the graph stroke-width. By default a quick animation + // to hover, slower to un-hover. + graph.animate( + attribs, + pick( + series.chart.options.chart.animation, + stateOptions[state] && stateOptions[state].animation + ) + ); + while (series['zone-graph-' + i]) { + series['zone-graph-' + i].attr(attribs); + i = i + 1; + } + } + + } + }, + + /** + * Show or hide the series. + * + * @param {Boolean} [visible] + * True to show the series, false to hide. If undefined, the + * visibility is toggled. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart after the series is altered. If doing + * more operations on the chart, it is a good idea to set redraw to + * false and call {@link Chart#redraw|chart.redraw()} after. + */ + setVisible: function (vis, redraw) { + var series = this, + chart = series.chart, + legendItem = series.legendItem, + showOrHide, + ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, + oldVisibility = series.visible; + + // if called without an argument, toggle visibility + series.visible = vis = series.options.visible = series.userOptions.visible = vis === undefined ? !oldVisibility : vis; // #5618 + showOrHide = vis ? 'show' : 'hide'; + + // show or hide elements + each(['group', 'dataLabelsGroup', 'markerGroup', 'tracker', 'tt'], function (key) { + if (series[key]) { + series[key][showOrHide](); + } + }); + + + // hide tooltip (#1361) + if (chart.hoverSeries === series || (chart.hoverPoint && chart.hoverPoint.series) === series) { + series.onMouseOut(); + } + + + if (legendItem) { + chart.legend.colorizeItem(series, vis); + } + + + // rescale or adapt to resized chart + series.isDirty = true; + // in a stack, all other series are affected + if (series.options.stacking) { + each(chart.series, function (otherSeries) { + if (otherSeries.options.stacking && otherSeries.visible) { + otherSeries.isDirty = true; + } + }); + } + + // show or hide linked series + each(series.linkedSeries, function (otherSeries) { + otherSeries.setVisible(vis, false); + }); + + if (ignoreHiddenSeries) { + chart.isDirtyBox = true; + } + if (redraw !== false) { + chart.redraw(); + } + + fireEvent(series, showOrHide); + }, + + /** + * Show the series if hidden. + * + * @sample highcharts/members/series-hide/ + * Toggle visibility from a button + */ + show: function () { + this.setVisible(true); + }, + + /** + * Hide the series if visible. If the {@link + * https://api.highcharts.com/highcharts/chart.ignoreHiddenSeries| + * chart.ignoreHiddenSeries} option is true, the chart is redrawn without + * this series. + * + * @sample highcharts/members/series-hide/ + * Toggle visibility from a button + */ + hide: function () { + this.setVisible(false); + }, + + + /** + * Select or unselect the series. This means its {@link + * Highcharts.Series.selected|selected} property is set, the checkbox in the + * legend is toggled and when selected, the series is returned by the + * {@link Highcharts.Chart#getSelectedSeries} function. + * + * @param {Boolean} [selected] + * True to select the series, false to unselect. If undefined, the + * selection state is toggled. + * + * @sample highcharts/members/series-select/ + * Select a series from a button + */ + select: function (selected) { + var series = this; + + series.selected = selected = (selected === undefined) ? + !series.selected : + selected; + + if (series.checkbox) { + series.checkbox.checked = selected; + } + + fireEvent(series, selected ? 'select' : 'unselect'); + }, + + drawTracker: TrackerMixin.drawTrackerGraph +}); + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var Chart = H.Chart, + each = H.each, + inArray = H.inArray, + isArray = H.isArray, + isObject = H.isObject, + pick = H.pick, + splat = H.splat; + + +/** + * Allows setting a set of rules to apply for different screen or chart + * sizes. Each rule specifies additional chart options. + * + * @sample {highstock} stock/demo/responsive/ Stock chart + * @sample highcharts/responsive/axis/ Axis + * @sample highcharts/responsive/legend/ Legend + * @sample highcharts/responsive/classname/ Class name + * @since 5.0.0 + * @apioption responsive + */ + +/** + * A set of rules for responsive settings. The rules are executed from + * the top down. + * + * @type {Array} + * @sample {highcharts} highcharts/responsive/axis/ Axis changes + * @sample {highstock} highcharts/responsive/axis/ Axis changes + * @sample {highmaps} highcharts/responsive/axis/ Axis changes + * @since 5.0.0 + * @apioption responsive.rules + */ + +/** + * A full set of chart options to apply as overrides to the general + * chart options. The chart options are applied when the given rule + * is active. + * + * A special case is configuration objects that take arrays, for example + * [xAxis](#xAxis), [yAxis](#yAxis) or [series](#series). For these + * collections, an `id` option is used to map the new option set to + * an existing object. If an existing object of the same id is not found, + * the item of the same indexupdated. So for example, setting `chartOptions` + * with two series items without an `id`, will cause the existing chart's + * two series to be updated with respective options. + * + * @type {Object} + * @sample {highstock} stock/demo/responsive/ Stock chart + * @sample highcharts/responsive/axis/ Axis + * @sample highcharts/responsive/legend/ Legend + * @sample highcharts/responsive/classname/ Class name + * @since 5.0.0 + * @apioption responsive.rules.chartOptions + */ + +/** + * Under which conditions the rule applies. + * + * @type {Object} + * @since 5.0.0 + * @apioption responsive.rules.condition + */ + +/** + * A callback function to gain complete control on when the responsive + * rule applies. Return `true` if it applies. This opens for checking + * against other metrics than the chart size, or example the document + * size or other elements. + * + * @type {Function} + * @context Chart + * @since 5.0.0 + * @apioption responsive.rules.condition.callback + */ + +/** + * The responsive rule applies if the chart height is less than this. + * + * @type {Number} + * @since 5.0.0 + * @apioption responsive.rules.condition.maxHeight + */ + +/** + * The responsive rule applies if the chart width is less than this. + * + * @type {Number} + * @sample highcharts/responsive/axis/ Max width is 500 + * @since 5.0.0 + * @apioption responsive.rules.condition.maxWidth + */ + +/** + * The responsive rule applies if the chart height is greater than this. + * + * @type {Number} + * @default 0 + * @since 5.0.0 + * @apioption responsive.rules.condition.minHeight + */ + +/** + * The responsive rule applies if the chart width is greater than this. + * + * @type {Number} + * @default 0 + * @since 5.0.0 + * @apioption responsive.rules.condition.minWidth + */ + +/** + * Update the chart based on the current chart/document size and options for + * responsiveness. + */ +Chart.prototype.setResponsive = function (redraw) { + var options = this.options.responsive, + ruleIds = [], + currentResponsive = this.currentResponsive, + currentRuleIds; + + if (options && options.rules) { + each(options.rules, function (rule) { + if (rule._id === undefined) { + rule._id = H.uniqueKey(); + } + + this.matchResponsiveRule(rule, ruleIds, redraw); + }, this); + } + + // Merge matching rules + var mergedOptions = H.merge.apply(0, H.map(ruleIds, function (ruleId) { + return H.find(options.rules, function (rule) { + return rule._id === ruleId; + }).chartOptions; + })); + + // Stringified key for the rules that currently apply. + ruleIds = ruleIds.toString() || undefined; + currentRuleIds = currentResponsive && currentResponsive.ruleIds; + + + // Changes in what rules apply + if (ruleIds !== currentRuleIds) { + + // Undo previous rules. Before we apply a new set of rules, we need to + // roll back completely to base options (#6291). + if (currentResponsive) { + this.update(currentResponsive.undoOptions, redraw); + } + + if (ruleIds) { + // Get undo-options for matching rules + this.currentResponsive = { + ruleIds: ruleIds, + mergedOptions: mergedOptions, + undoOptions: this.currentOptions(mergedOptions) + }; + + this.update(mergedOptions, redraw); + + } else { + this.currentResponsive = undefined; + } + } +}; + +/** + * Handle a single responsiveness rule + */ +Chart.prototype.matchResponsiveRule = function (rule, matches) { + var condition = rule.condition, + fn = condition.callback || function () { + return this.chartWidth <= pick(condition.maxWidth, Number.MAX_VALUE) && + this.chartHeight <= pick(condition.maxHeight, Number.MAX_VALUE) && + this.chartWidth >= pick(condition.minWidth, 0) && + this.chartHeight >= pick(condition.minHeight, 0); + }; + + if (fn.call(this)) { + matches.push(rule._id); + } + +}; + +/** + * Get the current values for a given set of options. Used before we update + * the chart with a new responsiveness rule. + * TODO: Restore axis options (by id?) + */ +Chart.prototype.currentOptions = function (options) { + + var ret = {}; + + /** + * Recurse over a set of options and its current values, + * and store the current values in the ret object. + */ + function getCurrent(options, curr, ret, depth) { + var i; + H.objectEach(options, function (val, key) { + if (!depth && inArray(key, ['series', 'xAxis', 'yAxis']) > -1) { + val = splat(val); + + ret[key] = []; + + // Iterate over collections like series, xAxis or yAxis and map + // the items by index. + for (i = 0; i < val.length; i++) { + if (curr[key][i]) { // Item exists in current data (#6347) + ret[key][i] = {}; + getCurrent( + val[i], + curr[key][i], + ret[key][i], + depth + 1 + ); + } + } + } else if (isObject(val)) { + ret[key] = isArray(val) ? [] : {}; + getCurrent(val, curr[key] || {}, ret[key], depth + 1); + } else { + ret[key] = curr[key] || null; + } + }); + } + + getCurrent(options, this.options, ret, 0); + return ret; +}; + +}(Highcharts)); +var Highcharts = (function (Highcharts) { + +return Highcharts; +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var addEvent = H.addEvent, + Axis = H.Axis, + Chart = H.Chart, + css = H.css, + defined = H.defined, + each = H.each, + extend = H.extend, + noop = H.noop, + pick = H.pick, + Series = H.Series, + timeUnits = H.timeUnits, + wrap = H.wrap; + +/* **************************************************************************** + * Start ordinal axis logic * + *****************************************************************************/ + + +wrap(Series.prototype, 'init', function (proceed) { + var series = this, + xAxis; + + // call the original function + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + + xAxis = series.xAxis; + + // Destroy the extended ordinal index on updated data + if (xAxis && xAxis.options.ordinal) { + addEvent(series, 'updatedData', function () { + delete xAxis.ordinalIndex; + }); + } +}); + +/** + * In an ordinal axis, there might be areas with dense consentrations of points, then large + * gaps between some. Creating equally distributed ticks over this entire range + * may lead to a huge number of ticks that will later be removed. So instead, break the + * positions up in segments, find the tick positions for each segment then concatenize them. + * This method is used from both data grouping logic and X axis tick position logic. + */ +wrap(Axis.prototype, 'getTimeTicks', function (proceed, normalizedInterval, min, max, startOfWeek, positions, closestDistance, findHigherRanks) { + + var start = 0, + end, + segmentPositions, + higherRanks = {}, + hasCrossedHigherRank, + info, + posLength, + outsideMax, + groupPositions = [], + lastGroupPosition = -Number.MAX_VALUE, + tickPixelIntervalOption = this.options.tickPixelInterval, + time = this.chart.time; + + // The positions are not always defined, for example for ordinal positions when data + // has regular interval (#1557, #2090) + if ((!this.options.ordinal && !this.options.breaks) || !positions || positions.length < 3 || min === undefined) { + return proceed.call(this, normalizedInterval, min, max, startOfWeek); + } + + // Analyze the positions array to split it into segments on gaps larger than 5 times + // the closest distance. The closest distance is already found at this point, so + // we reuse that instead of computing it again. + posLength = positions.length; + + for (end = 0; end < posLength; end++) { + + outsideMax = end && positions[end - 1] > max; + + if (positions[end] < min) { // Set the last position before min + start = end; + } + + if (end === posLength - 1 || positions[end + 1] - positions[end] > closestDistance * 5 || outsideMax) { + + // For each segment, calculate the tick positions from the getTimeTicks utility + // function. The interval will be the same regardless of how long the segment is. + if (positions[end] > lastGroupPosition) { // #1475 + + segmentPositions = proceed.call(this, normalizedInterval, positions[start], positions[end], startOfWeek); + + // Prevent duplicate groups, for example for multiple segments within one larger time frame (#1475) + while (segmentPositions.length && segmentPositions[0] <= lastGroupPosition) { + segmentPositions.shift(); + } + if (segmentPositions.length) { + lastGroupPosition = segmentPositions[segmentPositions.length - 1]; + } + + groupPositions = groupPositions.concat(segmentPositions); + } + // Set start of next segment + start = end + 1; + } + + if (outsideMax) { + break; + } + } + + // Get the grouping info from the last of the segments. The info is the same for + // all segments. + info = segmentPositions.info; + + // Optionally identify ticks with higher rank, for example when the ticks + // have crossed midnight. + if (findHigherRanks && info.unitRange <= timeUnits.hour) { + end = groupPositions.length - 1; + + // Compare points two by two + for (start = 1; start < end; start++) { + if ( + time.dateFormat('%d', groupPositions[start]) !== + time.dateFormat('%d', groupPositions[start - 1]) + ) { + higherRanks[groupPositions[start]] = 'day'; + hasCrossedHigherRank = true; + } + } + + // If the complete array has crossed midnight, we want to mark the first + // positions also as higher rank + if (hasCrossedHigherRank) { + higherRanks[groupPositions[0]] = 'day'; + } + info.higherRanks = higherRanks; + } + + // Save the info + groupPositions.info = info; + + + + // Don't show ticks within a gap in the ordinal axis, where the space between + // two points is greater than a portion of the tick pixel interval + if (findHigherRanks && defined(tickPixelIntervalOption)) { // check for squashed ticks + + var length = groupPositions.length, + i = length, + itemToRemove, + translated, + translatedArr = [], + lastTranslated, + medianDistance, + distance, + distances = []; + + // Find median pixel distance in order to keep a reasonably even distance between + // ticks (#748) + while (i--) { + translated = this.translate(groupPositions[i]); + if (lastTranslated) { + distances[i] = lastTranslated - translated; + } + translatedArr[i] = lastTranslated = translated; + } + distances.sort(); + medianDistance = distances[Math.floor(distances.length / 2)]; + if (medianDistance < tickPixelIntervalOption * 0.6) { + medianDistance = null; + } + + // Now loop over again and remove ticks where needed + i = groupPositions[length - 1] > max ? length - 1 : length; // #817 + lastTranslated = undefined; + while (i--) { + translated = translatedArr[i]; + distance = Math.abs(lastTranslated - translated); + // #4175 - when axis is reversed, the distance, is negative but + // tickPixelIntervalOption positive, so we need to compare the same values + + // Remove ticks that are closer than 0.6 times the pixel interval from the one to the right, + // but not if it is close to the median distance (#748). + if (lastTranslated && distance < tickPixelIntervalOption * 0.8 && + (medianDistance === null || distance < medianDistance * 0.8)) { + + // Is this a higher ranked position with a normal position to the right? + if (higherRanks[groupPositions[i]] && !higherRanks[groupPositions[i + 1]]) { + + // Yes: remove the lower ranked neighbour to the right + itemToRemove = i + 1; + lastTranslated = translated; // #709 + + } else { + + // No: remove this one + itemToRemove = i; + } + + groupPositions.splice(itemToRemove, 1); + + } else { + lastTranslated = translated; + } + } + } + return groupPositions; +}); + +// Extend the Axis prototype +extend(Axis.prototype, /** @lends Axis.prototype */ { + + /** + * Calculate the ordinal positions before tick positions are calculated. + */ + beforeSetTickPositions: function () { + var axis = this, + len, + ordinalPositions = [], + useOrdinal = false, + dist, + extremes = axis.getExtremes(), + min = extremes.min, + max = extremes.max, + minIndex, + maxIndex, + slope, + hasBreaks = axis.isXAxis && !!axis.options.breaks, + isOrdinal = axis.options.ordinal, + overscrollPointsRange = Number.MAX_VALUE, + ignoreHiddenSeries = axis.chart.options.chart.ignoreHiddenSeries, + isNavigatorAxis = axis.options.className === 'highcharts-navigator-xaxis', + i; + + if ( + axis.options.overscroll && + axis.max === axis.dataMax && + ( + // Panning is an execption, + // We don't want to apply overscroll when panning over the dataMax + !axis.chart.mouseIsDown || + isNavigatorAxis + ) && ( + // Scrollbar buttons are the other execption: + !axis.eventArgs || + axis.eventArgs && axis.eventArgs.trigger !== 'navigator' + ) + ) { + axis.max += axis.options.overscroll; + + // Live data and buttons require translation for the min: + if (!isNavigatorAxis && defined(axis.userMin)) { + axis.min += axis.options.overscroll; + } + } + + // Apply the ordinal logic + if (isOrdinal || hasBreaks) { // #4167 YAxis is never ordinal ? + + each(axis.series, function (series, i) { + + if ( + (!ignoreHiddenSeries || series.visible !== false) && + (series.takeOrdinalPosition !== false || hasBreaks) + ) { + + // concatenate the processed X data into the existing positions, or the empty array + ordinalPositions = ordinalPositions.concat(series.processedXData); + len = ordinalPositions.length; + + // remove duplicates (#1588) + ordinalPositions.sort(function (a, b) { + return a - b; // without a custom function it is sorted as strings + }); + + overscrollPointsRange = Math.min( + overscrollPointsRange, + pick( + // Check for a single-point series: + series.closestPointRange, + overscrollPointsRange + ) + ); + + if (len) { + i = len - 1; + while (i--) { + if (ordinalPositions[i] === ordinalPositions[i + 1]) { + ordinalPositions.splice(i, 1); + } + } + } + } + + }); + + // cache the length + len = ordinalPositions.length; + + // Check if we really need the overhead of mapping axis data against the ordinal positions. + // If the series consist of evenly spaced data any way, we don't need any ordinal logic. + if (len > 2) { // two points have equal distance by default + dist = ordinalPositions[1] - ordinalPositions[0]; + i = len - 1; + while (i-- && !useOrdinal) { + if (ordinalPositions[i + 1] - ordinalPositions[i] !== dist) { + useOrdinal = true; + } + } + + // When zooming in on a week, prevent axis padding for weekends even though the data within + // the week is evenly spaced. + if ( + !axis.options.keepOrdinalPadding && + ( + ordinalPositions[0] - min > dist || + max - ordinalPositions[ordinalPositions.length - 1] > dist + ) + ) { + useOrdinal = true; + } + } else if (axis.options.overscroll) { + if (len === 2) { + // Exactly two points, distance for overscroll is fixed: + overscrollPointsRange = ordinalPositions[1] - ordinalPositions[0]; + } else if (len === 1) { + // We have just one point, closest distance is unknown. + // Assume then it is last point and overscrolled range: + overscrollPointsRange = axis.options.overscroll; + ordinalPositions = [ordinalPositions[0], ordinalPositions[0] + overscrollPointsRange]; + } else { + // In case of zooming in on overscrolled range, stick to the old range: + overscrollPointsRange = axis.overscrollPointsRange; + } + } + + // Record the slope and offset to compute the linear values from the array index. + // Since the ordinal positions may exceed the current range, get the start and + // end positions within it (#719, #665b) + if (useOrdinal) { + + if (axis.options.overscroll) { + axis.overscrollPointsRange = overscrollPointsRange; + ordinalPositions = ordinalPositions.concat(axis.getOverscrollPositions()); + } + + // Register + axis.ordinalPositions = ordinalPositions; + + // This relies on the ordinalPositions being set. Use Math.max + // and Math.min to prevent padding on either sides of the data. + minIndex = axis.ordinal2lin( // #5979 + Math.max( + min, + ordinalPositions[0] + ), + true + ); + maxIndex = Math.max(axis.ordinal2lin( + Math.min( + max, + ordinalPositions[ordinalPositions.length - 1] + ), + true + ), 1); // #3339 + + // Set the slope and offset of the values compared to the indices in the ordinal positions + axis.ordinalSlope = slope = (max - min) / (maxIndex - minIndex); + axis.ordinalOffset = min - (minIndex * slope); + + } else { + axis.overscrollPointsRange = pick(axis.closestPointRange, axis.overscrollPointsRange); + axis.ordinalPositions = axis.ordinalSlope = axis.ordinalOffset = undefined; + } + } + + axis.isOrdinal = isOrdinal && useOrdinal; // #3818, #4196, #4926 + axis.groupIntervalFactor = null; // reset for next run + }, + /** + * Translate from a linear axis value to the corresponding ordinal axis position. If there + * are no gaps in the ordinal axis this will be the same. The translated value is the value + * that the point would have if the axis were linear, using the same min and max. + * + * @param Number val The axis value + * @param Boolean toIndex Whether to return the index in the ordinalPositions or the new value + */ + val2lin: function (val, toIndex) { + var axis = this, + ordinalPositions = axis.ordinalPositions, + ret; + + if (!ordinalPositions) { + ret = val; + + } else { + + var ordinalLength = ordinalPositions.length, + i, + distance, + ordinalIndex; + + // first look for an exact match in the ordinalpositions array + i = ordinalLength; + while (i--) { + if (ordinalPositions[i] === val) { + ordinalIndex = i; + break; + } + } + + // if that failed, find the intermediate position between the two nearest values + i = ordinalLength - 1; + while (i--) { + if (val > ordinalPositions[i] || i === 0) { // interpolate + distance = (val - ordinalPositions[i]) / (ordinalPositions[i + 1] - ordinalPositions[i]); // something between 0 and 1 + ordinalIndex = i + distance; + break; + } + } + ret = toIndex ? + ordinalIndex : + axis.ordinalSlope * (ordinalIndex || 0) + axis.ordinalOffset; + } + return ret; + }, + /** + * Translate from linear (internal) to axis value + * + * @param Number val The linear abstracted value + * @param Boolean fromIndex Translate from an index in the ordinal positions rather than a value + */ + lin2val: function (val, fromIndex) { + var axis = this, + ordinalPositions = axis.ordinalPositions, + ret; + + if (!ordinalPositions) { // the visible range contains only equally spaced values + ret = val; + + } else { + + var ordinalSlope = axis.ordinalSlope, + ordinalOffset = axis.ordinalOffset, + i = ordinalPositions.length - 1, + linearEquivalentLeft, + linearEquivalentRight, + distance; + + + // Handle the case where we translate from the index directly, used only + // when panning an ordinal axis + if (fromIndex) { + + if (val < 0) { // out of range, in effect panning to the left + val = ordinalPositions[0]; + } else if (val > i) { // out of range, panning to the right + val = ordinalPositions[i]; + } else { // split it up + i = Math.floor(val); + distance = val - i; // the decimal + } + + // Loop down along the ordinal positions. When the linear equivalent of i matches + // an ordinal position, interpolate between the left and right values. + } else { + while (i--) { + linearEquivalentLeft = (ordinalSlope * i) + ordinalOffset; + if (val >= linearEquivalentLeft) { + linearEquivalentRight = (ordinalSlope * (i + 1)) + ordinalOffset; + distance = (val - linearEquivalentLeft) / (linearEquivalentRight - linearEquivalentLeft); // something between 0 and 1 + break; + } + } + } + + // If the index is within the range of the ordinal positions, return the associated + // or interpolated value. If not, just return the value + return distance !== undefined && ordinalPositions[i] !== undefined ? + ordinalPositions[i] + (distance ? distance * (ordinalPositions[i + 1] - ordinalPositions[i]) : 0) : + val; + } + return ret; + }, + /** + * Get the ordinal positions for the entire data set. This is necessary in chart panning + * because we need to find out what points or data groups are available outside the + * visible range. When a panning operation starts, if an index for the given grouping + * does not exists, it is created and cached. This index is deleted on updated data, so + * it will be regenerated the next time a panning operation starts. + */ + getExtendedPositions: function () { + var axis = this, + chart = axis.chart, + grouping = axis.series[0].currentDataGrouping, + ordinalIndex = axis.ordinalIndex, + key = grouping ? grouping.count + grouping.unitName : 'raw', + overscroll = axis.options.overscroll, + extremes = axis.getExtremes(), + fakeAxis, + fakeSeries; + + // If this is the first time, or the ordinal index is deleted by updatedData, + // create it. + if (!ordinalIndex) { + ordinalIndex = axis.ordinalIndex = {}; + } + + + if (!ordinalIndex[key]) { + + // Create a fake axis object where the extended ordinal positions are emulated + fakeAxis = { + series: [], + chart: chart, + getExtremes: function () { + return { + min: extremes.dataMin, + max: extremes.dataMax + overscroll + }; + }, + options: { + ordinal: true + }, + val2lin: Axis.prototype.val2lin, // #2590 + ordinal2lin: Axis.prototype.ordinal2lin // #6276 + }; + + // Add the fake series to hold the full data, then apply processData to it + each(axis.series, function (series) { + fakeSeries = { + xAxis: fakeAxis, + xData: series.xData.slice(), + chart: chart, + destroyGroupedData: noop + }; + + fakeSeries.xData = fakeSeries.xData.concat(axis.getOverscrollPositions()); + + fakeSeries.options = { + dataGrouping: grouping ? { + enabled: true, + forced: true, + approximation: 'open', // doesn't matter which, use the fastest + units: [[grouping.unitName, [grouping.count]]] + } : { + enabled: false + } + }; + series.processData.apply(fakeSeries); + + + fakeAxis.series.push(fakeSeries); + }); + + // Run beforeSetTickPositions to compute the ordinalPositions + axis.beforeSetTickPositions.apply(fakeAxis); + + // Cache it + ordinalIndex[key] = fakeAxis.ordinalPositions; + } + return ordinalIndex[key]; + }, + + /** + * Get ticks for an ordinal axis within a range where points don't exist. + * It is required when overscroll is enabled. We can't base on points, + * because we may not have any, so we use approximated pointRange and + * generate these ticks between + * evenly spaced. Used in panning and navigator scrolling. + * + * @returns positions {Array} Generated ticks + * @private + */ + getOverscrollPositions: function () { + var axis = this, + extraRange = axis.options.overscroll, + distance = axis.overscrollPointsRange, + positions = [], + max = axis.dataMax; + + if (H.defined(distance)) { + // Max + pointRange because we need to scroll to the last + + positions.push(max); + + while (max <= axis.dataMax + extraRange) { + max += distance; + positions.push(max); + } + + } + + return positions; + }, + + /** + * Find the factor to estimate how wide the plot area would have been if ordinal + * gaps were included. This value is used to compute an imagined plot width in order + * to establish the data grouping interval. + * + * A real world case is the intraday-candlestick + * example. Without this logic, it would show the correct data grouping when viewing + * a range within each day, but once moving the range to include the gap between two + * days, the interval would include the cut-away night hours and the data grouping + * would be wrong. So the below method tries to compensate by identifying the most + * common point interval, in this case days. + * + * An opposite case is presented in issue #718. We have a long array of daily data, + * then one point is appended one hour after the last point. We expect the data grouping + * not to change. + * + * In the future, if we find cases where this estimation doesn't work optimally, we + * might need to add a second pass to the data grouping logic, where we do another run + * with a greater interval if the number of data groups is more than a certain fraction + * of the desired group count. + */ + getGroupIntervalFactor: function (xMin, xMax, series) { + var i, + processedXData = series.processedXData, + len = processedXData.length, + distances = [], + median, + groupIntervalFactor = this.groupIntervalFactor; + + // Only do this computation for the first series, let the other inherit it (#2416) + if (!groupIntervalFactor) { + + // Register all the distances in an array + for (i = 0; i < len - 1; i++) { + distances[i] = processedXData[i + 1] - processedXData[i]; + } + + // Sort them and find the median + distances.sort(function (a, b) { + return a - b; + }); + median = distances[Math.floor(len / 2)]; + + // Compensate for series that don't extend through the entire axis extent. #1675. + xMin = Math.max(xMin, processedXData[0]); + xMax = Math.min(xMax, processedXData[len - 1]); + + this.groupIntervalFactor = groupIntervalFactor = (len * median) / (xMax - xMin); + } + + // Return the factor needed for data grouping + return groupIntervalFactor; + }, + + /** + * Make the tick intervals closer because the ordinal gaps make the ticks spread out or cluster + */ + postProcessTickInterval: function (tickInterval) { + // Problem: http://jsfiddle.net/highcharts/FQm4E/1/ + // This is a case where this algorithm doesn't work optimally. In this case, the + // tick labels are spread out per week, but all the gaps reside within weeks. So + // we have a situation where the labels are courser than the ordinal gaps, and + // thus the tick interval should not be altered + var ordinalSlope = this.ordinalSlope, + ret; + + + if (ordinalSlope) { + if (!this.options.breaks) { + ret = tickInterval / (ordinalSlope / this.closestPointRange); + } else { + ret = this.closestPointRange || tickInterval; // #7275 + } + } else { + ret = tickInterval; + } + return ret; + } +}); + +// Record this to prevent overwriting by broken-axis module (#5979) +Axis.prototype.ordinal2lin = Axis.prototype.val2lin; + +// Extending the Chart.pan method for ordinal axes +wrap(Chart.prototype, 'pan', function (proceed, e) { + var chart = this, + xAxis = chart.xAxis[0], + overscroll = xAxis.options.overscroll, + chartX = e.chartX, + runBase = false; + + if (xAxis.options.ordinal && xAxis.series.length) { + + var mouseDownX = chart.mouseDownX, + extremes = xAxis.getExtremes(), + dataMax = extremes.dataMax, + min = extremes.min, + max = extremes.max, + trimmedRange, + hoverPoints = chart.hoverPoints, + closestPointRange = xAxis.closestPointRange || xAxis.overscrollPointsRange, + pointPixelWidth = xAxis.translationSlope * (xAxis.ordinalSlope || closestPointRange), + movedUnits = (mouseDownX - chartX) / pointPixelWidth, // how many ordinal units did we move? + extendedAxis = { ordinalPositions: xAxis.getExtendedPositions() }, // get index of all the chart's points + ordinalPositions, + searchAxisLeft, + lin2val = xAxis.lin2val, + val2lin = xAxis.val2lin, + searchAxisRight; + + if (!extendedAxis.ordinalPositions) { // we have an ordinal axis, but the data is equally spaced + runBase = true; + + } else if (Math.abs(movedUnits) > 1) { + + // Remove active points for shared tooltip + if (hoverPoints) { + each(hoverPoints, function (point) { + point.setState(); + }); + } + + if (movedUnits < 0) { + searchAxisLeft = extendedAxis; + searchAxisRight = xAxis.ordinalPositions ? xAxis : extendedAxis; + } else { + searchAxisLeft = xAxis.ordinalPositions ? xAxis : extendedAxis; + searchAxisRight = extendedAxis; + } + + // In grouped data series, the last ordinal position represents the grouped data, which is + // to the left of the real data max. If we don't compensate for this, we will be allowed + // to pan grouped data series passed the right of the plot area. + ordinalPositions = searchAxisRight.ordinalPositions; + if (dataMax > ordinalPositions[ordinalPositions.length - 1]) { + ordinalPositions.push(dataMax); + } + + // Get the new min and max values by getting the ordinal index for the current extreme, + // then add the moved units and translate back to values. This happens on the + // extended ordinal positions if the new position is out of range, else it happens + // on the current x axis which is smaller and faster. + chart.fixedRange = max - min; + trimmedRange = xAxis.toFixedRange(null, null, + lin2val.apply(searchAxisLeft, [ + val2lin.apply(searchAxisLeft, [min, true]) + movedUnits, // the new index + true // translate from index + ]), + lin2val.apply(searchAxisRight, [ + val2lin.apply(searchAxisRight, [max, true]) + movedUnits, // the new index + true // translate from index + ]) + ); + + // Apply it if it is within the available data range + if ( + trimmedRange.min >= Math.min(extremes.dataMin, min) && + trimmedRange.max <= Math.max(dataMax, max) + overscroll + ) { + xAxis.setExtremes(trimmedRange.min, trimmedRange.max, true, false, { trigger: 'pan' }); + } + + chart.mouseDownX = chartX; // set new reference for next run + css(chart.container, { cursor: 'move' }); + } + + } else { + runBase = true; + } + + // revert to the linear chart.pan version + if (runBase) { + if (overscroll) { + xAxis.max = xAxis.dataMax + overscroll; + } + // call the original function + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + } +}); + +/* **************************************************************************** + * End ordinal axis logic * + *****************************************************************************/ + +}(Highcharts)); +(function (H) { +/** + * (c) 2009-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ + +var pick = H.pick, + wrap = H.wrap, + each = H.each, + extend = H.extend, + isArray = H.isArray, + fireEvent = H.fireEvent, + Axis = H.Axis, + Series = H.Series; + +function stripArguments() { + return Array.prototype.slice.call(arguments, 1); +} + +extend(Axis.prototype, { + isInBreak: function (brk, val) { + var ret, + repeat = brk.repeat || Infinity, + from = brk.from, + length = brk.to - brk.from, + test = (val >= from ? (val - from) % repeat : repeat - ((from - val) % repeat)); + + if (!brk.inclusive) { + ret = test < length && test !== 0; + } else { + ret = test <= length; + } + return ret; + }, + + isInAnyBreak: function (val, testKeep) { + + var breaks = this.options.breaks, + i = breaks && breaks.length, + inbrk, + keep, + ret; + + + if (i) { + + while (i--) { + if (this.isInBreak(breaks[i], val)) { + inbrk = true; + if (!keep) { + keep = pick(breaks[i].showPoints, this.isXAxis ? false : true); + } + } + } + + if (inbrk && testKeep) { + ret = inbrk && !keep; + } else { + ret = inbrk; + } + } + return ret; + } +}); + +wrap(Axis.prototype, 'setTickPositions', function (proceed) { + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); + + if (this.options.breaks) { + var axis = this, + tickPositions = this.tickPositions, + info = this.tickPositions.info, + newPositions = [], + i; + + for (i = 0; i < tickPositions.length; i++) { + if (!axis.isInAnyBreak(tickPositions[i])) { + newPositions.push(tickPositions[i]); + } + } + + this.tickPositions = newPositions; + this.tickPositions.info = info; + } +}); + +wrap(Axis.prototype, 'init', function (proceed, chart, userOptions) { + var axis = this, + breaks; + // Force Axis to be not-ordinal when breaks are defined + if (userOptions.breaks && userOptions.breaks.length) { + userOptions.ordinal = false; + } + proceed.call(this, chart, userOptions); + breaks = this.options.breaks; + axis.isBroken = (isArray(breaks) && !!breaks.length); + if (axis.isBroken) { + axis.val2lin = function (val) { + var nval = val, + brk, + i; + + for (i = 0; i < axis.breakArray.length; i++) { + brk = axis.breakArray[i]; + if (brk.to <= val) { + nval -= brk.len; + } else if (brk.from >= val) { + break; + } else if (axis.isInBreak(brk, val)) { + nval -= (val - brk.from); + break; + } + } + + return nval; + }; + + axis.lin2val = function (val) { + var nval = val, + brk, + i; + + for (i = 0; i < axis.breakArray.length; i++) { + brk = axis.breakArray[i]; + if (brk.from >= nval) { + break; + } else if (brk.to < nval) { + nval += brk.len; + } else if (axis.isInBreak(brk, nval)) { + nval += brk.len; + } + } + return nval; + }; + + axis.setExtremes = function (newMin, newMax, redraw, animation, eventArguments) { + // If trying to set extremes inside a break, extend it to before and after the break ( #3857 ) + while (this.isInAnyBreak(newMin)) { + newMin -= this.closestPointRange; + } + while (this.isInAnyBreak(newMax)) { + newMax -= this.closestPointRange; + } + Axis.prototype.setExtremes.call(this, newMin, newMax, redraw, animation, eventArguments); + }; + + axis.setAxisTranslation = function (saveOld) { + Axis.prototype.setAxisTranslation.call(this, saveOld); + + var breaks = axis.options.breaks, + breakArrayT = [], // Temporary one + breakArray = [], + length = 0, + inBrk, + repeat, + min = axis.userMin || axis.min, + max = axis.userMax || axis.max, + pointRangePadding = pick(axis.pointRangePadding, 0), + start, + i; + + // Min & max check (#4247) + each(breaks, function (brk) { + repeat = brk.repeat || Infinity; + if (axis.isInBreak(brk, min)) { + min += (brk.to % repeat) - (min % repeat); + } + if (axis.isInBreak(brk, max)) { + max -= (max % repeat) - (brk.from % repeat); + } + }); + + // Construct an array holding all breaks in the axis + each(breaks, function (brk) { + start = brk.from; + repeat = brk.repeat || Infinity; + + while (start - repeat > min) { + start -= repeat; + } + while (start < min) { + start += repeat; + } + + for (i = start; i < max; i += repeat) { + breakArrayT.push({ + value: i, + move: 'in' + }); + breakArrayT.push({ + value: i + (brk.to - brk.from), + move: 'out', + size: brk.breakSize + }); + } + }); + + breakArrayT.sort(function (a, b) { + var ret; + if (a.value === b.value) { + ret = (a.move === 'in' ? 0 : 1) - (b.move === 'in' ? 0 : 1); + } else { + ret = a.value - b.value; + } + return ret; + }); + + // Simplify the breaks + inBrk = 0; + start = min; + + each(breakArrayT, function (brk) { + inBrk += (brk.move === 'in' ? 1 : -1); + + if (inBrk === 1 && brk.move === 'in') { + start = brk.value; + } + if (inBrk === 0) { + breakArray.push({ + from: start, + to: brk.value, + len: brk.value - start - (brk.size || 0) + }); + length += brk.value - start - (brk.size || 0); + } + }); + + axis.breakArray = breakArray; + + // Used with staticScale, and below, the actual axis length when + // breaks are substracted. + axis.unitLength = max - min - length + pointRangePadding; + + fireEvent(axis, 'afterBreaks'); + + if (axis.options.staticScale) { + axis.transA = axis.options.staticScale; + } else if (axis.unitLength) { + axis.transA *= (max - axis.min + pointRangePadding) / + axis.unitLength; + } + + if (pointRangePadding) { + axis.minPixelPadding = axis.transA * axis.minPointOffset; + } + + axis.min = min; + axis.max = max; + }; + } +}); + +wrap(Series.prototype, 'generatePoints', function (proceed) { + + proceed.apply(this, stripArguments(arguments)); + + var series = this, + xAxis = series.xAxis, + yAxis = series.yAxis, + points = series.points, + point, + i = points.length, + connectNulls = series.options.connectNulls, + nullGap; + + + if (xAxis && yAxis && (xAxis.options.breaks || yAxis.options.breaks)) { + while (i--) { + point = points[i]; + + nullGap = point.y === null && connectNulls === false; // respect nulls inside the break (#4275) + if (!nullGap && (xAxis.isInAnyBreak(point.x, true) || yAxis.isInAnyBreak(point.y, true))) { + points.splice(i, 1); + if (this.data[i]) { + this.data[i].destroyElements(); // removes the graphics for this point if they exist + } + } + } + } + +}); + +function drawPointsWrapped(proceed) { + proceed.apply(this); + this.drawBreaks(this.xAxis, ['x']); + this.drawBreaks(this.yAxis, pick(this.pointArrayMap, ['y'])); +} + +H.Series.prototype.drawBreaks = function (axis, keys) { + var series = this, + points = series.points, + breaks, + threshold, + eventName, + y; + + if (!axis) { + return; // #5950 + } + + each(keys, function (key) { + breaks = axis.breakArray || []; + threshold = axis.isXAxis ? axis.min : pick(series.options.threshold, axis.min); + each(points, function (point) { + y = pick(point['stack' + key.toUpperCase()], point[key]); + each(breaks, function (brk) { + eventName = false; + + if ((threshold < brk.from && y > brk.to) || (threshold > brk.from && y < brk.from)) { + eventName = 'pointBreak'; + } else if ((threshold < brk.from && y > brk.from && y < brk.to) || (threshold > brk.from && y > brk.to && y < brk.from)) { // point falls inside the break + eventName = 'pointInBreak'; + } + if (eventName) { + fireEvent(axis, eventName, { point: point, brk: brk }); + } + }); + }); + }); +}; + + +/** + * Extend getGraphPath by identifying gaps in the data so that we can draw a gap + * in the line or area. This was moved from ordinal axis module to broken axis + * module as of #5045. + */ +H.Series.prototype.gappedPath = function () { + var gapSize = this.options.gapSize, + points = this.points.slice(), + i = points.length - 1, + yAxis = this.yAxis, + xRange, + stack; + + /** + * Defines when to display a gap in the graph, together with the + * [gapUnit](plotOptions.series.gapUnit) option. + * + * In practice, this option is most often used to visualize gaps in + * time series. In a stock chart, intraday data is available for daytime + * hours, while gaps will appear in nights and weekends. + * + * @type {Number} + * @see [gapUnit](plotOptions.series.gapUnit) and [xAxis.breaks](#xAxis.breaks) + * @sample {highstock} stock/plotoptions/series-gapsize/ + * Setting the gap size to 2 introduces gaps for weekends in daily + * datasets. + * @default 0 + * @product highstock + * @apioption plotOptions.series.gapSize + */ + + /** + * Together with [gapSize](plotOptions.series.gapSize), this option defines + * where to draw gaps in the graph. + * + * When the `gapUnit` is `relative` (default), a gap size of 5 means + * that if the distance between two points is greater than five times + * that of the two closest points, the graph will be broken. + * + * When the `gapUnit` is `value`, the gap is based on absolute axis values, + * which on a datetime axis is milliseconds. Note that this may give + * unexpected results if `dataGrouping` is enabled (as it is by default), + * because if a series of points are grouped into a larger time span, the + * grouped points may have a greater distance than the absolute `gapSize`. + * This will cause the whole graph to disappear. This also applies to the + * navigator series that inherits gap options from the base series. + * + * @type {String} + * @see [gapSize](plotOptions.series.gapSize) + * @default relative + * @validvalue ["relative", "value"] + * @since 5.0.13 + * @product highstock + * @apioption plotOptions.series.gapUnit + */ + + if (gapSize && i > 0) { // #5008 + + // Gap unit is relative + if (this.options.gapUnit !== 'value') { + gapSize *= this.closestPointRange; + } + + // extension for ordinal breaks + while (i--) { + if (points[i + 1].x - points[i].x > gapSize) { + xRange = (points[i].x + points[i + 1].x) / 2; + + points.splice( // insert after this one + i + 1, + 0, + { + isNull: true, + x: xRange + } + ); + + // For stacked chart generate empty stack items, #6546 + if (this.options.stacking) { + stack = yAxis.stacks[this.stackKey][xRange] = new H.StackItem( + yAxis, + yAxis.options.stackLabels, + false, + xRange, + this.stack + ); + stack.total = 0; + } + } + } + } + + // Call base method + return this.getGraphPath(points); +}; + +wrap(H.seriesTypes.column.prototype, 'drawPoints', drawPointsWrapped); +wrap(H.Series.prototype, 'drawPoints', drawPointsWrapped); + +}(Highcharts)); +(function () { + + +}()); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var arrayMax = H.arrayMax, + arrayMin = H.arrayMin, + Axis = H.Axis, + defaultPlotOptions = H.defaultPlotOptions, + defined = H.defined, + each = H.each, + extend = H.extend, + format = H.format, + isNumber = H.isNumber, + merge = H.merge, + pick = H.pick, + Point = H.Point, + Series = H.Series, + Tooltip = H.Tooltip, + wrap = H.wrap; + +/* **************************************************************************** + * Start data grouping module * + ******************************************************************************/ + +/** + * Data grouping is the concept of sampling the data values into larger + * blocks in order to ease readability and increase performance of the + * JavaScript charts. Highstock by default applies data grouping when + * the points become closer than a certain pixel value, determined by + * the `groupPixelWidth` option. + * + * If data grouping is applied, the grouping information of grouped + * points can be read from the [Point.dataGroup](#Point.dataGroup). + * + * @product highstock + * @apioption plotOptions.series.dataGrouping + */ + +/** + * The method of approximation inside a group. When for example 30 days + * are grouped into one month, this determines what value should represent + * the group. Possible values are "average", "averages", "open", "high", + * "low", "close" and "sum". For OHLC and candlestick series the approximation + * is "ohlc" by default, which finds the open, high, low and close values + * within all the grouped data. For ranges, the approximation is "range", + * which finds the low and high values. For multi-dimensional data, + * like ranges and OHLC, "averages" will compute the average for each + * dimension. + * + * Custom aggregate methods can be added by assigning a callback function + * as the approximation. This function takes a numeric array as the + * argument and should return a single numeric value or `null`. Note + * that the numeric array will never contain null values, only true + * numbers. Instead, if null values are present in the raw data, the + * numeric array will have an `.hasNulls` property set to `true`. For + * single-value data sets the data is available in the first argument + * of the callback function. For OHLC data sets, all the open values + * are in the first argument, all high values in the second etc. + * + * Since v4.2.7, grouping meta data is available in the approximation + * callback from `this.dataGroupInfo`. It can be used to extract information + * from the raw data. + * + * Defaults to `average` for line-type series, `sum` for columns, `range` + * for range series and `ohlc` for OHLC and candlestick. + * + * @validvalue ["average", "averages", "open", "high", "low", "close", "sum"] + * @type {String|Function} + * @sample {highstock} stock/plotoptions/series-datagrouping-approximation Approximation callback with custom data + * @product highstock + * @apioption plotOptions.series.dataGrouping.approximation + */ + +/** + * Datetime formats for the header of the tooltip in a stock chart. + * The format can vary within a chart depending on the currently selected + * time range and the current data grouping. + * + * The default formats are: + * + *
{
+ *     millisecond: ['%A, %b %e, %H:%M:%S.%L', '%A, %b %e, %H:%M:%S.%L', '-%H:%M:%S.%L'],
+ *     second: ['%A, %b %e, %H:%M:%S', '%A, %b %e, %H:%M:%S', '-%H:%M:%S'],
+ *     minute: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
+ *     hour: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'],
+ *     day: ['%A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
+ *     week: ['Week from %A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'],
+ *     month: ['%B %Y', '%B', '-%B %Y'],
+ *     year: ['%Y', '%Y', '-%Y']
+ * }
+ * + * For each of these array definitions, the first item is the format + * used when the active time span is one unit. For instance, if the + * current data applies to one week, the first item of the week array + * is used. The second and third items are used when the active time + * span is more than two units. For instance, if the current data applies + * to two weeks, the second and third item of the week array are used, + * and applied to the start and end date of the time span. + * + * @type {Object} + * @product highstock + * @apioption plotOptions.series.dataGrouping.dateTimeLabelFormats + */ + +/** + * Enable or disable data grouping. + * + * @type {Boolean} + * @default true + * @product highstock + * @apioption plotOptions.series.dataGrouping.enabled + */ + +/** + * When data grouping is forced, it runs no matter how small the intervals + * are. This can be handy for example when the sum should be calculated + * for values appearing at random times within each hour. + * + * @type {Boolean} + * @default false + * @product highstock + * @apioption plotOptions.series.dataGrouping.forced + */ + +/** + * The approximate pixel width of each group. If for example a series + * with 30 points is displayed over a 600 pixel wide plot area, no grouping + * is performed. If however the series contains so many points that + * the spacing is less than the groupPixelWidth, Highcharts will try + * to group it into appropriate groups so that each is more or less + * two pixels wide. If multiple series with different group pixel widths + * are drawn on the same x axis, all series will take the greatest width. + * For example, line series have 2px default group width, while column + * series have 10px. If combined, both the line and the column will + * have 10px by default. + * + * @type {Number} + * @default 2 + * @product highstock + * @apioption plotOptions.series.dataGrouping.groupPixelWidth + */ + +/** + * Normally, a group is indexed by the start of that group, so for example + * when 30 daily values are grouped into one month, that month's x value + * will be the 1st of the month. This apparently shifts the data to + * the left. When the smoothed option is true, this is compensated for. + * The data is shifted to the middle of the group, and min and max + * values are preserved. Internally, this is used in the Navigator series. + * + * @type {Boolean} + * @default false + * @product highstock + * @apioption plotOptions.series.dataGrouping.smoothed + */ + +/** + * An array determining what time intervals the data is allowed to be + * grouped to. Each array item is an array where the first value is + * the time unit and the second value another array of allowed multiples. + * Defaults to: + * + *
units: [[
+ *     'millisecond', // unit name
+ *     [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples
+ * ], [
+ *     'second',
+ *     [1, 2, 5, 10, 15, 30]
+ * ], [
+ *     'minute',
+ *     [1, 2, 5, 10, 15, 30]
+ * ], [
+ *     'hour',
+ *     [1, 2, 3, 4, 6, 8, 12]
+ * ], [
+ *     'day',
+ *     [1]
+ * ], [
+ *     'week',
+ *     [1]
+ * ], [
+ *     'month',
+ *     [1, 3, 6]
+ * ], [
+ *     'year',
+ *     null
+ * ]]
+ * + * @type {Array} + * @product highstock + * @apioption plotOptions.series.dataGrouping.units + */ + +/** + * The approximate pixel width of each group. If for example a series + * with 30 points is displayed over a 600 pixel wide plot area, no grouping + * is performed. If however the series contains so many points that + * the spacing is less than the groupPixelWidth, Highcharts will try + * to group it into appropriate groups so that each is more or less + * two pixels wide. Defaults to `10`. + * + * @type {Number} + * @sample {highstock} stock/plotoptions/series-datagrouping-grouppixelwidth/ + * Two series with the same data density but different groupPixelWidth + * @default 10 + * @product highstock + * @apioption plotOptions.column.dataGrouping.groupPixelWidth + */ + +var seriesProto = Series.prototype, + baseProcessData = seriesProto.processData, + baseGeneratePoints = seriesProto.generatePoints, + + /** + * + */ + commonOptions = { + approximation: 'average', // average, open, high, low, close, sum + // enabled: null, // (true for stock charts, false for basic), + // forced: undefined, + groupPixelWidth: 2, + // the first one is the point or start value, the second is the start value if we're dealing with range, + // the third one is the end value if dealing with a range + dateTimeLabelFormats: { + millisecond: ['%A, %b %e, %H:%M:%S.%L', '%A, %b %e, %H:%M:%S.%L', '-%H:%M:%S.%L'], + second: ['%A, %b %e, %H:%M:%S', '%A, %b %e, %H:%M:%S', '-%H:%M:%S'], + minute: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'], + hour: ['%A, %b %e, %H:%M', '%A, %b %e, %H:%M', '-%H:%M'], + day: ['%A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'], + week: ['Week from %A, %b %e, %Y', '%A, %b %e', '-%A, %b %e, %Y'], + month: ['%B %Y', '%B', '-%B %Y'], + year: ['%Y', '%Y', '-%Y'] + } + // smoothed = false, // enable this for navigator series only + }, + + specificOptions = { // extends common options + line: {}, + spline: {}, + area: {}, + areaspline: {}, + column: { + approximation: 'sum', + groupPixelWidth: 10 + }, + arearange: { + approximation: 'range' + }, + areasplinerange: { + approximation: 'range' + }, + columnrange: { + approximation: 'range', + groupPixelWidth: 10 + }, + candlestick: { + approximation: 'ohlc', + groupPixelWidth: 10 + }, + ohlc: { + approximation: 'ohlc', + groupPixelWidth: 5 + } + }, + + // units are defined in a separate array to allow complete overriding in case of a user option + defaultDataGroupingUnits = H.defaultDataGroupingUnits = [ + [ + 'millisecond', // unit name + [1, 2, 5, 10, 20, 25, 50, 100, 200, 500] // allowed multiples + ], [ + 'second', + [1, 2, 5, 10, 15, 30] + ], [ + 'minute', + [1, 2, 5, 10, 15, 30] + ], [ + 'hour', + [1, 2, 3, 4, 6, 8, 12] + ], [ + 'day', + [1] + ], [ + 'week', + [1] + ], [ + 'month', + [1, 3, 6] + ], [ + 'year', + null + ] + ], + + + /** + * Define the available approximation types. The data grouping + * approximations takes an array or numbers as the first parameter. In case + * of ohlc, four arrays are sent in as four parameters. Each array consists + * only of numbers. In case null values belong to the group, the property + * .hasNulls will be set to true on the array. + */ + approximations = H.approximations = { + sum: function (arr) { + var len = arr.length, + ret; + + // 1. it consists of nulls exclusively + if (!len && arr.hasNulls) { + ret = null; + // 2. it has a length and real values + } else if (len) { + ret = 0; + while (len--) { + ret += arr[len]; + } + } + // 3. it has zero length, so just return undefined + // => doNothing() + + return ret; + }, + average: function (arr) { + var len = arr.length, + ret = approximations.sum(arr); + + // If we have a number, return it divided by the length. If not, + // return null or undefined based on what the sum method finds. + if (isNumber(ret) && len) { + ret = ret / len; + } + + return ret; + }, + // The same as average, but for series with multiple values, like area + // ranges. + averages: function () { // #5479 + var ret = []; + + each(arguments, function (arr) { + ret.push(approximations.average(arr)); + }); + + // Return undefined when first elem. is undefined and let + // sum method handle null (#7377) + return ret[0] === undefined ? undefined : ret; + }, + open: function (arr) { + return arr.length ? arr[0] : (arr.hasNulls ? null : undefined); + }, + high: function (arr) { + return arr.length ? arrayMax(arr) : (arr.hasNulls ? null : undefined); + }, + low: function (arr) { + return arr.length ? arrayMin(arr) : (arr.hasNulls ? null : undefined); + }, + close: function (arr) { + return arr.length ? arr[arr.length - 1] : (arr.hasNulls ? null : undefined); + }, + // ohlc and range are special cases where a multidimensional array is input and an array is output + ohlc: function (open, high, low, close) { + open = approximations.open(open); + high = approximations.high(high); + low = approximations.low(low); + close = approximations.close(close); + + if (isNumber(open) || isNumber(high) || isNumber(low) || isNumber(close)) { + return [open, high, low, close]; + } + // else, return is undefined + }, + range: function (low, high) { + low = approximations.low(low); + high = approximations.high(high); + + if (isNumber(low) || isNumber(high)) { + return [low, high]; + } else if (low === null && high === null) { + return null; + } + // else, return is undefined + } + }; + +/** + * Takes parallel arrays of x and y data and groups the data into intervals + * defined by groupPositions, a collection of starting x values for each group. + */ +seriesProto.groupData = function (xData, yData, groupPositions, approximation) { + var series = this, + data = series.data, + dataOptions = series.options.data, + groupedXData = [], + groupedYData = [], + groupMap = [], + dataLength = xData.length, + pointX, + pointY, + groupedY, + // when grouping the fake extended axis for panning, + // we don't need to consider y + handleYData = !!yData, + values = [], + approximationFn = typeof approximation === 'function' ? + approximation : + approximations[approximation] || + // if the approximation is not found use default series type + // approximation (#2914) + ( + specificOptions[series.type] && + approximations[specificOptions[series.type].approximation] + ) || approximations[commonOptions.approximation], + pointArrayMap = series.pointArrayMap, + pointArrayMapLength = pointArrayMap && pointArrayMap.length, + pos = 0, + start = 0, + valuesLen, + i, j; + + // Calculate values array size from pointArrayMap length + if (pointArrayMapLength) { + each(pointArrayMap, function () { + values.push([]); + }); + } else { + values.push([]); + } + valuesLen = pointArrayMapLength || 1; + + // Start with the first point within the X axis range (#2696) + for (i = 0; i <= dataLength; i++) { + if (xData[i] >= groupPositions[0]) { + break; + } + } + + for (i; i <= dataLength; i++) { + + // when a new group is entered, summarize and initiate + // the previous group + while (( + groupPositions[pos + 1] !== undefined && + xData[i] >= groupPositions[pos + 1] + ) || i === dataLength) { // get the last group + + // get group x and y + pointX = groupPositions[pos]; + series.dataGroupInfo = { start: start, length: values[0].length }; + groupedY = approximationFn.apply(series, values); + + // push the grouped data + if (groupedY !== undefined) { + groupedXData.push(pointX); + groupedYData.push(groupedY); + groupMap.push(series.dataGroupInfo); + } + + // reset the aggregate arrays + start = i; + for (j = 0; j < valuesLen; j++) { + values[j].length = 0; // faster than values[j] = [] + values[j].hasNulls = false; + } + + // Advance on the group positions + pos += 1; + + // don't loop beyond the last group + if (i === dataLength) { + break; + } + } + + // break out + if (i === dataLength) { + break; + } + + // for each raw data point, push it to an array that contains all values + // for this specific group + if (pointArrayMap) { + + var index = series.cropStart + i, + point = (data && data[index]) || + series.pointClass.prototype.applyOptions.apply({ + series: series + }, [dataOptions[index]]), + val; + + for (j = 0; j < pointArrayMapLength; j++) { + val = point[pointArrayMap[j]]; + if (isNumber(val)) { + values[j].push(val); + } else if (val === null) { + values[j].hasNulls = true; + } + } + + } else { + pointY = handleYData ? yData[i] : null; + + if (isNumber(pointY)) { + values[0].push(pointY); + } else if (pointY === null) { + values[0].hasNulls = true; + } + } + } + + return [groupedXData, groupedYData, groupMap]; +}; + +/** + * Extend the basic processData method, that crops the data to the current zoom + * range, with data grouping logic. + */ +seriesProto.processData = function () { + var series = this, + chart = series.chart, + options = series.options, + dataGroupingOptions = options.dataGrouping, + groupingEnabled = series.allowDG !== false && dataGroupingOptions && + pick(dataGroupingOptions.enabled, chart.options.isStock), + visible = series.visible || !chart.options.chart.ignoreHiddenSeries, + hasGroupedData, + skip, + lastDataGrouping = this.currentDataGrouping, + currentDataGrouping; + + // run base method + series.forceCrop = groupingEnabled; // #334 + series.groupPixelWidth = null; // #2110 + series.hasProcessed = true; // #2692 + + // skip if processData returns false or if grouping is disabled (in that order) + skip = baseProcessData.apply(series, arguments) === false || !groupingEnabled; + if (!skip) { + series.destroyGroupedData(); + + var i, + processedXData = series.processedXData, + processedYData = series.processedYData, + plotSizeX = chart.plotSizeX, + xAxis = series.xAxis, + ordinal = xAxis.options.ordinal, + groupPixelWidth = series.groupPixelWidth = xAxis.getGroupPixelWidth && xAxis.getGroupPixelWidth(); + + // Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth + if (groupPixelWidth) { + hasGroupedData = true; + + series.isDirty = true; // force recreation of point instances in series.translate, #5699 + series.points = null; // #6709 + + var extremes = xAxis.getExtremes(), + xMin = extremes.min, + xMax = extremes.max, + groupIntervalFactor = (ordinal && xAxis.getGroupIntervalFactor(xMin, xMax, series)) || 1, + interval = (groupPixelWidth * (xMax - xMin) / plotSizeX) * groupIntervalFactor, + groupPositions = xAxis.getTimeTicks( + xAxis.normalizeTimeTickInterval(interval, dataGroupingOptions.units || defaultDataGroupingUnits), + Math.min(xMin, processedXData[0]), // Processed data may extend beyond axis (#4907) + Math.max(xMax, processedXData[processedXData.length - 1]), + xAxis.options.startOfWeek, + processedXData, + series.closestPointRange + ), + groupedData = seriesProto.groupData.apply(series, [processedXData, processedYData, groupPositions, dataGroupingOptions.approximation]), + groupedXData = groupedData[0], + groupedYData = groupedData[1]; + + // prevent the smoothed data to spill out left and right, and make + // sure data is not shifted to the left + if (dataGroupingOptions.smoothed && groupedXData.length) { + i = groupedXData.length - 1; + groupedXData[i] = Math.min(groupedXData[i], xMax); + while (i-- && i > 0) { + groupedXData[i] += interval / 2; + } + groupedXData[0] = Math.max(groupedXData[0], xMin); + } + + // record what data grouping values were used + currentDataGrouping = groupPositions.info; + series.closestPointRange = groupPositions.info.totalRange; + series.groupMap = groupedData[2]; + + // Make sure the X axis extends to show the first group (#2533) + // But only for visible series (#5493, #6393) + if (defined(groupedXData[0]) && groupedXData[0] < xAxis.dataMin && visible) { + if (xAxis.min === xAxis.dataMin) { + xAxis.min = groupedXData[0]; + } + xAxis.dataMin = groupedXData[0]; + } + + // set series props + series.processedXData = groupedXData; + series.processedYData = groupedYData; + } else { + series.groupMap = null; + } + series.hasGroupedData = hasGroupedData; + series.currentDataGrouping = currentDataGrouping; + + series.preventGraphAnimation = + (lastDataGrouping && lastDataGrouping.totalRange) !== + (currentDataGrouping && currentDataGrouping.totalRange); + } +}; + +/** + * Destroy the grouped data points. #622, #740 + */ +seriesProto.destroyGroupedData = function () { + + var groupedData = this.groupedData; + + // clear previous groups + each(groupedData || [], function (point, i) { + if (point) { + groupedData[i] = point.destroy ? point.destroy() : null; + } + }); + this.groupedData = null; +}; + +/** + * Override the generatePoints method by adding a reference to grouped data + */ +seriesProto.generatePoints = function () { + + baseGeneratePoints.apply(this); + + // record grouped data in order to let it be destroyed the next time processData runs + this.destroyGroupedData(); // #622 + this.groupedData = this.hasGroupedData ? this.points : null; +}; + +/** + * Override point prototype to throw a warning when trying to update grouped points + */ +wrap(Point.prototype, 'update', function (proceed) { + if (this.dataGroup) { + H.error(24); + } else { + proceed.apply(this, [].slice.call(arguments, 1)); + } +}); + +/** + * Extend the original method, make the tooltip's header reflect the grouped range + */ +wrap(Tooltip.prototype, 'tooltipFooterHeaderFormatter', function (proceed, labelConfig, isFooter) { + var tooltip = this, + time = this.chart.time, + series = labelConfig.series, + options = series.options, + tooltipOptions = series.tooltipOptions, + dataGroupingOptions = options.dataGrouping, + xDateFormat = tooltipOptions.xDateFormat, + xDateFormatEnd, + xAxis = series.xAxis, + currentDataGrouping, + dateTimeLabelFormats, + labelFormats, + formattedKey; + + // apply only to grouped series + if (xAxis && xAxis.options.type === 'datetime' && dataGroupingOptions && isNumber(labelConfig.key)) { + + // set variables + currentDataGrouping = series.currentDataGrouping; + dateTimeLabelFormats = dataGroupingOptions.dateTimeLabelFormats; + + // if we have grouped data, use the grouping information to get the right format + if (currentDataGrouping) { + labelFormats = dateTimeLabelFormats[currentDataGrouping.unitName]; + if (currentDataGrouping.count === 1) { + xDateFormat = labelFormats[0]; + } else { + xDateFormat = labelFormats[1]; + xDateFormatEnd = labelFormats[2]; + } + // if not grouped, and we don't have set the xDateFormat option, get the best fit, + // so if the least distance between points is one minute, show it, but if the + // least distance is one day, skip hours and minutes etc. + } else if (!xDateFormat && dateTimeLabelFormats) { + xDateFormat = tooltip.getXDateFormat(labelConfig, tooltipOptions, xAxis); + } + + // now format the key + formattedKey = time.dateFormat(xDateFormat, labelConfig.key); + if (xDateFormatEnd) { + formattedKey += time.dateFormat( + xDateFormatEnd, + labelConfig.key + currentDataGrouping.totalRange - 1 + ); + } + + // return the replaced format + return format(tooltipOptions[(isFooter ? 'footer' : 'header') + 'Format'], { + point: extend(labelConfig.point, { key: formattedKey }), + series: series + }, time); + + } + + // else, fall back to the regular formatter + return proceed.call(tooltip, labelConfig, isFooter); +}); + +/** + * Destroy grouped data on series destroy + */ +wrap(seriesProto, 'destroy', function (proceed) { + this.destroyGroupedData(); + proceed.call(this); +}); + + +// Handle default options for data grouping. This must be set at runtime because some series types are +// defined after this. +wrap(seriesProto, 'setOptions', function (proceed, itemOptions) { + + var options = proceed.call(this, itemOptions), + type = this.type, + plotOptions = this.chart.options.plotOptions, + defaultOptions = defaultPlotOptions[type].dataGrouping; + + if (specificOptions[type]) { // #1284 + if (!defaultOptions) { + defaultOptions = merge(commonOptions, specificOptions[type]); + } + + options.dataGrouping = merge( + defaultOptions, + plotOptions.series && plotOptions.series.dataGrouping, // #1228 + plotOptions[type].dataGrouping, // Set by the StockChart constructor + itemOptions.dataGrouping + ); + } + + if (this.chart.options.isStock) { + this.requireSorting = true; + } + + return options; +}); + + +/** + * When resetting the scale reset the hasProccessed flag to avoid taking previous data grouping + * of neighbour series into accound when determining group pixel width (#2692). + */ +wrap(Axis.prototype, 'setScale', function (proceed) { + proceed.call(this); + each(this.series, function (series) { + series.hasProcessed = false; + }); +}); + +/** + * Get the data grouping pixel width based on the greatest defined individual width + * of the axis' series, and if whether one of the axes need grouping. + */ +Axis.prototype.getGroupPixelWidth = function () { + + var series = this.series, + len = series.length, + i, + groupPixelWidth = 0, + doGrouping = false, + dataLength, + dgOptions; + + // If multiple series are compared on the same x axis, give them the same + // group pixel width (#334) + i = len; + while (i--) { + dgOptions = series[i].options.dataGrouping; + if (dgOptions) { + groupPixelWidth = Math.max(groupPixelWidth, dgOptions.groupPixelWidth); + + } + } + + // If one of the series needs grouping, apply it to all (#1634) + i = len; + while (i--) { + dgOptions = series[i].options.dataGrouping; + + if (dgOptions && series[i].hasProcessed) { // #2692 + + dataLength = (series[i].processedXData || series[i].data).length; + + // Execute grouping if the amount of points is greater than the limit defined in groupPixelWidth + if (series[i].groupPixelWidth || dataLength > (this.chart.plotSizeX / groupPixelWidth) || (dataLength && dgOptions.forced)) { + doGrouping = true; + } + } + } + + return doGrouping ? groupPixelWidth : 0; +}; + +/** + * Highstock only. Force data grouping on all the axis' series. + * + * @param {SeriesDatagroupingOptions} [dataGrouping] + * A `dataGrouping` configuration. Use `false` to disable data grouping + * dynamically. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart or wait for a later call to {@link + * Chart#redraw}. + * + * @function setDataGrouping + * @memberOf Axis.prototype + */ +Axis.prototype.setDataGrouping = function (dataGrouping, redraw) { + var i; + + redraw = pick(redraw, true); + + if (!dataGrouping) { + dataGrouping = { + forced: false, + units: null + }; + } + + // Axis is instantiated, update all series + if (this instanceof Axis) { + i = this.series.length; + while (i--) { + this.series[i].update({ + dataGrouping: dataGrouping + }, false); + } + + // Axis not yet instanciated, alter series options + } else { + each(this.chart.options.series, function (seriesOptions) { + seriesOptions.dataGrouping = dataGrouping; + }, false); + } + + if (redraw) { + this.chart.redraw(); + } +}; + + + +/* **************************************************************************** + * End data grouping module * + ******************************************************************************/ + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var each = H.each, + Point = H.Point, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; + +/** + * The ohlc series type. + * + * @constructor seriesTypes.ohlc + * @augments seriesTypes.column + */ +/** + * An OHLC chart is a style of financial chart used to describe price + * movements over time. It displays open, high, low and close values per data + * point. + * + * @sample stock/demo/ohlc/ OHLC chart + * @extends {plotOptions.column} + * @excluding borderColor,borderRadius,borderWidth,crisp + * @product highstock + * @optionparent plotOptions.ohlc + */ +seriesType('ohlc', 'column', { + + /** + * The approximate pixel width of each group. If for example a series + * with 30 points is displayed over a 600 pixel wide plot area, no grouping + * is performed. If however the series contains so many points that + * the spacing is less than the groupPixelWidth, Highcharts will try + * to group it into appropriate groups so that each is more or less + * two pixels wide. Defaults to `5`. + * + * @type {Number} + * @default 5 + * @product highstock + * @apioption plotOptions.ohlc.dataGrouping.groupPixelWidth + */ + + /** + * The pixel width of the line/border. Defaults to `1`. + * + * @type {Number} + * @sample {highstock} stock/plotoptions/ohlc-linewidth/ + * A greater line width + * @default 1 + * @product highstock + */ + lineWidth: 1, + + tooltip: { + + + pointFormat: '\u25CF {series.name}
' + // eslint-disable-line max-len + 'Open: {point.open}
' + + 'High: {point.high}
' + + 'Low: {point.low}
' + + 'Close: {point.close}
' + + }, + + threshold: null, + + + states: { + + /** + * @extends plotOptions.column.states.hover + * @product highstock + */ + hover: { + + /** + * The pixel width of the line representing the OHLC point. + * + * @type {Number} + * @default 3 + * @product highstock + */ + lineWidth: 3 + } + }, + + + /** + * Line color for up points. + * + * @type {Color} + * @product highstock + * @apioption plotOptions.ohlc.upColor + */ + + + + stickyTracking: true + +}, /** @lends seriesTypes.ohlc */ { + directTouch: false, + pointArrayMap: ['open', 'high', 'low', 'close'], + toYData: function (point) { // return a plain array for speedy calculation + return [point.open, point.high, point.low, point.close]; + }, + pointValKey: 'close', + + + pointAttrToOptions: { + 'stroke': 'color', + 'stroke-width': 'lineWidth' + }, + + /** + * Postprocess mapping between options and SVG attributes + */ + pointAttribs: function (point, state) { + var attribs = seriesTypes.column.prototype.pointAttribs.call( + this, + point, + state + ), + options = this.options; + + delete attribs.fill; + + if ( + !point.options.color && + options.upColor && + point.open < point.close + ) { + attribs.stroke = options.upColor; + } + + return attribs; + }, + + + /** + * Translate data points from raw values x and y to plotX and plotY + */ + translate: function () { + var series = this, + yAxis = series.yAxis, + hasModifyValue = !!series.modifyValue, + translated = [ + 'plotOpen', + 'plotHigh', + 'plotLow', + 'plotClose', + 'yBottom' + ]; // translate OHLC for + + seriesTypes.column.prototype.translate.apply(series); + + // Do the translation + each(series.points, function (point) { + each( + [point.open, point.high, point.low, point.close, point.low], + function (value, i) { + if (value !== null) { + if (hasModifyValue) { + value = series.modifyValue(value); + } + point[translated[i]] = yAxis.toPixels(value, true); + } + } + ); + + // Align the tooltip to the high value to avoid covering the point + point.tooltipPos[1] = + point.plotHigh + yAxis.pos - series.chart.plotTop; + }); + }, + + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, + points = series.points, + chart = series.chart; + + + each(points, function (point) { + var plotOpen, + plotClose, + crispCorr, + halfWidth, + path, + graphic = point.graphic, + crispX, + isNew = !graphic; + + if (point.plotY !== undefined) { + + // Create and/or update the graphic + if (!graphic) { + point.graphic = graphic = chart.renderer.path() + .add(series.group); + } + + + graphic.attr( + series.pointAttribs(point, point.selected && 'select') + ); // #3897 + + + // crisp vector coordinates + crispCorr = (graphic.strokeWidth() % 2) / 2; + crispX = Math.round(point.plotX) - crispCorr; // #2596 + halfWidth = Math.round(point.shapeArgs.width / 2); + + // the vertical stem + path = [ + 'M', + crispX, Math.round(point.yBottom), + 'L', + crispX, Math.round(point.plotHigh) + ]; + + // open + if (point.open !== null) { + plotOpen = Math.round(point.plotOpen) + crispCorr; + path.push( + 'M', + crispX, + plotOpen, + 'L', + crispX - halfWidth, + plotOpen + ); + } + + // close + if (point.close !== null) { + plotClose = Math.round(point.plotClose) + crispCorr; + path.push( + 'M', + crispX, + plotClose, + 'L', + crispX + halfWidth, + plotClose + ); + } + + graphic[isNew ? 'attr' : 'animate']({ d: path }) + .addClass(point.getClassName(), true); + + } + + + }); + + }, + + animate: null // Disable animation + +/** + * @constructor seriesTypes.ohlc.prototype.pointClass + * @extends {Point} + */ +}, /** @lends seriesTypes.ohlc.prototype.pointClass.prototype */ { + /** + * Extend the parent method by adding up or down to the class name. + */ + getClassName: function () { + return Point.prototype.getClassName.call(this) + + ( + this.open < this.close ? + ' highcharts-point-up' : + ' highcharts-point-down' + ); + } +}); + +/** + * A `ohlc` series. If the [type](#series.ohlc.type) option is not + * specified, it is inherited from [chart.type](#chart.type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * ohlc](#plotOptions.ohlc). + * + * @type {Object} + * @extends series,plotOptions.ohlc + * @excluding dataParser,dataURL + * @product highstock + * @apioption series.ohlc + */ + +/** + * An array of data points for the series. For the `ohlc` series type, + * points can be given in the following ways: + * + * 1. An array of arrays with 5 or 4 values. In this case, the values + * correspond to `x,open,high,low,close`. If the first value is a string, + * it is applied as the name of the point, and the `x` value is inferred. + * The `x` value can also be omitted, in which case the inner arrays + * should be of length 4\. Then the `x` value is automatically calculated, + * either starting at 0 and incremented by 1, or from `pointStart` + * and `pointInterval` given in the series options. + * + * ```js + * data: [ + * [0, 6, 5, 6, 7], + * [1, 9, 4, 8, 2], + * [2, 6, 3, 4, 10] + * ] + * ``` + * + * 2. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.ohlc.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * x: 1, + * open: 3, + * high: 4, + * low: 5, + * close: 2, + * name: "Point2", + * color: "#00FF00" + * }, { + * x: 1, + * open: 4, + * high: 3, + * low: 6, + * close: 7, + * name: "Point1", + * color: "#FF00FF" + * }] + * ``` + * + * @type {Array} + * @extends series.arearange.data + * @excluding y,marker + * @product highstock + * @apioption series.ohlc.data + */ + +/** + * The closing value of each data point. + * + * @type {Number} + * @product highstock + * @apioption series.ohlc.data.close + */ + +/** + * The opening value of each data point. + * + * @type {Number} + * @product highstock + * @apioption series.ohlc.data.open + */ + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var defaultPlotOptions = H.defaultPlotOptions, + each = H.each, + merge = H.merge, + seriesType = H.seriesType, + seriesTypes = H.seriesTypes; + +/** + * A candlestick chart is a style of financial chart used to describe price + * movements over time. + * + * @sample stock/demo/candlestick/ Candlestick chart + * + * @extends {plotOptions.ohlc} + * @excluding borderColor,borderRadius,borderWidth + * @product highstock + * @optionparent plotOptions.candlestick + */ +var candlestickOptions = { + + states: { + + /** + * @extends plotOptions.column.states.hover + * @product highstock + */ + hover: { + + /** + * The pixel width of the line/border around the candlestick. + * + * @type {Number} + * @default 2 + * @product highstock + */ + lineWidth: 2 + } + }, + + /** + * @extends {plotOptions.ohlc.tooltip} + */ + tooltip: defaultPlotOptions.ohlc.tooltip, + + threshold: null, + + + /** + * The color of the line/border of the candlestick. + * + * In styled mode, the line stroke can be set with the `.highcharts- + * candlestick-series .highcahrts-point` rule. + * + * @type {Color} + * @see [upLineColor](#plotOptions.candlestick.upLineColor) + * @sample {highstock} stock/plotoptions/candlestick-linecolor/ + * Candlestick line colors + * @default #000000 + * @product highstock + */ + lineColor: '#000000', + + /** + * The pixel width of the candlestick line/border. Defaults to `1`. + * + * + * In styled mode, the line stroke width can be set with the `. + * highcharts-candlestick-series .highcahrts-point` rule. + * + * @type {Number} + * @default 1 + * @product highstock + */ + lineWidth: 1, + + /** + * The fill color of the candlestick when values are rising. + * + * In styled mode, the up color can be set with the `.highcharts- + * candlestick-series .highcharts-point-up` rule. + * + * @type {Color} + * @sample {highstock} stock/plotoptions/candlestick-color/ Custom colors + * @sample {highstock} highcharts/css/candlestick/ Colors in styled mode + * @default #ffffff + * @product highstock + */ + upColor: '#ffffff', + + stickyTracking: true + + /** + * The specific line color for up candle sticks. The default is to inherit + * the general `lineColor` setting. + * + * @type {Color} + * @sample {highstock} stock/plotoptions/candlestick-linecolor/ Candlestick line colors + * @default null + * @since 1.3.6 + * @product highstock + * @apioption plotOptions.candlestick.upLineColor + */ + + + /** + * @default ohlc + * @apioption plotOptions.candlestick.dataGrouping.approximation + */ + +}; + +/** + * The candlestick series type. + * + * @constructor seriesTypes.candlestick + * @augments seriesTypes.ohlc + */ +seriesType('candlestick', 'ohlc', merge( + defaultPlotOptions.column, + candlestickOptions +), /** @lends seriesTypes.candlestick */ { + + /** + * Postprocess mapping between options and SVG attributes + */ + pointAttribs: function (point, state) { + var attribs = seriesTypes.column.prototype.pointAttribs.call(this, point, state), + options = this.options, + isUp = point.open < point.close, + stroke = options.lineColor || this.color, + stateOptions; + + attribs['stroke-width'] = options.lineWidth; + + attribs.fill = point.options.color || (isUp ? (options.upColor || this.color) : this.color); + attribs.stroke = point.lineColor || (isUp ? (options.upLineColor || stroke) : stroke); + + // Select or hover states + if (state) { + stateOptions = options.states[state]; + attribs.fill = stateOptions.color || attribs.fill; + attribs.stroke = stateOptions.lineColor || attribs.stroke; + attribs['stroke-width'] = + stateOptions.lineWidth || attribs['stroke-width']; + } + + + return attribs; + }, + + /** + * Draw the data points + */ + drawPoints: function () { + var series = this, + points = series.points, + chart = series.chart; + + + each(points, function (point) { + + var graphic = point.graphic, + plotOpen, + plotClose, + topBox, + bottomBox, + hasTopWhisker, + hasBottomWhisker, + crispCorr, + crispX, + path, + halfWidth, + isNew = !graphic; + + if (point.plotY !== undefined) { + + if (!graphic) { + point.graphic = graphic = chart.renderer.path() + .add(series.group); + } + + + graphic + .attr(series.pointAttribs(point, point.selected && 'select')) // #3897 + .shadow(series.options.shadow); + + + // Crisp vector coordinates + crispCorr = (graphic.strokeWidth() % 2) / 2; + crispX = Math.round(point.plotX) - crispCorr; // #2596 + plotOpen = point.plotOpen; + plotClose = point.plotClose; + topBox = Math.min(plotOpen, plotClose); + bottomBox = Math.max(plotOpen, plotClose); + halfWidth = Math.round(point.shapeArgs.width / 2); + hasTopWhisker = Math.round(topBox) !== Math.round(point.plotHigh); + hasBottomWhisker = bottomBox !== point.yBottom; + topBox = Math.round(topBox) + crispCorr; + bottomBox = Math.round(bottomBox) + crispCorr; + + // Create the path. Due to a bug in Chrome 49, the path is first instanciated + // with no values, then the values pushed. For unknown reasons, instanciated + // the path array with all the values would lead to a crash when updating + // frequently (#5193). + path = []; + path.push( + 'M', + crispX - halfWidth, bottomBox, + 'L', + crispX - halfWidth, topBox, + 'L', + crispX + halfWidth, topBox, + 'L', + crispX + halfWidth, bottomBox, + 'Z', // Use a close statement to ensure a nice rectangle #2602 + 'M', + crispX, topBox, + 'L', + crispX, hasTopWhisker ? Math.round(point.plotHigh) : topBox, // #460, #2094 + 'M', + crispX, bottomBox, + 'L', + crispX, hasBottomWhisker ? Math.round(point.yBottom) : bottomBox // #460, #2094 + ); + + graphic[isNew ? 'attr' : 'animate']({ d: path }) + .addClass(point.getClassName(), true); + + } + }); + + } + + +}); + +/** + * A `candlestick` series. If the [type](#series.candlestick.type) + * option is not specified, it is inherited from [chart.type](#chart. + * type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * candlestick](#plotOptions.candlestick). + * + * @type {Object} + * @extends series,plotOptions.candlestick + * @excluding dataParser,dataURL + * @product highstock + * @apioption series.candlestick + */ + +/** + * An array of data points for the series. For the `candlestick` series + * type, points can be given in the following ways: + * + * 1. An array of arrays with 5 or 4 values. In this case, the values + * correspond to `x,open,high,low,close`. If the first value is a string, + * it is applied as the name of the point, and the `x` value is inferred. + * The `x` value can also be omitted, in which case the inner arrays + * should be of length 4\. Then the `x` value is automatically calculated, + * either starting at 0 and incremented by 1, or from `pointStart` + * and `pointInterval` given in the series options. + * + * ```js + * data: [ + * [0, 7, 2, 0, 4], + * [1, 1, 4, 2, 8], + * [2, 3, 3, 9, 3] + * ] + * ``` + * + * 2. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.candlestick. + * turboThreshold), this option is not available. + * + * ```js + * data: [{ + * x: 1, + * open: 9, + * high: 2, + * low: 4, + * close: 6, + * name: "Point2", + * color: "#00FF00" + * }, { + * x: 1, + * open: 1, + * high: 4, + * low: 7, + * close: 7, + * name: "Point1", + * color: "#FF00FF" + * }] + * ``` + * + * @type {Array} + * @extends series.ohlc.data + * @excluding y + * @product highstock + * @apioption series.candlestick.data + */ + +}(Highcharts)); +var onSeriesMixin = (function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ + +var each = H.each, + seriesTypes = H.seriesTypes, + stableSort = H.stableSort; + +var onSeriesMixin = { + /** + * Extend the translate method by placing the point on the related series + */ + translate: function () { + + seriesTypes.column.prototype.translate.apply(this); + + var series = this, + options = series.options, + chart = series.chart, + points = series.points, + cursor = points.length - 1, + point, + lastPoint, + optionsOnSeries = options.onSeries, + onSeries = optionsOnSeries && chart.get(optionsOnSeries), + onKey = options.onKey || 'y', + step = onSeries && onSeries.options.step, + onData = onSeries && onSeries.points, + i = onData && onData.length, + xAxis = series.xAxis, + yAxis = series.yAxis, + xAxisExt = xAxis.getExtremes(), + xOffset = 0, + leftPoint, + lastX, + rightPoint, + currentDataGrouping, + distanceRatio; + + // relate to a master series + if (onSeries && onSeries.visible && i) { + xOffset = (onSeries.pointXOffset || 0) + (onSeries.barW || 0) / 2; + currentDataGrouping = onSeries.currentDataGrouping; + lastX = ( + onData[i - 1].x + + (currentDataGrouping ? currentDataGrouping.totalRange : 0) + ); // #2374 + + // sort the data points + stableSort(points, function (a, b) { + return (a.x - b.x); + }); + + onKey = 'plot' + onKey[0].toUpperCase() + onKey.substr(1); + while (i-- && points[cursor]) { + leftPoint = onData[i]; + point = points[cursor]; + point.y = leftPoint.y; + + if (leftPoint.x <= point.x && leftPoint[onKey] !== undefined) { + if (point.x <= lastX) { // #803 + + point.plotY = leftPoint[onKey]; + + // interpolate between points, #666 + if (leftPoint.x < point.x && !step) { + rightPoint = onData[i + 1]; + if (rightPoint && rightPoint[onKey] !== undefined) { + // the distance ratio, between 0 and 1 + distanceRatio = (point.x - leftPoint.x) / + (rightPoint.x - leftPoint.x); + point.plotY += + distanceRatio * + // the plotY distance + (rightPoint[onKey] - leftPoint[onKey]); + point.y += + distanceRatio * + (rightPoint.y - leftPoint.y); + } + } + } + cursor--; + i++; // check again for points in the same x position + if (cursor < 0) { + break; + } + } + } + } + + // Add plotY position and handle stacking + each(points, function (point, i) { + + var stackIndex; + + // Undefined plotY means the point is either on axis, outside series + // range or hidden series. If the series is outside the range of the + // x axis it should fall through with an undefined plotY, but then + // we must remove the shapeArgs (#847). + if (point.plotY === undefined) { + if (point.x >= xAxisExt.min && point.x <= xAxisExt.max) { + // we're inside xAxis range + point.plotY = chart.chartHeight - xAxis.bottom - + (xAxis.opposite ? xAxis.height : 0) + + xAxis.offset - yAxis.top; // #3517 + } else { + point.shapeArgs = {}; // 847 + } + } + point.plotX += xOffset; // #2049 + // if multiple flags appear at the same x, order them into a stack + lastPoint = points[i - 1]; + if (lastPoint && lastPoint.plotX === point.plotX) { + if (lastPoint.stackIndex === undefined) { + lastPoint.stackIndex = 0; + } + stackIndex = lastPoint.stackIndex + 1; + } + point.stackIndex = stackIndex; // #3639 + }); + + + } +}; +return onSeriesMixin; +}(Highcharts)); +(function (H, onSeriesMixin) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var addEvent = H.addEvent, + each = H.each, + merge = H.merge, + noop = H.noop, + Renderer = H.Renderer, + Series = H.Series, + seriesType = H.seriesType, + SVGRenderer = H.SVGRenderer, + TrackerMixin = H.TrackerMixin, + VMLRenderer = H.VMLRenderer, + symbols = SVGRenderer.prototype.symbols; + +/** + * The Flags series. + * @constructor seriesTypes.flags + * @augments seriesTypes.column + */ +/** + * Flags are used to mark events in stock charts. They can be added on the + * timeline, or attached to a specific series. + * + * @sample stock/demo/flags-general/ Flags on a line series + * @extends {plotOptions.column} + * @excluding animation,borderColor,borderRadius,borderWidth,colorByPoint,dataGrouping,pointPadding,pointWidth,turboThreshold + * @product highstock + * @optionparent plotOptions.flags + */ +seriesType('flags', 'column', { + + /** + * In case the flag is placed on a series, on what point key to place + * it. Line and columns have one key, `y`. In range or OHLC-type series, + * however, the flag can optionally be placed on the `open`, `high`, + * `low` or `close` key. + * + * @validvalue ["y", "open", "high", "low", "close"] + * @type {String} + * @sample {highstock} stock/plotoptions/flags-onkey/ Range series, flag on high + * @default y + * @since 4.2.2 + * @product highstock + * @apioption plotOptions.flags.onKey + */ + + /** + * The id of the series that the flags should be drawn on. If no id + * is given, the flags are drawn on the x axis. + * + * @type {String} + * @sample {highstock} stock/plotoptions/flags/ Flags on series and on x axis + * @default undefined + * @product highstock + * @apioption plotOptions.flags.onSeries + */ + + pointRange: 0, // #673 + + /** + * Whether the flags are allowed to overlap sideways. If `false`, the flags + * are moved sideways using an algorithm that seeks to place every flag as + * close as possible to its original position. + * + * @sample {highstock} stock/plotoptions/flags-allowoverlapx + * Allow sideways overlap + * + * @since 6.0.4 + */ + allowOverlapX: false, + + /** + * The shape of the marker. Can be one of "flag", "circlepin", "squarepin", + * or an image on the format `url(/path-to-image.jpg)`. Individual + * shapes can also be set for each point. + * + * @validvalue ["flag", "circlepin", "squarepin"] + * @type {String} + * @sample {highstock} stock/plotoptions/flags/ Different shapes + * @default flag + * @product highstock + */ + shape: 'flag', + + /** + * When multiple flags in the same series fall on the same value, this + * number determines the vertical offset between them. + * + * @type {Number} + * @sample {highstock} stock/plotoptions/flags-stackdistance/ A greater stack distance + * @default 12 + * @product highstock + */ + stackDistance: 12, + + /** + * Text alignment for the text inside the flag. + * + * @validvalue ["left", "center", "right"] + * @type {String} + * @default center + * @since 5.0.0 + * @product highstock + */ + textAlign: 'center', + + /** + * Specific tooltip options for flag series. Flag series tooltips are + * different from most other types in that a flag doesn't have a data + * value, so the tooltip rather displays the `text` option for each + * point. + * + * @type {Object} + * @extends plotOptions.series.tooltip + * @excluding changeDecimals,valueDecimals,valuePrefix,valueSuffix + * @product highstock + */ + tooltip: { + pointFormat: '{point.text}
' + }, + + threshold: null, + + /** + * The text to display on each flag. This can be defined on series level, + * or individually for each point. Defaults to `"A"`. + * + * @type {String} + * @default A + * @product highstock + * @apioption plotOptions.flags.title + */ + + /** + * The y position of the top left corner of the flag relative to either + * the series (if onSeries is defined), or the x axis. Defaults to + * `-30`. + * + * @type {Number} + * @default -30 + * @product highstock + */ + y: -30, + + /** + * Whether to use HTML to render the flag texts. Using HTML allows for + * advanced formatting, images and reliable bi-directional text rendering. + * Note that exported images won't respect the HTML, and that HTML + * won't respect Z-index settings. + * + * @type {Boolean} + * @default false + * @since 1.3 + * @product highstock + * @apioption plotOptions.flags.useHTML + */ + + + + /** + * The fill color for the flags. + */ + fillColor: '#ffffff', + + /** + * The color of the line/border of the flag. + * + * In styled mode, the stroke is set in the `.highcharts-flag-series + * .highcharts-point` rule. + * + * @type {Color} + * @default #000000 + * @product highstock + * @apioption plotOptions.flags.lineColor + */ + + /** + * The pixel width of the flag's line/border. + * + * @type {Number} + * @default 1 + * @product highstock + */ + lineWidth: 1, + + states: { + + /** + * @extends plotOptions.column.states.hover + * @product highstock + */ + hover: { + + /** + * The color of the line/border of the flag. + * + * @product highstock + */ + lineColor: '#000000', + + /** + * The fill or background color of the flag. + * + * @product highstock + */ + fillColor: '#ccd6eb' + } + }, + + /** + * The text styles of the flag. + * + * In styled mode, the styles are set in the `.highcharts-flag- + * series .highcharts-point` rule. + * + * @type {CSSObject} + * @default { "fontSize": "11px", "fontWeight": "bold" } + * @product highstock + */ + style: { + fontSize: '11px', + fontWeight: 'bold' + } + + +}, /** @lends seriesTypes.flags.prototype */ { + sorted: false, + noSharedTooltip: true, + allowDG: false, + takeOrdinalPosition: false, // #1074 + trackerGroups: ['markerGroup'], + forceCrop: true, + /** + * Inherit the initialization from base Series. + */ + init: Series.prototype.init, + + + /** + * Get presentational attributes + */ + pointAttribs: function (point, state) { + var options = this.options, + color = (point && point.color) || this.color, + lineColor = options.lineColor, + lineWidth = (point && point.lineWidth), + fill = (point && point.fillColor) || options.fillColor; + + if (state) { + fill = options.states[state].fillColor; + lineColor = options.states[state].lineColor; + lineWidth = options.states[state].lineWidth; + } + + return { + 'fill': fill || color, + 'stroke': lineColor || color, + 'stroke-width': lineWidth || options.lineWidth || 0 + }; + }, + + + translate: onSeriesMixin.translate, + + /** + * Draw the markers + */ + drawPoints: function () { + var series = this, + points = series.points, + chart = series.chart, + renderer = chart.renderer, + plotX, + plotY, + options = series.options, + optionsY = options.y, + shape, + i, + point, + graphic, + stackIndex, + anchorY, + attribs, + outsideRight, + yAxis = series.yAxis, + boxesMap = {}, + boxes = []; + + i = points.length; + while (i--) { + point = points[i]; + outsideRight = point.plotX > series.xAxis.len; + plotX = point.plotX; + stackIndex = point.stackIndex; + shape = point.options.shape || options.shape; + plotY = point.plotY; + + if (plotY !== undefined) { + plotY = point.plotY + optionsY - (stackIndex !== undefined && stackIndex * options.stackDistance); + } + point.anchorX = stackIndex ? undefined : point.plotX; // skip connectors for higher level stacked points + anchorY = stackIndex ? undefined : point.plotY; + + graphic = point.graphic; + + // Only draw the point if y is defined and the flag is within the visible area + if (plotY !== undefined && plotX >= 0 && !outsideRight) { + + // Create the flag + if (!graphic) { + graphic = point.graphic = renderer.label( + '', + null, + null, + shape, + null, + null, + options.useHTML + ) + + .attr(series.pointAttribs(point)) + .css(merge(options.style, point.style)) + + .attr({ + align: shape === 'flag' ? 'left' : 'center', + width: options.width, + height: options.height, + 'text-align': options.textAlign + }) + .addClass('highcharts-point') + .add(series.markerGroup); + + // Add reference to the point for tracker (#6303) + if (point.graphic.div) { + point.graphic.div.point = point; + } + + + graphic.shadow(options.shadow); + + graphic.isNew = true; + } + + if (plotX > 0) { // #3119 + plotX -= graphic.strokeWidth() % 2; // #4285 + } + + // Plant the flag + attribs = { + y: plotY, + anchorY: anchorY + }; + if (options.allowOverlapX) { + attribs.x = plotX; + attribs.anchorX = point.anchorX; + } + graphic.attr({ + text: point.options.title || options.title || 'A' + })[graphic.isNew ? 'attr' : 'animate'](attribs); + + // Rig for the distribute function + if (!options.allowOverlapX) { + if (!boxesMap[point.plotX]) { + boxesMap[point.plotX] = { + align: 0, + size: graphic.width, + target: plotX, + anchorX: plotX + }; + } else { + boxesMap[point.plotX].size = Math.max( + boxesMap[point.plotX].size, + graphic.width + ); + } + } + + // Set the tooltip anchor position + point.tooltipPos = chart.inverted ? + [yAxis.len + yAxis.pos - chart.plotLeft - plotY, series.xAxis.len - plotX] : + [plotX, plotY + yAxis.pos - chart.plotTop]; // #6327 + + } else if (graphic) { + point.graphic = graphic.destroy(); + } + + } + + // Handle X-dimension overlapping + if (!options.allowOverlapX) { + H.objectEach(boxesMap, function (box) { + box.plotX = box.anchorX; + boxes.push(box); + }); + + H.distribute(boxes, this.xAxis.len); + + each(points, function (point) { + var box = point.graphic && boxesMap[point.plotX]; + if (box) { + point.graphic[point.graphic.isNew ? 'attr' : 'animate']({ + x: box.pos, + anchorX: point.anchorX + }); + point.graphic.isNew = false; + } + }); + } + + // Might be a mix of SVG and HTML and we need events for both (#6303) + if (options.useHTML) { + H.wrap(series.markerGroup, 'on', function (proceed) { + return H.SVGElement.prototype.on.apply( + proceed.apply(this, [].slice.call(arguments, 1)), // for HTML + [].slice.call(arguments, 1)); // and for SVG + }); + } + + }, + + /** + * Extend the column trackers with listeners to expand and contract stacks + */ + drawTracker: function () { + var series = this, + points = series.points; + + TrackerMixin.drawTrackerPoint.apply(this); + + // Bring each stacked flag up on mouse over, this allows readability of vertically + // stacked elements as well as tight points on the x axis. #1924. + each(points, function (point) { + var graphic = point.graphic; + if (graphic) { + addEvent(graphic.element, 'mouseover', function () { + + // Raise this point + if (point.stackIndex > 0 && !point.raised) { + point._y = graphic.y; + graphic.attr({ + y: point._y - 8 + }); + point.raised = true; + } + + // Revert other raised points + each(points, function (otherPoint) { + if (otherPoint !== point && otherPoint.raised && otherPoint.graphic) { + otherPoint.graphic.attr({ + y: otherPoint._y + }); + otherPoint.raised = false; + } + }); + }); + } + }); + }, + + animate: noop, // Disable animation + buildKDTree: noop, + setClip: noop + +}); + +// create the flag icon with anchor +symbols.flag = function (x, y, w, h, options) { + var anchorX = (options && options.anchorX) || x, + anchorY = (options && options.anchorY) || y; + + return symbols.circle(anchorX - 1, anchorY - 1, 2, 2).concat( + [ + 'M', anchorX, anchorY, + 'L', x, y + h, + x, y, + x + w, y, + x + w, y + h, + x, y + h, + 'Z' + ] + ); +}; + /* - Highstock JS v2.0.4 (2014-09-02) + * Create the circlepin and squarepin icons with anchor + */ +function createPinSymbol(shape) { + symbols[shape + 'pin'] = function (x, y, w, h, options) { + + var anchorX = options && options.anchorX, + anchorY = options && options.anchorY, + path, + labelTopOrBottomY; + + // For single-letter flags, make sure circular flags are not taller than their width + if (shape === 'circle' && h > w) { + x -= Math.round((h - w) / 2); + w = h; + } + + path = symbols[shape](x, y, w, h); + + if (anchorX && anchorY) { + // if the label is below the anchor, draw the connecting line from the top edge of the label + // otherwise start drawing from the bottom edge + labelTopOrBottomY = (y > anchorY) ? y : y + h; + path.push( + 'M', + shape === 'circle' ? path[1] - path[4] : path[1] + path[4] / 2, + labelTopOrBottomY, + 'L', + anchorX, + anchorY + ); + path = path.concat( + symbols.circle(anchorX - 1, anchorY - 1, 2, 2) + ); + } + + return path; + }; +} +createPinSymbol('circle'); +createPinSymbol('square'); + + +// The symbol callbacks are generated on the SVGRenderer object in all browsers. Even +// VML browsers need this in order to generate shapes in export. Now share +// them with the VMLRenderer. +if (Renderer === VMLRenderer) { + each(['flag', 'circlepin', 'squarepin'], function (shape) { + VMLRenderer.prototype.symbols[shape] = symbols[shape]; + }); +} + + +/** + * A `flags` series. If the [type](#series.flags.type) option is not + * specified, it is inherited from [chart.type](#chart.type). + * + * For options that apply to multiple series, it is recommended to add + * them to the [plotOptions.series](#plotOptions.series) options structure. + * To apply to all series of this specific type, apply it to [plotOptions. + * flags](#plotOptions.flags). + * + * @type {Object} + * @extends series,plotOptions.flags + * @excluding dataParser,dataURL + * @product highstock + * @apioption series.flags + */ + +/** + * An array of data points for the series. For the `flags` series type, + * points can be given in the following ways: + * + * 1. An array of objects with named values. The objects are point + * configuration objects as seen below. If the total number of data + * points exceeds the series' [turboThreshold](#series.flags.turboThreshold), + * this option is not available. + * + * ```js + * data: [{ + * x: 1, + * title: "A", + * text: "First event" + * }, { + * x: 1, + * title: "B", + * text: "Second event" + * }] + * + * @type {Array} + * @extends series.line.data + * @excluding y,dataLabels,marker,name + * @product highstock + * @apioption series.flags.data + */ + +/** + * The fill color of an individual flag. By default it inherits from + * the series color. + * + * @type {Color} + * @product highstock + * @apioption series.flags.data.fillColor + */ + +/** + * The longer text to be shown in the flag's tooltip. + * + * @type {String} + * @product highstock + * @apioption series.flags.data.text + */ + +/** + * The short text to be shown on the flag. + * + * @type {String} + * @product highstock + * @apioption series.flags.data.title + */ + + +}(Highcharts, onSeriesMixin)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var addEvent = H.addEvent, + Axis = H.Axis, + correctFloat = H.correctFloat, + defaultOptions = H.defaultOptions, + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + each = H.each, + fireEvent = H.fireEvent, + hasTouch = H.hasTouch, + isTouchDevice = H.isTouchDevice, + merge = H.merge, + pick = H.pick, + removeEvent = H.removeEvent, + svg = H.svg, + wrap = H.wrap, + swapXY; + +/** + * + * The scrollbar is a means of panning over the X axis of a stock chart. + * + * In styled mode, all the presentational options for the + * scrollbar are replaced by the classes `.highcharts-scrollbar-thumb`, + * `.highcharts-scrollbar-arrow`, `.highcharts-scrollbar-button`, + * `.highcharts-scrollbar-rifles` and `.highcharts-scrollbar-track`. + * + * @product highstock + * @optionparent scrollbar + */ +var defaultScrollbarOptions = { + + /** + * The height of the scrollbar. The height also applies to the width + * of the scroll arrows so that they are always squares. Defaults to + * 20 for touch devices and 14 for mouse devices. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/height/ A 30px scrollbar + * @product highstock + */ + height: isTouchDevice ? 20 : 14, + + /** + * The border rounding radius of the bar. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default 0 + * @product highstock + */ + barBorderRadius: 0, + + /** + * The corner radius of the scrollbar buttons. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default 0 + * @product highstock + */ + buttonBorderRadius: 0, + + /** + * Whether to redraw the main chart as the scrollbar or the navigator + * zoomed window is moved. Defaults to `true` for modern browsers and + * `false` for legacy IE browsers as well as mobile devices. + * + * @type {Boolean} + * @since 1.3 + * @product highstock + */ + liveRedraw: svg && !isTouchDevice, + + /** + * The margin between the scrollbar and its axis when the scrollbar is + * applied directly to an axis. + */ + margin: 10, + + /** + * The minimum width of the scrollbar. + * + * @type {Number} + * @default 6 + * @since 1.2.5 + * @product highstock + */ + minWidth: 6, + + step: 0.2, + + /** + * The z index of the scrollbar group. + */ + zIndex: 3, + + + /** + * The background color of the scrollbar itself. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #cccccc + * @product highstock + */ + barBackgroundColor: '#cccccc', + + /** + * The width of the bar's border. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default 1 + * @product highstock + */ + barBorderWidth: 1, + + /** + * The color of the scrollbar's border. + * + * @type {Color} + * @default #cccccc + * @product highstock + */ + barBorderColor: '#cccccc', + + /** + * The color of the small arrow inside the scrollbar buttons. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #333333 + * @product highstock + */ + buttonArrowColor: '#333333', + + /** + * The color of scrollbar buttons. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #e6e6e6 + * @product highstock + */ + buttonBackgroundColor: '#e6e6e6', + + /** + * The color of the border of the scrollbar buttons. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #cccccc + * @product highstock + */ + buttonBorderColor: '#cccccc', + + /** + * The border width of the scrollbar buttons. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default 1 + * @product highstock + */ + buttonBorderWidth: 1, + + /** + * The color of the small rifles in the middle of the scrollbar. + * + * @type {Color} + * @default #333333 + * @product highstock + */ + rifleColor: '#333333', + + /** + * The color of the track background. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #f2f2f2 + * @product highstock + */ + trackBackgroundColor: '#f2f2f2', + + /** + * The color of the border of the scrollbar track. + * + * @type {Color} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default #f2f2f2 + * @product highstock + */ + trackBorderColor: '#f2f2f2', + + /** + * The width of the border of the scrollbar track. + * + * @type {Number} + * @sample {highstock} stock/scrollbar/style/ Scrollbar styling + * @default 1 + * @product highstock + */ + trackBorderWidth: 1 + +}; + +defaultOptions.scrollbar = merge(true, defaultScrollbarOptions, defaultOptions.scrollbar); + +/** +* When we have vertical scrollbar, rifles and arrow in buttons should be rotated. +* The same method is used in Navigator's handles, to rotate them. +* @param {Array} path - path to be rotated +* @param {Boolean} vertical - if vertical scrollbar, swap x-y values +*/ +H.swapXY = swapXY = function (path, vertical) { + var i, + len = path.length, + temp; + + if (vertical) { + for (i = 0; i < len; i += 3) { + temp = path[i + 1]; + path[i + 1] = path[i + 2]; + path[i + 2] = temp; + } + } + + return path; +}; + +/** + * A reusable scrollbar, internally used in Highstock's navigator and optionally + * on individual axes. + * + * @class + * @param {Object} renderer + * @param {Object} options + * @param {Object} chart + */ +function Scrollbar(renderer, options, chart) { // docs + this.init(renderer, options, chart); +} + +Scrollbar.prototype = { + + init: function (renderer, options, chart) { + + this.scrollbarButtons = []; + + this.renderer = renderer; + + this.userOptions = options; + this.options = merge(defaultScrollbarOptions, options); + + this.chart = chart; + + this.size = pick(this.options.size, this.options.height); // backward compatibility + + // Init + if (options.enabled) { + this.render(); + this.initEvents(); + this.addEvents(); + } + }, + + /** + * Render scrollbar with all required items. + */ + render: function () { + var scroller = this, + renderer = scroller.renderer, + options = scroller.options, + size = scroller.size, + group; + + // Draw the scrollbar group + scroller.group = group = renderer.g('scrollbar').attr({ + zIndex: options.zIndex, + translateY: -99999 + }).add(); + + // Draw the scrollbar track: + scroller.track = renderer.rect() + .addClass('highcharts-scrollbar-track') + .attr({ + x: 0, + r: options.trackBorderRadius || 0, + height: size, + width: size + }).add(group); + + + scroller.track.attr({ + fill: options.trackBackgroundColor, + stroke: options.trackBorderColor, + 'stroke-width': options.trackBorderWidth + }); + + this.trackBorderWidth = scroller.track.strokeWidth(); + scroller.track.attr({ + y: -this.trackBorderWidth % 2 / 2 + }); + + + // Draw the scrollbar itself + scroller.scrollbarGroup = renderer.g().add(group); + + scroller.scrollbar = renderer.rect() + .addClass('highcharts-scrollbar-thumb') + .attr({ + height: size, + width: size, + r: options.barBorderRadius || 0 + }).add(scroller.scrollbarGroup); + + scroller.scrollbarRifles = renderer.path( + swapXY([ + 'M', + -3, size / 4, + 'L', + -3, 2 * size / 3, + 'M', + 0, size / 4, + 'L', + 0, 2 * size / 3, + 'M', + 3, size / 4, + 'L', + 3, 2 * size / 3 + ], options.vertical)) + .addClass('highcharts-scrollbar-rifles') + .add(scroller.scrollbarGroup); + + + scroller.scrollbar.attr({ + fill: options.barBackgroundColor, + stroke: options.barBorderColor, + 'stroke-width': options.barBorderWidth + }); + scroller.scrollbarRifles.attr({ + stroke: options.rifleColor, + 'stroke-width': 1 + }); + + scroller.scrollbarStrokeWidth = scroller.scrollbar.strokeWidth(); + scroller.scrollbarGroup.translate( + -scroller.scrollbarStrokeWidth % 2 / 2, + -scroller.scrollbarStrokeWidth % 2 / 2 + ); + + // Draw the buttons: + scroller.drawScrollbarButton(0); + scroller.drawScrollbarButton(1); + }, + + /** + * Position the scrollbar, method called from a parent with defined dimensions + * @param {Number} x - x-position on the chart + * @param {Number} y - y-position on the chart + * @param {Number} width - width of the scrollbar + * @param {Number} height - height of the scorllbar + */ + position: function (x, y, width, height) { + var scroller = this, + options = scroller.options, + vertical = options.vertical, + xOffset = height, + yOffset = 0, + method = scroller.rendered ? 'animate' : 'attr'; + + scroller.x = x; + scroller.y = y + this.trackBorderWidth; + scroller.width = width; // width with buttons + scroller.height = height; + scroller.xOffset = xOffset; + scroller.yOffset = yOffset; + + // If Scrollbar is a vertical type, swap options: + if (vertical) { + scroller.width = scroller.yOffset = width = yOffset = scroller.size; + scroller.xOffset = xOffset = 0; + scroller.barWidth = height - width * 2; // width without buttons + scroller.x = x = x + scroller.options.margin; + } else { + scroller.height = scroller.xOffset = height = xOffset = scroller.size; + scroller.barWidth = width - height * 2; // width without buttons + scroller.y = scroller.y + scroller.options.margin; + } + + // Set general position for a group: + scroller.group[method]({ + translateX: x, + translateY: scroller.y + }); + + // Resize background/track: + scroller.track[method]({ + width: width, + height: height + }); + + // Move right/bottom button ot it's place: + scroller.scrollbarButtons[1][method]({ + translateX: vertical ? 0 : width - xOffset, + translateY: vertical ? height - yOffset : 0 + }); + }, + + /** + * Draw the scrollbar buttons with arrows + * @param {Number} index 0 is left, 1 is right + */ + drawScrollbarButton: function (index) { + var scroller = this, + renderer = scroller.renderer, + scrollbarButtons = scroller.scrollbarButtons, + options = scroller.options, + size = scroller.size, + group, + tempElem; + + group = renderer.g().add(scroller.group); + scrollbarButtons.push(group); + + // Create a rectangle for the scrollbar button + tempElem = renderer.rect() + .addClass('highcharts-scrollbar-button') + .add(group); + + + // Presentational attributes + tempElem.attr({ + stroke: options.buttonBorderColor, + 'stroke-width': options.buttonBorderWidth, + fill: options.buttonBackgroundColor + }); + + + // Place the rectangle based on the rendered stroke width + tempElem.attr(tempElem.crisp({ + x: -0.5, + y: -0.5, + width: size + 1, // +1 to compensate for crispifying in rect method + height: size + 1, + r: options.buttonBorderRadius + }, tempElem.strokeWidth())); + + // Button arrow + tempElem = renderer + .path(swapXY([ + 'M', + size / 2 + (index ? -1 : 1), + size / 2 - 3, + 'L', + size / 2 + (index ? -1 : 1), + size / 2 + 3, + 'L', + size / 2 + (index ? 2 : -2), + size / 2 + ], options.vertical)) + .addClass('highcharts-scrollbar-arrow') + .add(scrollbarButtons[index]); + + + tempElem.attr({ + fill: options.buttonArrowColor + }); + + }, + + /** + * Set scrollbar size, with a given scale. + * @param {Number} from - scale (0-1) where bar should start + * @param {Number} to - scale (0-1) where bar should end + */ + setRange: function (from, to) { + var scroller = this, + options = scroller.options, + vertical = options.vertical, + minWidth = options.minWidth, + fullWidth = scroller.barWidth, + fromPX, + toPX, + newPos, + newSize, + newRiflesPos, + method = this.rendered && !this.hasDragged ? 'animate' : 'attr'; + + if (!defined(fullWidth)) { + return; + } + + from = Math.max(from, 0); + fromPX = Math.ceil(fullWidth * from); + toPX = fullWidth * Math.min(to, 1); + scroller.calculatedWidth = newSize = correctFloat(toPX - fromPX); + + // We need to recalculate position, if minWidth is used + if (newSize < minWidth) { + fromPX = (fullWidth - minWidth + newSize) * from; + newSize = minWidth; + } + newPos = Math.floor(fromPX + scroller.xOffset + scroller.yOffset); + newRiflesPos = newSize / 2 - 0.5; // -0.5 -> rifle line width / 2 + + // Store current position: + scroller.from = from; + scroller.to = to; + + if (!vertical) { + scroller.scrollbarGroup[method]({ + translateX: newPos + }); + scroller.scrollbar[method]({ + width: newSize + }); + scroller.scrollbarRifles[method]({ + translateX: newRiflesPos + }); + scroller.scrollbarLeft = newPos; + scroller.scrollbarTop = 0; + } else { + scroller.scrollbarGroup[method]({ + translateY: newPos + }); + scroller.scrollbar[method]({ + height: newSize + }); + scroller.scrollbarRifles[method]({ + translateY: newRiflesPos + }); + scroller.scrollbarTop = newPos; + scroller.scrollbarLeft = 0; + } + + if (newSize <= 12) { + scroller.scrollbarRifles.hide(); + } else { + scroller.scrollbarRifles.show(true); + } + + // Show or hide the scrollbar based on the showFull setting + if (options.showFull === false) { + if (from <= 0 && to >= 1) { + scroller.group.hide(); + } else { + scroller.group.show(); + } + } + + scroller.rendered = true; + }, + + /** + * Init events methods, so we have an access to the Scrollbar itself + */ + initEvents: function () { + var scroller = this; + /** + * Event handler for the mouse move event. + */ + scroller.mouseMoveHandler = function (e) { + var normalizedEvent = scroller.chart.pointer.normalize(e), + options = scroller.options, + direction = options.vertical ? 'chartY' : 'chartX', + initPositions = scroller.initPositions, + scrollPosition, + chartPosition, + change; + + // In iOS, a mousemove event with e.pageX === 0 is fired when holding the finger + // down in the center of the scrollbar. This should be ignored. + if (scroller.grabbedCenter && (!e.touches || e.touches[0][direction] !== 0)) { // #4696, scrollbar failed on Android + chartPosition = scroller.cursorToScrollbarPosition(normalizedEvent)[direction]; + scrollPosition = scroller[direction]; + + change = chartPosition - scrollPosition; + + scroller.hasDragged = true; + scroller.updatePosition(initPositions[0] + change, initPositions[1] + change); + + if (scroller.hasDragged) { + fireEvent(scroller, 'changed', { + from: scroller.from, + to: scroller.to, + trigger: 'scrollbar', + DOMType: e.type, + DOMEvent: e + }); + } + } + }; + + /** + * Event handler for the mouse up event. + */ + scroller.mouseUpHandler = function (e) { + if (scroller.hasDragged) { + fireEvent(scroller, 'changed', { + from: scroller.from, + to: scroller.to, + trigger: 'scrollbar', + DOMType: e.type, + DOMEvent: e + }); + } + scroller.grabbedCenter = scroller.hasDragged = scroller.chartX = scroller.chartY = null; + }; + + scroller.mouseDownHandler = function (e) { + var normalizedEvent = scroller.chart.pointer.normalize(e), + mousePosition = scroller.cursorToScrollbarPosition(normalizedEvent); + + scroller.chartX = mousePosition.chartX; + scroller.chartY = mousePosition.chartY; + scroller.initPositions = [scroller.from, scroller.to]; + + scroller.grabbedCenter = true; + }; + + scroller.buttonToMinClick = function (e) { + var range = correctFloat(scroller.to - scroller.from) * scroller.options.step; + scroller.updatePosition(correctFloat(scroller.from - range), correctFloat(scroller.to - range)); + fireEvent(scroller, 'changed', { + from: scroller.from, + to: scroller.to, + trigger: 'scrollbar', + DOMEvent: e + }); + }; + + scroller.buttonToMaxClick = function (e) { + var range = (scroller.to - scroller.from) * scroller.options.step; + scroller.updatePosition(scroller.from + range, scroller.to + range); + fireEvent(scroller, 'changed', { + from: scroller.from, + to: scroller.to, + trigger: 'scrollbar', + DOMEvent: e + }); + }; + + scroller.trackClick = function (e) { + var normalizedEvent = scroller.chart.pointer.normalize(e), + range = scroller.to - scroller.from, + top = scroller.y + scroller.scrollbarTop, + left = scroller.x + scroller.scrollbarLeft; + + if ((scroller.options.vertical && normalizedEvent.chartY > top) || + (!scroller.options.vertical && normalizedEvent.chartX > left)) { + // On the top or on the left side of the track: + scroller.updatePosition(scroller.from + range, scroller.to + range); + } else { + // On the bottom or the right side of the track: + scroller.updatePosition(scroller.from - range, scroller.to - range); + } + + fireEvent(scroller, 'changed', { + from: scroller.from, + to: scroller.to, + trigger: 'scrollbar', + DOMEvent: e + }); + }; + }, + + /** + * Get normalized (0-1) cursor position over the scrollbar + * @param {Event} normalizedEvent - normalized event, with chartX and chartY values + * @return {Object} Local position {chartX, chartY} + */ + cursorToScrollbarPosition: function (normalizedEvent) { + var scroller = this, + options = scroller.options, + minWidthDifference = options.minWidth > scroller.calculatedWidth ? options.minWidth : 0; // minWidth distorts translation + + return { + chartX: (normalizedEvent.chartX - scroller.x - scroller.xOffset) / (scroller.barWidth - minWidthDifference), + chartY: (normalizedEvent.chartY - scroller.y - scroller.yOffset) / (scroller.barWidth - minWidthDifference) + }; + }, + + /** + * Update position option in the Scrollbar, with normalized 0-1 scale + */ + updatePosition: function (from, to) { + if (to > 1) { + from = correctFloat(1 - correctFloat(to - from)); + to = 1; + } + + if (from < 0) { + to = correctFloat(to - from); + from = 0; + } + + this.from = from; + this.to = to; + }, + + /** + * Update the scrollbar with new options + */ + update: function (options) { + this.destroy(); + this.init(this.chart.renderer, merge(true, this.options, options), this.chart); + }, + + /** + * Set up the mouse and touch events for the Scrollbar + */ + addEvents: function () { + var buttonsOrder = this.options.inverted ? [1, 0] : [0, 1], + buttons = this.scrollbarButtons, + bar = this.scrollbarGroup.element, + track = this.track.element, + mouseDownHandler = this.mouseDownHandler, + mouseMoveHandler = this.mouseMoveHandler, + mouseUpHandler = this.mouseUpHandler, + _events; + + // Mouse events + _events = [ + [buttons[buttonsOrder[0]].element, 'click', this.buttonToMinClick], + [buttons[buttonsOrder[1]].element, 'click', this.buttonToMaxClick], + [track, 'click', this.trackClick], + [bar, 'mousedown', mouseDownHandler], + [bar.ownerDocument, 'mousemove', mouseMoveHandler], + [bar.ownerDocument, 'mouseup', mouseUpHandler] + ]; + + // Touch events + if (hasTouch) { + _events.push( + [bar, 'touchstart', mouseDownHandler], + [bar.ownerDocument, 'touchmove', mouseMoveHandler], + [bar.ownerDocument, 'touchend', mouseUpHandler] + ); + } + + // Add them all + each(_events, function (args) { + addEvent.apply(null, args); + }); + this._events = _events; + }, + + /** + * Removes the event handlers attached previously with addEvents. + */ + removeEvents: function () { + each(this._events, function (args) { + removeEvent.apply(null, args); + }); + this._events.length = 0; + }, + + /** + * Destroys allocated elements. + */ + destroy: function () { + + var scroller = this.chart.scroller; + + // Disconnect events added in addEvents + this.removeEvents(); + + // Destroy properties + each(['track', 'scrollbarRifles', 'scrollbar', 'scrollbarGroup', 'group'], function (prop) { + if (this[prop] && this[prop].destroy) { + this[prop] = this[prop].destroy(); + } + }, this); + + if (scroller && this === scroller.scrollbar) { // #6421, chart may have more scrollbars + scroller.scrollbar = null; + + // Destroy elements in collection + destroyObjectProperties(scroller.scrollbarButtons); + } + } +}; + +/** +* Wrap axis initialization and create scrollbar if enabled: +*/ +wrap(Axis.prototype, 'init', function (proceed) { + var axis = this; + proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); + + if (axis.options.scrollbar && axis.options.scrollbar.enabled) { + // Predefined options: + axis.options.scrollbar.vertical = !axis.horiz; + axis.options.startOnTick = axis.options.endOnTick = false; + + axis.scrollbar = new Scrollbar(axis.chart.renderer, axis.options.scrollbar, axis.chart); + + addEvent(axis.scrollbar, 'changed', function (e) { + var unitedMin = Math.min(pick(axis.options.min, axis.min), axis.min, axis.dataMin), + unitedMax = Math.max(pick(axis.options.max, axis.max), axis.max, axis.dataMax), + range = unitedMax - unitedMin, + to, + from; + + if ((axis.horiz && !axis.reversed) || (!axis.horiz && axis.reversed)) { + to = unitedMin + range * this.to; + from = unitedMin + range * this.from; + } else { + // y-values in browser are reversed, but this also applies for reversed horizontal axis: + to = unitedMin + range * (1 - this.from); + from = unitedMin + range * (1 - this.to); + } + + axis.setExtremes(from, to, true, false, e); + }); + } +}); + +/** +* Wrap rendering axis, and update scrollbar if one is created: +*/ +wrap(Axis.prototype, 'render', function (proceed) { + var axis = this, + scrollMin = Math.min( + pick(axis.options.min, axis.min), + axis.min, + pick(axis.dataMin, axis.min) // #6930 + ), + scrollMax = Math.max( + pick(axis.options.max, axis.max), + axis.max, + pick(axis.dataMax, axis.max) // #6930 + ), + scrollbar = axis.scrollbar, + titleOffset = axis.titleOffset || 0, + offsetsIndex, + from, + to; + + proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); + + if (scrollbar) { + + if (axis.horiz) { + scrollbar.position( + axis.left, + axis.top + axis.height + 2 + axis.chart.scrollbarsOffsets[1] + + (axis.opposite ? + 0 : + titleOffset + axis.axisTitleMargin + axis.offset + ), + axis.width, + axis.height + ); + offsetsIndex = 1; + } else { + scrollbar.position( + axis.left + axis.width + 2 + axis.chart.scrollbarsOffsets[0] + + (axis.opposite ? + titleOffset + axis.axisTitleMargin + axis.offset : + 0 + ), + axis.top, + axis.width, + axis.height + ); + offsetsIndex = 0; + } + + if ((!axis.opposite && !axis.horiz) || (axis.opposite && axis.horiz)) { + axis.chart.scrollbarsOffsets[offsetsIndex] += + axis.scrollbar.size + axis.scrollbar.options.margin; + } + + if (isNaN(scrollMin) || isNaN(scrollMax) || !defined(axis.min) || !defined(axis.max)) { + scrollbar.setRange(0, 0); // default action: when there is not extremes on the axis, but scrollbar exists, make it full size + } else { + from = (axis.min - scrollMin) / (scrollMax - scrollMin); + to = (axis.max - scrollMin) / (scrollMax - scrollMin); + + if ((axis.horiz && !axis.reversed) || (!axis.horiz && axis.reversed)) { + scrollbar.setRange(from, to); + } else { + scrollbar.setRange(1 - to, 1 - from); // inverse vertical axis + } + } + } +}); + +/** +* Make space for a scrollbar +*/ +wrap(Axis.prototype, 'getOffset', function (proceed) { + var axis = this, + index = axis.horiz ? 2 : 1, + scrollbar = axis.scrollbar; + + proceed.apply(axis, Array.prototype.slice.call(arguments, 1)); + + if (scrollbar) { + axis.chart.scrollbarsOffsets = [0, 0]; // reset scrollbars offsets + axis.chart.axisOffset[index] += scrollbar.size + scrollbar.options.margin; + } +}); + +/** +* Destroy scrollbar when connected to the specific axis +*/ +wrap(Axis.prototype, 'destroy', function (proceed) { + if (this.scrollbar) { + this.scrollbar = this.scrollbar.destroy(); + } + + proceed.apply(this, Array.prototype.slice.call(arguments, 1)); +}); + +H.Scrollbar = Scrollbar; + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +/* eslint max-len: ["warn", 80, 4] */ + +/** + * Options for the corresponding navigator series if `showInNavigator` + * is `true` for this series. Available options are the same as any + * series, documented at [plotOptions](#plotOptions.series) and + * [series](#series). + * + * + * These options are merged with options in [navigator.series](#navigator. + * series), and will take precedence if the same option is defined both + * places. + * + * @type {Object} + * @see [navigator.series](#navigator.series) + * @default undefined + * @since 5.0.0 + * @product highstock + * @apioption plotOptions.series.navigatorOptions + */ + +/** + * Whether or not to show the series in the navigator. Takes precedence + * over [navigator.baseSeries](#navigator.baseSeries) if defined. + * + * @type {Boolean} + * @default undefined + * @since 5.0.0 + * @product highstock + * @apioption plotOptions.series.showInNavigator + */ + +var addEvent = H.addEvent, + Axis = H.Axis, + Chart = H.Chart, + color = H.color, + defaultDataGroupingUnits = H.defaultDataGroupingUnits, + defaultOptions = H.defaultOptions, + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + each = H.each, + erase = H.erase, + error = H.error, + extend = H.extend, + grep = H.grep, + hasTouch = H.hasTouch, + isArray = H.isArray, + isNumber = H.isNumber, + isObject = H.isObject, + merge = H.merge, + pick = H.pick, + removeEvent = H.removeEvent, + Scrollbar = H.Scrollbar, + Series = H.Series, + seriesTypes = H.seriesTypes, + wrap = H.wrap, + + units = [].concat(defaultDataGroupingUnits), // copy + defaultSeriesType, + + // Finding the min or max of a set of variables where we don't know if they + // are defined, is a pattern that is repeated several places in Highcharts. + // Consider making this a global utility method. + numExt = function (extreme) { + var numbers = grep(arguments, isNumber); + if (numbers.length) { + return Math[extreme].apply(0, numbers); + } + }; + +// add more resolution to units +units[4] = ['day', [1, 2, 3, 4]]; // allow more days +units[5] = ['week', [1, 2, 3]]; // allow more weeks + +defaultSeriesType = seriesTypes.areaspline === undefined ? + 'line' : + 'areaspline'; + +extend(defaultOptions, { + + /** + * The navigator is a small series below the main series, displaying + * a view of the entire data set. It provides tools to zoom in and + * out on parts of the data as well as panning across the dataset. + * + * @product highstock + * @optionparent navigator + */ + navigator: { + /** + * The height of the navigator. + * + * @type {Number} + * @sample {highstock} stock/navigator/height/ A higher navigator + * @default 40 + * @product highstock + */ + height: 40, + + /** + * The distance from the nearest element, the X axis or X axis labels. + * + * @type {Number} + * @sample {highstock} stock/navigator/margin/ + * A margin of 2 draws the navigator closer to the X axis labels + * @default 25 + * @product highstock + */ + margin: 25, + + /** + * Whether the mask should be inside the range marking the zoomed + * range, or outside. In Highstock 1.x it was always `false`. + * + * @type {Boolean} + * @sample {highstock} stock/navigator/maskinside-false/ + * False, mask outside + * @default true + * @since 2.0 + * @product highstock + */ + maskInside: true, + + /** + * Options for the handles for dragging the zoomed area. + * + * @type {Object} + * @sample {highstock} stock/navigator/handles/ Colored handles + * @product highstock + */ + handles: { + /** + * Width for handles. + * + * @type {Number} + * @default 7 + * @product highstock + * @sample {highstock} stock/navigator/styled-handles/ + * Styled handles + * @since 6.0.0 + */ + width: 7, + + /** + * Height for handles. + * + * @type {Number} + * @default 15 + * @product highstock + * @sample {highstock} stock/navigator/styled-handles/ + * Styled handles + * @since 6.0.0 + */ + height: 15, + + /** + * Array to define shapes of handles. 0-index for left, 1-index for + * right. + * + * Additionally, the URL to a graphic can be given on this form: + * `url(graphic.png)`. Note that for the image to be applied to + * exported charts, its URL needs to be accessible by the export + * server. + * + * Custom callbacks for symbol path generation can also be added to + * `Highcharts.SVGRenderer.prototype.symbols`. The callback is then + * used by its method name, as shown in the demo. + * + * @type {Array} + * @default ['navigator-handle', 'navigator-handle'] + * @product highstock + * @sample {highstock} stock/navigator/styled-handles/ + * Styled handles + * @since 6.0.0 + */ + symbols: ['navigator-handle', 'navigator-handle'], + + /** + * Allows to enable/disable handles. + * + * @type {Boolean} + * @default true + * @product highstock + * @since 6.0.0 + */ + enabled: true, + + + /** + * The width for the handle border and the stripes inside. + * + * @type {Number} + * @default 7 + * @product highstock + * @sample {highstock} stock/navigator/styled-handles/ + * Styled handles + * @since 6.0.0 + */ + lineWidth: 1, + + /** + * The fill for the handle. + * + * @type {Color} + * @product highstock + */ + backgroundColor: '#f2f2f2', + + /** + * The stroke for the handle border and the stripes inside. + * + * @type {Color} + * @product highstock + */ + borderColor: '#999999' + + + }, + + + + /** + * The color of the mask covering the areas of the navigator series + * that are currently not visible in the main series. The default + * color is bluish with an opacity of 0.3 to see the series below. + * + * @type {Color} + * @see In styled mode, the mask is styled with the + * `.highcharts-navigator-mask` and + * `.highcharts-navigator-mask-inside` classes. + * @sample {highstock} stock/navigator/maskfill/ + * Blue, semi transparent mask + * @default rgba(102,133,194,0.3) + * @product highstock + */ + maskFill: color('#6685c2').setOpacity(0.3).get(), + + /** + * The color of the line marking the currently zoomed area in the + * navigator. + * + * @type {Color} + * @sample {highstock} stock/navigator/outline/ 2px blue outline + * @default #cccccc + * @product highstock + */ + outlineColor: '#cccccc', + + /** + * The width of the line marking the currently zoomed area in the + * navigator. + * + * @type {Number} + * @see In styled mode, the outline stroke width is set with the `. + * highcharts-navigator-outline` class. + * @sample {highstock} stock/navigator/outline/ 2px blue outline + * @default 2 + * @product highstock + */ + outlineWidth: 1, + + + /** + * Options for the navigator series. Available options are the same + * as any series, documented at [plotOptions](#plotOptions.series) + * and [series](#series). + * + * Unless data is explicitly defined on navigator.series, the data + * is borrowed from the first series in the chart. + * + * Default series options for the navigator series are: + * + *
series: {
+		 *     type: 'areaspline',
+		 *     fillOpacity: 0.05,
+		 *     dataGrouping: {
+		 *         smoothed: true
+		 *     },
+		 *     lineWidth: 1,
+		 *     marker: {
+		 *         enabled: false
+		 *     }
+		 * }
+ * + * @type {Object} + * @see In styled mode, the navigator series is styled with the `. + * highcharts-navigator-series` class. + * @sample {highstock} stock/navigator/series-data/ + * Using a separate data set for the navigator + * @sample {highstock} stock/navigator/series/ + * A green navigator series + * @product highstock + */ + series: { + + /** + * The type of the navigator series. Defaults to `areaspline` if + * defined, otherwise `line`. + * + * @type {String} + */ + type: defaultSeriesType, + + + + /** + * The fill opacity of the navigator series. + */ + fillOpacity: 0.05, + + /** + * The pixel line width of the navigator series. + */ + lineWidth: 1, + + + /** + * @ignore + */ + compare: null, + + /** + * Data grouping options for the navigator series. + * + * @extends {plotOptions.series.dataGrouping} + */ + dataGrouping: { + approximation: 'average', + enabled: true, + groupPixelWidth: 2, + smoothed: true, + units: units + }, + + /** + * Data label options for the navigator series. Data labels are + * disabled by default on the navigator series. + * + * @extends {plotOptions.series.dataLabels} + */ + dataLabels: { + enabled: false, + zIndex: 2 // #1839 + }, + + id: 'highcharts-navigator-series', + className: 'highcharts-navigator-series', + + /** + * Line color for the navigator series. Allows setting the color + * while disallowing the default candlestick setting. + * + * @type {Color} + */ + lineColor: null, // #4602 + + marker: { + enabled: false + }, + + pointRange: 0, + /** + * The threshold option. Setting it to 0 will make the default + * navigator area series draw its area from the 0 value and up. + * @type {Number} + */ + threshold: null + }, + + /** + * Options for the navigator X axis. Default series options + * for the navigator xAxis are: + * + *
xAxis: {
+		 *     tickWidth: 0,
+		 *     lineWidth: 0,
+		 *     gridLineWidth: 1,
+		 *     tickPixelInterval: 200,
+		 *     labels: {
+		 *     	   align: 'left',
+		 *         style: {
+		 *             color: '#888'
+		 *         },
+		 *         x: 3,
+		 *         y: -4
+		 *     }
+		 * }
+ * + * @type {Object} + * @extends {xAxis} + * @excluding linkedTo,maxZoom,minRange,opposite,range,scrollbar, + * showEmpty,maxRange + * @product highstock + */ + xAxis: { + /** + * Additional range on the right side of the xAxis. Works similar to + * xAxis.maxPadding, but value is set in milliseconds. + * Can be set for both, main xAxis and navigator's xAxis. + * + * @type {Number} + * @default 0 + * @since 6.0.0 + * @product highstock + * @apioption xAxis.overscroll + */ + overscroll: 0, + + className: 'highcharts-navigator-xaxis', + tickLength: 0, + + + lineWidth: 0, + gridLineColor: '#e6e6e6', + gridLineWidth: 1, + + + tickPixelInterval: 200, + + labels: { + align: 'left', + + + style: { + color: '#999999' + }, + + + x: 3, + y: -4 + }, + + crosshair: false + }, + + /** + * Options for the navigator Y axis. Default series options + * for the navigator yAxis are: + * + *
yAxis: {
+		 *     gridLineWidth: 0,
+		 *     startOnTick: false,
+		 *     endOnTick: false,
+		 *     minPadding: 0.1,
+		 *     maxPadding: 0.1,
+		 *     labels: {
+		 *         enabled: false
+		 *     },
+		 *     title: {
+		 *         text: null
+		 *     },
+		 *     tickWidth: 0
+		 * }
+ * + * @type {Object} + * @extends {yAxis} + * @excluding height,linkedTo,maxZoom,minRange,ordinal,range,showEmpty, + * scrollbar,top,units,maxRange + * @product highstock + */ + yAxis: { + + className: 'highcharts-navigator-yaxis', + + + gridLineWidth: 0, + + + startOnTick: false, + endOnTick: false, + minPadding: 0.1, + maxPadding: 0.1, + labels: { + enabled: false + }, + crosshair: false, + title: { + text: null + }, + tickLength: 0, + tickWidth: 0 + } + } +}); + +/** + * Draw one of the handles on the side of the zoomed range in the navigator + * @param {Boolean} inverted flag for chart.inverted + * @returns {Array} Path to be used in a handle + */ +H.Renderer.prototype.symbols['navigator-handle'] = function ( + x, + y, + w, + h, + options +) { + var halfWidth = options.width / 2, + markerPosition = Math.round(halfWidth / 3) + 0.5, + height = options.height; + + return [ + 'M', + -halfWidth - 1, 0.5, + 'L', + halfWidth, 0.5, + 'L', + halfWidth, height + 0.5, + 'L', + -halfWidth - 1, height + 0.5, + 'L', + -halfWidth - 1, 0.5, + 'M', + -markerPosition, 4, + 'L', + -markerPosition, height - 3, + 'M', + markerPosition - 1, 4, + 'L', + markerPosition - 1, height - 3 + ]; +}; + +/** + * The Navigator class + * @param {Object} chart - Chart object + * @class + */ +function Navigator(chart) { + this.init(chart); +} + +Navigator.prototype = { + /** + * Draw one of the handles on the side of the zoomed range in the navigator + * @param {Number} x The x center for the handle + * @param {Number} index 0 for left and 1 for right + * @param {Boolean} inverted flag for chart.inverted + * @param {String} verb use 'animate' or 'attr' + */ + drawHandle: function (x, index, inverted, verb) { + var navigator = this, + height = navigator.navigatorOptions.handles.height; + + // Place it + navigator.handles[index][verb](inverted ? { + translateX: Math.round(navigator.left + navigator.height / 2), + translateY: Math.round( + navigator.top + parseInt(x, 10) + 0.5 - height + ) + } : { + translateX: Math.round(navigator.left + parseInt(x, 10)), + translateY: Math.round( + navigator.top + navigator.height / 2 - height / 2 - 1 + ) + }); + }, + + /** + * Render outline around the zoomed range + * @param {Number} zoomedMin in pixels position where zoomed range starts + * @param {Number} zoomedMax in pixels position where zoomed range ends + * @param {Boolean} inverted flag if chart is inverted + * @param {String} verb use 'animate' or 'attr' + */ + drawOutline: function (zoomedMin, zoomedMax, inverted, verb) { + var navigator = this, + maskInside = navigator.navigatorOptions.maskInside, + outlineWidth = navigator.outline.strokeWidth(), + halfOutline = outlineWidth / 2, + outlineCorrection = (outlineWidth % 2) / 2, // #5800 + outlineHeight = navigator.outlineHeight, + scrollbarHeight = navigator.scrollbarHeight, + navigatorSize = navigator.size, + left = navigator.left - scrollbarHeight, + navigatorTop = navigator.top, + verticalMin, + path; + + if (inverted) { + left -= halfOutline; + verticalMin = navigatorTop + zoomedMax + outlineCorrection; + zoomedMax = navigatorTop + zoomedMin + outlineCorrection; + + path = [ + 'M', + left + outlineHeight, + navigatorTop - scrollbarHeight - outlineCorrection, // top edge + 'L', + left + outlineHeight, + verticalMin, // top right of zoomed range + 'L', + left, + verticalMin, // top left of z.r. + 'L', + left, + zoomedMax, // bottom left of z.r. + 'L', + left + outlineHeight, + zoomedMax, // bottom right of z.r. + 'L', + left + outlineHeight, + navigatorTop + navigatorSize + scrollbarHeight // bottom edge + ].concat(maskInside ? [ + 'M', + left + outlineHeight, + verticalMin - halfOutline, // upper left of zoomed range + 'L', + left + outlineHeight, + zoomedMax + halfOutline // upper right of z.r. + ] : []); + } else { + zoomedMin += left + scrollbarHeight - outlineCorrection; + zoomedMax += left + scrollbarHeight - outlineCorrection; + navigatorTop += halfOutline; + + path = [ + 'M', + left, + navigatorTop, // left + 'L', + zoomedMin, + navigatorTop, // upper left of zoomed range + 'L', + zoomedMin, + navigatorTop + outlineHeight, // lower left of z.r. + 'L', + zoomedMax, + navigatorTop + outlineHeight, // lower right of z.r. + 'L', + zoomedMax, + navigatorTop, // upper right of z.r. + 'L', + left + navigatorSize + scrollbarHeight * 2, + navigatorTop // right + ].concat(maskInside ? [ + 'M', + zoomedMin - halfOutline, + navigatorTop, // upper left of zoomed range + 'L', + zoomedMax + halfOutline, + navigatorTop // upper right of z.r. + ] : []); + } + navigator.outline[verb]({ + d: path + }); + }, + + /** + * Render outline around the zoomed range + * @param {Number} zoomedMin in pixels position where zoomed range starts + * @param {Number} zoomedMax in pixels position where zoomed range ends + * @param {Boolean} inverted flag if chart is inverted + * @param {String} verb use 'animate' or 'attr' + */ + drawMasks: function (zoomedMin, zoomedMax, inverted, verb) { + var navigator = this, + left = navigator.left, + top = navigator.top, + navigatorHeight = navigator.height, + height, + width, + x, + y; + + // Determine rectangle position & size + // According to (non)inverted position: + if (inverted) { + x = [left, left, left]; + y = [top, top + zoomedMin, top + zoomedMax]; + width = [navigatorHeight, navigatorHeight, navigatorHeight]; + height = [ + zoomedMin, + zoomedMax - zoomedMin, + navigator.size - zoomedMax + ]; + } else { + x = [left, left + zoomedMin, left + zoomedMax]; + y = [top, top, top]; + width = [ + zoomedMin, + zoomedMax - zoomedMin, + navigator.size - zoomedMax + ]; + height = [navigatorHeight, navigatorHeight, navigatorHeight]; + } + each(navigator.shades, function (shade, i) { + shade[verb]({ + x: x[i], + y: y[i], + width: width[i], + height: height[i] + }); + }); + }, + + /** + * Generate DOM elements for a navigator: + * - main navigator group + * - all shades + * - outline + * - handles + */ + renderElements: function () { + var navigator = this, + navigatorOptions = navigator.navigatorOptions, + maskInside = navigatorOptions.maskInside, + chart = navigator.chart, + inverted = chart.inverted, + renderer = chart.renderer, + navigatorGroup; + + // Create the main navigator group + navigator.navigatorGroup = navigatorGroup = renderer.g('navigator') + .attr({ + zIndex: 8, + visibility: 'hidden' + }) + .add(); + + + + var mouseCursor = { + cursor: inverted ? 'ns-resize' : 'ew-resize' + }; + + + // Create masks, each mask will get events and fill: + each([!maskInside, maskInside, !maskInside], function (hasMask, index) { + navigator.shades[index] = renderer.rect() + .addClass('highcharts-navigator-mask' + + (index === 1 ? '-inside' : '-outside')) + + .attr({ + fill: hasMask ? navigatorOptions.maskFill : 'rgba(0,0,0,0)' + }) + .css(index === 1 && mouseCursor) + + .add(navigatorGroup); + }); + + // Create the outline: + navigator.outline = renderer.path() + .addClass('highcharts-navigator-outline') + + .attr({ + 'stroke-width': navigatorOptions.outlineWidth, + stroke: navigatorOptions.outlineColor + }) + + .add(navigatorGroup); + + // Create the handlers: + if (navigatorOptions.handles.enabled) { + each([0, 1], function (index) { + navigatorOptions.handles.inverted = chart.inverted; + navigator.handles[index] = renderer.symbol( + navigatorOptions.handles.symbols[index], + -navigatorOptions.handles.width / 2 - 1, + 0, + navigatorOptions.handles.width, + navigatorOptions.handles.height, + navigatorOptions.handles + ); + // zIndex = 6 for right handle, 7 for left. + // Can't be 10, because of the tooltip in inverted chart #2908 + navigator.handles[index].attr({ zIndex: 7 - index }) + .addClass( + 'highcharts-navigator-handle ' + + 'highcharts-navigator-handle-' + + ['left', 'right'][index] + ).add(navigatorGroup); + + + var handlesOptions = navigatorOptions.handles; + navigator.handles[index] + .attr({ + fill: handlesOptions.backgroundColor, + stroke: handlesOptions.borderColor, + 'stroke-width': handlesOptions.lineWidth + }) + .css(mouseCursor); + + }); + } + }, + + /** + * Update navigator + * @param {Object} options Options to merge in when updating navigator + */ + update: function (options) { + // Remove references to old navigator series in base series + each(this.series || [], function (series) { + if (series.baseSeries) { + delete series.baseSeries.navigatorSeries; + } + }); + // Destroy and rebuild navigator + this.destroy(); + var chartOptions = this.chart.options; + merge(true, chartOptions.navigator, this.options, options); + this.init(this.chart); + }, + + /** + * Render the navigator + * @param {Number} min X axis value minimum + * @param {Number} max X axis value maximum + * @param {Number} pxMin Pixel value minimum + * @param {Number} pxMax Pixel value maximum + */ + render: function (min, max, pxMin, pxMax) { + + var navigator = this, + chart = navigator.chart, + navigatorWidth, + scrollbarLeft, + scrollbarTop, + scrollbarHeight = navigator.scrollbarHeight, + navigatorSize, + xAxis = navigator.xAxis, + scrollbarXAxis = xAxis.fake ? chart.xAxis[0] : xAxis, + navigatorEnabled = navigator.navigatorEnabled, + zoomedMin, + zoomedMax, + rendered = navigator.rendered, + inverted = chart.inverted, + verb, + newMin, + newMax, + currentRange, + minRange = chart.xAxis[0].minRange, + maxRange = chart.xAxis[0].options.maxRange; + + // Don't redraw while moving the handles (#4703). + if (this.hasDragged && !defined(pxMin)) { + return; + } + + // Don't render the navigator until we have data (#486, #4202, #5172). + if (!isNumber(min) || !isNumber(max)) { + // However, if navigator was already rendered, we may need to resize + // it. For example hidden series, but visible navigator (#6022). + if (rendered) { + pxMin = 0; + pxMax = pick(xAxis.width, scrollbarXAxis.width); + } else { + return; + } + } + + navigator.left = pick( + xAxis.left, + // in case of scrollbar only, without navigator + chart.plotLeft + scrollbarHeight + (inverted ? chart.plotWidth : 0) + ); + + navigator.size = zoomedMax = navigatorSize = pick( + xAxis.len, + (inverted ? chart.plotHeight : chart.plotWidth) - + 2 * scrollbarHeight + ); + + if (inverted) { + navigatorWidth = scrollbarHeight; + } else { + navigatorWidth = navigatorSize + 2 * scrollbarHeight; + } + + // Get the pixel position of the handles + pxMin = pick(pxMin, xAxis.toPixels(min, true)); + pxMax = pick(pxMax, xAxis.toPixels(max, true)); + + // Verify (#1851, #2238) + if (!isNumber(pxMin) || Math.abs(pxMin) === Infinity) { + pxMin = 0; + pxMax = navigatorWidth; + } + + // Are we below the minRange? (#2618, #6191) + newMin = xAxis.toValue(pxMin, true); + newMax = xAxis.toValue(pxMax, true); + currentRange = Math.abs(H.correctFloat(newMax - newMin)); + if (currentRange < minRange) { + if (this.grabbedLeft) { + pxMin = xAxis.toPixels(newMax - minRange, true); + } else if (this.grabbedRight) { + pxMax = xAxis.toPixels(newMin + minRange, true); + } + } else if (defined(maxRange) && currentRange > maxRange) { + /** + * Maximum range which can be set using the navigator's handles. + * Opposite of [xAxis.minRange](#xAxis.minRange). + * + * @type {Number} + * @default undefined + * @product highstock + * @sample {highstock} stock/navigator/maxrange/ + * Defined max and min range + * @since 6.0.0 + * @apioption xAxis.maxRange + */ + if (this.grabbedLeft) { + pxMin = xAxis.toPixels(newMax - maxRange, true); + } else if (this.grabbedRight) { + pxMax = xAxis.toPixels(newMin + maxRange, true); + } + } + + // Handles are allowed to cross, but never exceed the plot area + navigator.zoomedMax = Math.min(Math.max(pxMin, pxMax, 0), zoomedMax); + navigator.zoomedMin = Math.min( + Math.max( + navigator.fixedWidth ? + navigator.zoomedMax - navigator.fixedWidth : + Math.min(pxMin, pxMax), + 0 + ), + zoomedMax + ); + + navigator.range = navigator.zoomedMax - navigator.zoomedMin; + + zoomedMax = Math.round(navigator.zoomedMax); + zoomedMin = Math.round(navigator.zoomedMin); + + if (navigatorEnabled) { + navigator.navigatorGroup.attr({ + visibility: 'visible' + }); + // Place elements + verb = rendered && !navigator.hasDragged ? 'animate' : 'attr'; + + navigator.drawMasks(zoomedMin, zoomedMax, inverted, verb); + navigator.drawOutline(zoomedMin, zoomedMax, inverted, verb); + + if (navigator.navigatorOptions.handles.enabled) { + navigator.drawHandle(zoomedMin, 0, inverted, verb); + navigator.drawHandle(zoomedMax, 1, inverted, verb); + } + } + + if (navigator.scrollbar) { + if (inverted) { + scrollbarTop = navigator.top - scrollbarHeight; + scrollbarLeft = navigator.left - scrollbarHeight + + (navigatorEnabled || !scrollbarXAxis.opposite ? 0 : + // Multiple axes has offsets: + (scrollbarXAxis.titleOffset || 0) + + // Self margin from the axis.title + scrollbarXAxis.axisTitleMargin + ); + scrollbarHeight = navigatorSize + 2 * scrollbarHeight; + } else { + scrollbarTop = navigator.top + + (navigatorEnabled ? navigator.height : -scrollbarHeight); + scrollbarLeft = navigator.left - scrollbarHeight; + } + // Reposition scrollbar + navigator.scrollbar.position( + scrollbarLeft, + scrollbarTop, + navigatorWidth, + scrollbarHeight + ); + // Keep scale 0-1 + navigator.scrollbar.setRange( + // Use real value, not rounded because range can be very small + // (#1716) + navigator.zoomedMin / navigatorSize, + navigator.zoomedMax / navigatorSize + ); + } + navigator.rendered = true; + }, + + /** + * Set up the mouse and touch events for the navigator + */ + addMouseEvents: function () { + var navigator = this, + chart = navigator.chart, + container = chart.container, + eventsToUnbind = [], + mouseMoveHandler, + mouseUpHandler; + + /** + * Create mouse events' handlers. + * Make them as separate functions to enable wrapping them: + */ + navigator.mouseMoveHandler = mouseMoveHandler = function (e) { + navigator.onMouseMove(e); + }; + navigator.mouseUpHandler = mouseUpHandler = function (e) { + navigator.onMouseUp(e); + }; + + // Add shades and handles mousedown events + eventsToUnbind = navigator.getPartsEvents('mousedown'); + // Add mouse move and mouseup events. These are bind to doc/container, + // because Navigator.grabbedSomething flags are stored in mousedown + // events + eventsToUnbind.push( + addEvent(container, 'mousemove', mouseMoveHandler), + addEvent(container.ownerDocument, 'mouseup', mouseUpHandler) + ); + + // Touch events + if (hasTouch) { + eventsToUnbind.push( + addEvent(container, 'touchmove', mouseMoveHandler), + addEvent(container.ownerDocument, 'touchend', mouseUpHandler) + ); + eventsToUnbind.concat(navigator.getPartsEvents('touchstart')); + } + + navigator.eventsToUnbind = eventsToUnbind; + + // Data events + if (navigator.series && navigator.series[0]) { + eventsToUnbind.push( + addEvent( + navigator.series[0].xAxis, + 'foundExtremes', + function () { + chart.navigator.modifyNavigatorAxisExtremes(); + } + ) + ); + } + }, + + /** + * Generate events for handles and masks + * @param {String} eventName Event name handler, 'mousedown' or 'touchstart' + * @returns {Array} An array of arrays: [DOMElement, eventName, callback]. + */ + getPartsEvents: function (eventName) { + var navigator = this, + events = []; + each(['shades', 'handles'], function (name) { + each(navigator[name], function (navigatorItem, index) { + events.push( + addEvent( + navigatorItem.element, + eventName, + function (e) { + navigator[name + 'Mousedown'](e, index); + } + ) + ); + }); + }); + return events; + }, + + /** + * Mousedown on a shaded mask, either: + * - will be stored for future drag&drop + * - will directly shift to a new range + * + * @param {Object} e Mouse event + * @param {Number} index Index of a mask in Navigator.shades array + */ + shadesMousedown: function (e, index) { + e = this.chart.pointer.normalize(e); + + var navigator = this, + chart = navigator.chart, + xAxis = navigator.xAxis, + zoomedMin = navigator.zoomedMin, + navigatorPosition = navigator.left, + navigatorSize = navigator.size, + range = navigator.range, + chartX = e.chartX, + fixedMax, + ext, + left; + + // For inverted chart, swap some options: + if (chart.inverted) { + chartX = e.chartY; + navigatorPosition = navigator.top; + } + + if (index === 1) { + // Store information for drag&drop + navigator.grabbedCenter = chartX; + navigator.fixedWidth = range; + navigator.dragOffset = chartX - zoomedMin; + } else { + // Shift the range by clicking on shaded areas + left = chartX - navigatorPosition - range / 2; + if (index === 0) { + left = Math.max(0, left); + } else if (index === 2 && left + range >= navigatorSize) { + left = navigatorSize - range; + fixedMax = navigator.getUnionExtremes().dataMax; // #2293, #3543 + } + if (left !== zoomedMin) { // it has actually moved + navigator.fixedWidth = range; // #1370 + + ext = xAxis.toFixedRange(left, left + range, null, fixedMax); + if (defined(ext.min)) { // #7411 + chart.xAxis[0].setExtremes( + Math.min(ext.min, ext.max), + Math.max(ext.min, ext.max), + true, + null, // auto animation + { trigger: 'navigator' } + ); + } + } + } + }, + + /** + * Mousedown on a handle mask. + * Will store necessary information for drag&drop. + * + * @param {Object} e Mouse event + * @param {Number} index Index of a handle in Navigator.handles array + */ + handlesMousedown: function (e, index) { + e = this.chart.pointer.normalize(e); + + var navigator = this, + chart = navigator.chart, + baseXAxis = chart.xAxis[0], + // For reversed axes, min and max are chagned, + // so the other extreme should be stored + reverse = (chart.inverted && !baseXAxis.reversed) || + (!chart.inverted && baseXAxis.reversed); + + if (index === 0) { + // Grab the left handle + navigator.grabbedLeft = true; + navigator.otherHandlePos = navigator.zoomedMax; + navigator.fixedExtreme = reverse ? baseXAxis.min : baseXAxis.max; + } else { + // Grab the right handle + navigator.grabbedRight = true; + navigator.otherHandlePos = navigator.zoomedMin; + navigator.fixedExtreme = reverse ? baseXAxis.max : baseXAxis.min; + } + + chart.fixedRange = null; + }, + /** + * Mouse move event based on x/y mouse position. + * @param {Object} e Mouse event + */ + onMouseMove: function (e) { + var navigator = this, + chart = navigator.chart, + left = navigator.left, + navigatorSize = navigator.navigatorSize, + range = navigator.range, + dragOffset = navigator.dragOffset, + inverted = chart.inverted, + chartX; + + + // In iOS, a mousemove event with e.pageX === 0 is fired when holding + // the finger down in the center of the scrollbar. This should be + // ignored. + if (!e.touches || e.touches[0].pageX !== 0) { // #4696 + + e = chart.pointer.normalize(e); + chartX = e.chartX; + + // Swap some options for inverted chart + if (inverted) { + left = navigator.top; + chartX = e.chartY; + } + + // Drag left handle or top handle + if (navigator.grabbedLeft) { + navigator.hasDragged = true; + navigator.render( + 0, + 0, + chartX - left, + navigator.otherHandlePos + ); + // Drag right handle or bottom handle + } else if (navigator.grabbedRight) { + navigator.hasDragged = true; + navigator.render( + 0, + 0, + navigator.otherHandlePos, + chartX - left + ); + // Drag scrollbar or open area in navigator + } else if (navigator.grabbedCenter) { + navigator.hasDragged = true; + if (chartX < dragOffset) { // outside left + chartX = dragOffset; + // outside right + } else if (chartX > navigatorSize + dragOffset - range) { + chartX = navigatorSize + dragOffset - range; + } + + navigator.render( + 0, + 0, + chartX - dragOffset, + chartX - dragOffset + range + ); + } + if ( + navigator.hasDragged && + navigator.scrollbar && + navigator.scrollbar.options.liveRedraw + ) { + e.DOMType = e.type; // DOMType is for IE8 + setTimeout(function () { + navigator.onMouseUp(e); + }, 0); + } + } + }, + + /** + * Mouse up event based on x/y mouse position. + * @param {Object} e Mouse event + */ + onMouseUp: function (e) { + var navigator = this, + chart = navigator.chart, + xAxis = navigator.xAxis, + reversed = xAxis && xAxis.reversed, + scrollbar = navigator.scrollbar, + unionExtremes, + fixedMin, + fixedMax, + ext, + DOMEvent = e.DOMEvent || e; + + if ( + // MouseUp is called for both, navigator and scrollbar (that order), + // which causes calling afterSetExtremes twice. Prevent first call + // by checking if scrollbar is going to set new extremes (#6334) + (navigator.hasDragged && (!scrollbar || !scrollbar.hasDragged)) || + e.trigger === 'scrollbar' + ) { + unionExtremes = navigator.getUnionExtremes(); + + // When dragging one handle, make sure the other one doesn't change + if (navigator.zoomedMin === navigator.otherHandlePos) { + fixedMin = navigator.fixedExtreme; + } else if (navigator.zoomedMax === navigator.otherHandlePos) { + fixedMax = navigator.fixedExtreme; + } + // Snap to right edge (#4076) + if (navigator.zoomedMax === navigator.size) { + fixedMax = reversed ? + unionExtremes.dataMin : unionExtremes.dataMax; + } + + // Snap to left edge (#7576) + if (navigator.zoomedMin === 0) { + fixedMin = reversed ? + unionExtremes.dataMax : unionExtremes.dataMin; + } + + ext = xAxis.toFixedRange( + navigator.zoomedMin, + navigator.zoomedMax, + fixedMin, + fixedMax + ); + + if (defined(ext.min)) { + chart.xAxis[0].setExtremes( + Math.min(ext.min, ext.max), + Math.max(ext.min, ext.max), + true, + // Run animation when clicking buttons, scrollbar track etc, + // but not when dragging handles or scrollbar + navigator.hasDragged ? false : null, + { + trigger: 'navigator', + triggerOp: 'navigator-drag', + DOMEvent: DOMEvent // #1838 + } + ); + } + } + + if (e.DOMType !== 'mousemove') { + navigator.grabbedLeft = navigator.grabbedRight = + navigator.grabbedCenter = navigator.fixedWidth = + navigator.fixedExtreme = navigator.otherHandlePos = + navigator.hasDragged = navigator.dragOffset = null; + } + }, + + /** + * Removes the event handlers attached previously with addEvents. + */ + removeEvents: function () { + if (this.eventsToUnbind) { + each(this.eventsToUnbind, function (unbind) { + unbind(); + }); + this.eventsToUnbind = undefined; + } + this.removeBaseSeriesEvents(); + }, + + /** + * Remove data events. + */ + removeBaseSeriesEvents: function () { + var baseSeries = this.baseSeries || []; + if (this.navigatorEnabled && baseSeries[0]) { + if (this.navigatorOptions.adaptToUpdatedData !== false) { + each(baseSeries, function (series) { + removeEvent(series, 'updatedData', this.updatedDataHandler); + }, this); + } + + // We only listen for extremes-events on the first baseSeries + if (baseSeries[0].xAxis) { + removeEvent( + baseSeries[0].xAxis, + 'foundExtremes', + this.modifyBaseAxisExtremes + ); + } + } + }, + + /** + * Initiate the Navigator object + */ + init: function (chart) { + var chartOptions = chart.options, + navigatorOptions = chartOptions.navigator, + navigatorEnabled = navigatorOptions.enabled, + scrollbarOptions = chartOptions.scrollbar, + scrollbarEnabled = scrollbarOptions.enabled, + height = navigatorEnabled ? navigatorOptions.height : 0, + scrollbarHeight = scrollbarEnabled ? scrollbarOptions.height : 0; + + this.handles = []; + this.shades = []; + + this.chart = chart; + this.setBaseSeries(); + + this.height = height; + this.scrollbarHeight = scrollbarHeight; + this.scrollbarEnabled = scrollbarEnabled; + this.navigatorEnabled = navigatorEnabled; + this.navigatorOptions = navigatorOptions; + this.scrollbarOptions = scrollbarOptions; + this.outlineHeight = height + scrollbarHeight; + + this.opposite = pick( + navigatorOptions.opposite, + !navigatorEnabled && chart.inverted + ); // #6262 + + var navigator = this, + baseSeries = navigator.baseSeries, + xAxisIndex = chart.xAxis.length, + yAxisIndex = chart.yAxis.length, + baseXaxis = baseSeries && baseSeries[0] && baseSeries[0].xAxis || + chart.xAxis[0]; + + // Make room for the navigator, can be placed around the chart: + chart.extraMargin = { + type: navigator.opposite ? 'plotTop' : 'marginBottom', + value: ( + navigatorEnabled || !chart.inverted ? + navigator.outlineHeight : + 0 + ) + navigatorOptions.margin + }; + if (chart.inverted) { + chart.extraMargin.type = navigator.opposite ? + 'marginRight' : + 'plotLeft'; + } + chart.isDirtyBox = true; + + if (navigator.navigatorEnabled) { + // an x axis is required for scrollbar also + navigator.xAxis = new Axis(chart, merge({ + // inherit base xAxis' break and ordinal options + breaks: baseXaxis.options.breaks, + ordinal: baseXaxis.options.ordinal + }, navigatorOptions.xAxis, { + id: 'navigator-x-axis', + yAxis: 'navigator-y-axis', + isX: true, + type: 'datetime', + index: xAxisIndex, + offset: 0, + keepOrdinalPadding: true, // #2436 + startOnTick: false, + endOnTick: false, + minPadding: 0, + maxPadding: 0, + zoomEnabled: false + }, chart.inverted ? { + offsets: [scrollbarHeight, 0, -scrollbarHeight, 0], + width: height + } : { + offsets: [0, -scrollbarHeight, 0, scrollbarHeight], + height: height + })); + + navigator.yAxis = new Axis(chart, merge(navigatorOptions.yAxis, { + id: 'navigator-y-axis', + alignTicks: false, + offset: 0, + index: yAxisIndex, + zoomEnabled: false + }, chart.inverted ? { + width: height + } : { + height: height + })); + + // If we have a base series, initialize the navigator series + if (baseSeries || navigatorOptions.series.data) { + navigator.updateNavigatorSeries(); + + // If not, set up an event to listen for added series + } else if (chart.series.length === 0) { + + wrap(chart, 'redraw', function (proceed, animation) { + // We've got one, now add it as base and reset chart.redraw + if (chart.series.length > 0 && !navigator.series) { + navigator.setBaseSeries(); + chart.redraw = proceed; // reset + } + proceed.call(chart, animation); + }); + } + + // Render items, so we can bind events to them: + navigator.renderElements(); + // Add mouse events + navigator.addMouseEvents(); + + // in case of scrollbar only, fake an x axis to get translation + } else { + navigator.xAxis = { + translate: function (value, reverse) { + var axis = chart.xAxis[0], + ext = axis.getExtremes(), + scrollTrackWidth = axis.len - 2 * scrollbarHeight, + min = numExt('min', axis.options.min, ext.dataMin), + valueRange = numExt( + 'max', + axis.options.max, + ext.dataMax + ) - min; + + return reverse ? + // from pixel to value + (value * valueRange / scrollTrackWidth) + min : + // from value to pixel + scrollTrackWidth * (value - min) / valueRange; + }, + toPixels: function (value) { + return this.translate(value); + }, + toValue: function (value) { + return this.translate(value, true); + }, + toFixedRange: Axis.prototype.toFixedRange, + fake: true + }; + } + + + // Initialize the scrollbar + if (chart.options.scrollbar.enabled) { + chart.scrollbar = navigator.scrollbar = new Scrollbar( + chart.renderer, + merge(chart.options.scrollbar, { + margin: navigator.navigatorEnabled ? 0 : 10, + vertical: chart.inverted + }), + chart + ); + addEvent(navigator.scrollbar, 'changed', function (e) { + var range = navigator.size, + to = range * this.to, + from = range * this.from; + + navigator.hasDragged = navigator.scrollbar.hasDragged; + navigator.render(0, 0, from, to); + + if ( + chart.options.scrollbar.liveRedraw || + ( + e.DOMType !== 'mousemove' && + e.DOMType !== 'touchmove' + ) + ) { + setTimeout(function () { + navigator.onMouseUp(e); + }); + } + }); + } + + // Add data events + navigator.addBaseSeriesEvents(); + // Add redraw events + navigator.addChartEvents(); + }, + + /** + * Get the union data extremes of the chart - the outer data extremes of the + * base X axis and the navigator axis. + * @param {boolean} returnFalseOnNoBaseSeries - as the param says. + */ + getUnionExtremes: function (returnFalseOnNoBaseSeries) { + var baseAxis = this.chart.xAxis[0], + navAxis = this.xAxis, + navAxisOptions = navAxis.options, + baseAxisOptions = baseAxis.options, + ret; + + if (!returnFalseOnNoBaseSeries || baseAxis.dataMin !== null) { + ret = { + dataMin: pick( // #4053 + navAxisOptions && navAxisOptions.min, + numExt( + 'min', + baseAxisOptions.min, + baseAxis.dataMin, + navAxis.dataMin, + navAxis.min + ) + ), + dataMax: pick( + navAxisOptions && navAxisOptions.max, + numExt( + 'max', + baseAxisOptions.max, + baseAxis.dataMax, + navAxis.dataMax, + navAxis.max + ) + ) + }; + } + return ret; + }, + + /** + * Set the base series and update the navigator series from this. With a bit + * of modification we should be able to make this an API method to be called + * from the outside + * @param {Object} baseSeriesOptions + * Additional series options for a navigator + * @param {Boolean} [redraw] + * Whether to redraw after update. + */ + setBaseSeries: function (baseSeriesOptions, redraw) { + var chart = this.chart, + baseSeries = this.baseSeries = []; + + baseSeriesOptions = ( + baseSeriesOptions || + chart.options && chart.options.navigator.baseSeries || + 0 + ); + + // Iterate through series and add the ones that should be shown in + // navigator. + each(chart.series || [], function (series, i) { + if ( + // Don't include existing nav series + !series.options.isInternal && + ( + series.options.showInNavigator || + ( + i === baseSeriesOptions || + series.options.id === baseSeriesOptions + ) && + series.options.showInNavigator !== false + ) + ) { + baseSeries.push(series); + } + }); + + // When run after render, this.xAxis already exists + if (this.xAxis && !this.xAxis.fake) { + this.updateNavigatorSeries(redraw); + } + }, + + /* + * Update series in the navigator from baseSeries, adding new if does not + * exist. + */ + updateNavigatorSeries: function (redraw) { + var navigator = this, + chart = navigator.chart, + baseSeries = navigator.baseSeries, + baseOptions, + mergedNavSeriesOptions, + chartNavigatorSeriesOptions = navigator.navigatorOptions.series, + baseNavigatorOptions, + navSeriesMixin = { + enableMouseTracking: false, + index: null, // #6162 + linkedTo: null, // #6734 + group: 'nav', // for columns + padXAxis: false, + xAxis: 'navigator-x-axis', + yAxis: 'navigator-y-axis', + showInLegend: false, + stacking: false, // #4823 + isInternal: true, + visible: true + }, + // Remove navigator series that are no longer in the baseSeries + navigatorSeries = navigator.series = H.grep( + navigator.series || [], function (navSeries) { + var base = navSeries.baseSeries; + if (H.inArray(base, baseSeries) < 0) { // Not in array + // If there is still a base series connected to this + // series, remove event handler and reference. + if (base) { + removeEvent( + base, + 'updatedData', + navigator.updatedDataHandler + ); + delete base.navigatorSeries; + } + // Kill the nav series + navSeries.destroy(); + return false; + } + return true; + } + ); + + // Go through each base series and merge the options to create new + // series + if (baseSeries && baseSeries.length) { + each(baseSeries, function eachBaseSeries(base) { + var linkedNavSeries = base.navigatorSeries, + userNavOptions = extend( + // Grab color from base as default + { + color: base.color + }, + !isArray(chartNavigatorSeriesOptions) ? + chartNavigatorSeriesOptions : + defaultOptions.navigator.series + ); + + // Don't update if the series exists in nav and we have disabled + // adaptToUpdatedData. + if ( + linkedNavSeries && + navigator.navigatorOptions.adaptToUpdatedData === false + ) { + return; + } + + navSeriesMixin.name = 'Navigator ' + baseSeries.length; + + baseOptions = base.options || {}; + baseNavigatorOptions = baseOptions.navigatorOptions || {}; + mergedNavSeriesOptions = merge( + baseOptions, + navSeriesMixin, + userNavOptions, + baseNavigatorOptions + ); + + // Merge data separately. Do a slice to avoid mutating the + // navigator options from base series (#4923). + var navigatorSeriesData = + baseNavigatorOptions.data || userNavOptions.data; + navigator.hasNavigatorData = + navigator.hasNavigatorData || !!navigatorSeriesData; + mergedNavSeriesOptions.data = + navigatorSeriesData || + baseOptions.data && baseOptions.data.slice(0); + + // Update or add the series + if (linkedNavSeries && linkedNavSeries.options) { + linkedNavSeries.update(mergedNavSeriesOptions, redraw); + } else { + base.navigatorSeries = chart.initSeries( + mergedNavSeriesOptions + ); + base.navigatorSeries.baseSeries = base; // Store ref + navigatorSeries.push(base.navigatorSeries); + } + }); + } + + // If user has defined data (and no base series) or explicitly defined + // navigator.series as an array, we create these series on top of any + // base series. + if ( + chartNavigatorSeriesOptions.data && + !(baseSeries && baseSeries.length) || + isArray(chartNavigatorSeriesOptions) + ) { + navigator.hasNavigatorData = false; + // Allow navigator.series to be an array + chartNavigatorSeriesOptions = H.splat(chartNavigatorSeriesOptions); + each(chartNavigatorSeriesOptions, function (userSeriesOptions, i) { + navSeriesMixin.name = + 'Navigator ' + (navigatorSeries.length + 1); + mergedNavSeriesOptions = merge( + defaultOptions.navigator.series, + { + // Since we don't have a base series to pull color from, + // try to fake it by using color from series with same + // index. Otherwise pull from the colors array. We need + // an explicit color as otherwise updates will increment + // color counter and we'll get a new color for each + // update of the nav series. + color: chart.series[i] && + !chart.series[i].options.isInternal && + chart.series[i].color || + chart.options.colors[i] || + chart.options.colors[0] + }, + navSeriesMixin, + userSeriesOptions + ); + mergedNavSeriesOptions.data = userSeriesOptions.data; + if (mergedNavSeriesOptions.data) { + navigator.hasNavigatorData = true; + navigatorSeries.push( + chart.initSeries(mergedNavSeriesOptions) + ); + } + }); + } + + this.addBaseSeriesEvents(); + }, + + /** + * Add data events. + * For example when main series is updated we need to recalculate extremes + */ + addBaseSeriesEvents: function () { + var navigator = this, + baseSeries = navigator.baseSeries || []; + + // Bind modified extremes event to first base's xAxis only. + // In event of > 1 base-xAxes, the navigator will ignore those. + // Adding this multiple times to the same axis is no problem, as + // duplicates should be discarded by the browser. + if (baseSeries[0] && baseSeries[0].xAxis) { + addEvent( + baseSeries[0].xAxis, + 'foundExtremes', + this.modifyBaseAxisExtremes + ); + } + + each(baseSeries, function (base) { + // Link base series show/hide to navigator series visibility + addEvent(base, 'show', function () { + if (this.navigatorSeries) { + this.navigatorSeries.setVisible(true, false); + } + }); + addEvent(base, 'hide', function () { + if (this.navigatorSeries) { + this.navigatorSeries.setVisible(false, false); + } + }); + + // Respond to updated data in the base series, unless explicitily + // not adapting to data changes. + if (this.navigatorOptions.adaptToUpdatedData !== false) { + if (base.xAxis) { + addEvent(base, 'updatedData', this.updatedDataHandler); + } + } + + // Handle series removal + addEvent(base, 'remove', function () { + if (this.navigatorSeries) { + erase(navigator.series, this.navigatorSeries); + this.navigatorSeries.remove(false); + delete this.navigatorSeries; + } + }); + }, this); + }, + + /** + * Set the navigator x axis extremes to reflect the total. The navigator + * extremes should always be the extremes of the union of all series in the + * chart as well as the navigator series. + */ + modifyNavigatorAxisExtremes: function () { + var xAxis = this.xAxis, + unionExtremes; + + if (xAxis.getExtremes) { + unionExtremes = this.getUnionExtremes(true); + if ( + unionExtremes && + ( + unionExtremes.dataMin !== xAxis.min || + unionExtremes.dataMax !== xAxis.max + ) + ) { + xAxis.min = unionExtremes.dataMin; + xAxis.max = unionExtremes.dataMax; + } + } + }, + + /** + * Hook to modify the base axis extremes with information from the Navigator + */ + modifyBaseAxisExtremes: function () { + var baseXAxis = this, + navigator = baseXAxis.chart.navigator, + baseExtremes = baseXAxis.getExtremes(), + baseMin = baseExtremes.min, + baseMax = baseExtremes.max, + baseDataMin = baseExtremes.dataMin, + baseDataMax = baseExtremes.dataMax, + range = baseMax - baseMin, + stickToMin = navigator.stickToMin, + stickToMax = navigator.stickToMax, + overscroll = baseXAxis.options.overscroll, + newMax, + newMin, + navigatorSeries = navigator.series && navigator.series[0], + hasSetExtremes = !!baseXAxis.setExtremes, + + // When the extremes have been set by range selector button, don't + // stick to min or max. The range selector buttons will handle the + // extremes. (#5489) + unmutable = baseXAxis.eventArgs && + baseXAxis.eventArgs.trigger === 'rangeSelectorButton'; + + if (!unmutable) { + + // If the zoomed range is already at the min, move it to the right + // as new data comes in + if (stickToMin) { + newMin = baseDataMin; + newMax = newMin + range; + } + + // If the zoomed range is already at the max, move it to the right + // as new data comes in + if (stickToMax) { + newMax = baseDataMax + overscroll; + + // if stickToMin is true, the new min value is set above + if (!stickToMin) { + newMin = Math.max( + newMax - range, + navigatorSeries && navigatorSeries.xData ? + navigatorSeries.xData[0] : -Number.MAX_VALUE + ); + } + } + + // Update the extremes + if (hasSetExtremes && (stickToMin || stickToMax)) { + if (isNumber(newMin)) { + baseXAxis.min = baseXAxis.userMin = newMin; + baseXAxis.max = baseXAxis.userMax = newMax; + } + } + } + + // Reset + navigator.stickToMin = navigator.stickToMax = null; + }, + + /** + * Handler for updated data on the base series. When data is modified, the + * navigator series must reflect it. This is called from the Chart.redraw + * function before axis and series extremes are computed. + */ + updatedDataHandler: function () { + var navigator = this.chart.navigator, + baseSeries = this, + navigatorSeries = this.navigatorSeries; + + // If the scrollbar is scrolled all the way to the right, keep right as + // new data comes in. + navigator.stickToMax = + Math.round(navigator.zoomedMax) >= Math.round(navigator.size); + + // Detect whether the zoomed area should stick to the minimum or + // maximum. If the current axis minimum falls outside the new updated + // dataset, we must adjust. + navigator.stickToMin = isNumber(baseSeries.xAxis.min) && + (baseSeries.xAxis.min <= baseSeries.xData[0]) && + (!this.chart.fixedRange || !navigator.stickToMax); + + // Set the navigator series data to the new data of the base series + if (navigatorSeries && !navigator.hasNavigatorData) { + navigatorSeries.options.pointStart = baseSeries.xData[0]; + navigatorSeries.setData( + baseSeries.options.data, + false, + null, + false + ); // #5414 + } + }, + + /** + * Add chart events, like redrawing navigator, when chart requires that. + */ + addChartEvents: function () { + addEvent(this.chart, 'redraw', function () { + // Move the scrollbar after redraw, like after data updata even if + // axes don't redraw + var navigator = this.navigator, + xAxis = navigator && ( + navigator.baseSeries && + navigator.baseSeries[0] && + navigator.baseSeries[0].xAxis || + navigator.scrollbar && this.xAxis[0] + ); // #5709 + + if (xAxis) { + navigator.render(xAxis.min, xAxis.max); + } + }); + }, + + /** + * Destroys allocated elements. + */ + destroy: function () { + + // Disconnect events added in addEvents + this.removeEvents(); + + if (this.xAxis) { + erase(this.chart.xAxis, this.xAxis); + erase(this.chart.axes, this.xAxis); + } + if (this.yAxis) { + erase(this.chart.yAxis, this.yAxis); + erase(this.chart.axes, this.yAxis); + } + // Destroy series + each(this.series || [], function (s) { + if (s.destroy) { + s.destroy(); + } + }); + + // Destroy properties + each([ + 'series', 'xAxis', 'yAxis', 'shades', 'outline', 'scrollbarTrack', + 'scrollbarRifles', 'scrollbarGroup', 'scrollbar', 'navigatorGroup', + 'rendered' + ], function (prop) { + if (this[prop] && this[prop].destroy) { + this[prop].destroy(); + } + this[prop] = null; + }, this); + + // Destroy elements in collection + each([this.handles], function (coll) { + destroyObjectProperties(coll); + }, this); + } +}; + +H.Navigator = Navigator; + +/** + * For Stock charts, override selection zooming with some special features + * because X axis zooming is already allowed by the Navigator and Range + * selector. + */ +wrap(Axis.prototype, 'zoom', function (proceed, newMin, newMax) { + var chart = this.chart, + chartOptions = chart.options, + zoomType = chartOptions.chart.zoomType, + previousZoom, + navigator = chartOptions.navigator, + rangeSelector = chartOptions.rangeSelector, + ret; + + if (this.isXAxis && ((navigator && navigator.enabled) || + (rangeSelector && rangeSelector.enabled))) { + + // For x only zooming, fool the chart.zoom method not to create the zoom + // button because the property already exists + if (zoomType === 'x') { + chart.resetZoomButton = 'blocked'; + + // For y only zooming, ignore the X axis completely + } else if (zoomType === 'y') { + ret = false; + + // For xy zooming, record the state of the zoom before zoom selection, + // then when the reset button is pressed, revert to this state. This + // should apply only if the chart is initialized with a range (#6612), + // otherwise zoom all the way out. + } else if (zoomType === 'xy' && this.options.range) { + + previousZoom = this.previousZoom; + if (defined(newMin)) { + this.previousZoom = [this.min, this.max]; + } else if (previousZoom) { + newMin = previousZoom[0]; + newMax = previousZoom[1]; + delete this.previousZoom; + } + } + + } + return ret !== undefined ? ret : proceed.call(this, newMin, newMax); +}); + +// Initialize navigator for stock charts +wrap(Chart.prototype, 'init', function (proceed, options, callback) { + + addEvent(this, 'beforeRender', function () { + var options = this.options; + if (options.navigator.enabled || options.scrollbar.enabled) { + this.scroller = this.navigator = new Navigator(this); + } + }); + + proceed.call(this, options, callback); + +}); + +/** + * For stock charts, extend the Chart.setChartSize method so that we can set the + * final top position of the navigator once the height of the chart, including + * the legend, is determined. #367. We can't use Chart.getMargins, because + * labels offsets are not calculated yet. + */ +wrap(Chart.prototype, 'setChartSize', function (proceed) { + + var legend = this.legend, + navigator = this.navigator, + scrollbarHeight, + legendOptions, + xAxis, + yAxis; + + proceed.apply(this, [].slice.call(arguments, 1)); + + if (navigator) { + legendOptions = legend && legend.options; + xAxis = navigator.xAxis; + yAxis = navigator.yAxis; + scrollbarHeight = navigator.scrollbarHeight; + + // Compute the top position + if (this.inverted) { + navigator.left = navigator.opposite ? + this.chartWidth - scrollbarHeight - navigator.height : + this.spacing[3] + scrollbarHeight; + navigator.top = this.plotTop + scrollbarHeight; + } else { + navigator.left = this.plotLeft + scrollbarHeight; + navigator.top = navigator.navigatorOptions.top || + this.chartHeight - + navigator.height - + scrollbarHeight - + this.spacing[2] - + ( + this.rangeSelector && this.extraBottomMargin ? + this.rangeSelector.getHeight() : + 0 + ) - + ( + ( + legendOptions && + legendOptions.verticalAlign === 'bottom' && + legendOptions.enabled && + !legendOptions.floating + ) ? + legend.legendHeight + pick(legendOptions.margin, 10) : + 0 + ); + } + + if (xAxis && yAxis) { // false if navigator is disabled (#904) + + if (this.inverted) { + xAxis.options.left = yAxis.options.left = navigator.left; + } else { + xAxis.options.top = yAxis.options.top = navigator.top; + } + + xAxis.setAxisSize(); + yAxis.setAxisSize(); + } + } +}); + +// Pick up badly formatted point options to addPoint +wrap(Series.prototype, 'addPoint', function ( + proceed, + options, + redraw, + shift, + animation +) { + var turboThreshold = this.options.turboThreshold; + if ( + turboThreshold && + this.xData.length > turboThreshold && + isObject(options, true) && + this.chart.navigator + ) { + error(20, true); + } + proceed.call(this, options, redraw, shift, animation); +}); + +// Handle adding new series +wrap(Chart.prototype, 'addSeries', function ( + proceed, + options, + redraw, + animation +) { + var series = proceed.call(this, options, false, animation); + if (this.navigator) { + // Recompute which series should be shown in navigator, and add them + this.navigator.setBaseSeries(null, false); + } + if (pick(redraw, true)) { + this.redraw(); + } + return series; +}); + +// Handle updating series +wrap(Series.prototype, 'update', function (proceed, newOptions, redraw) { + proceed.call(this, newOptions, false); + if (this.chart.navigator && !this.options.isInternal) { + this.chart.navigator.setBaseSeries(null, false); + } + if (pick(redraw, true)) { + this.chart.redraw(); + } +}); + +Chart.prototype.callbacks.push(function (chart) { + var extremes, + navigator = chart.navigator; + + // Initiate the navigator + if (navigator) { + extremes = chart.xAxis[0].getExtremes(); + navigator.render(extremes.min, extremes.max); + } +}); + + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var addEvent = H.addEvent, + Axis = H.Axis, + Chart = H.Chart, + css = H.css, + createElement = H.createElement, + defaultOptions = H.defaultOptions, + defined = H.defined, + destroyObjectProperties = H.destroyObjectProperties, + discardElement = H.discardElement, + each = H.each, + extend = H.extend, + fireEvent = H.fireEvent, + isNumber = H.isNumber, + merge = H.merge, + pick = H.pick, + pInt = H.pInt, + splat = H.splat, + wrap = H.wrap; + +/* **************************************************************************** + * Start Range Selector code * + *****************************************************************************/ +extend(defaultOptions, { + + /** + * The range selector is a tool for selecting ranges to display within + * the chart. It provides buttons to select preconfigured ranges in + * the chart, like 1 day, 1 week, 1 month etc. It also provides input + * boxes where min and max dates can be manually input. + * + * @product highstock + * @optionparent rangeSelector + */ + rangeSelector: { + // allButtonsEnabled: false, + // enabled: true, + // buttons: {Object} + // buttonSpacing: 0, + + /** + * The vertical alignment of the rangeselector box. Allowed properties are `top`, + * `middle`, `bottom`. + * + * @since 6.0.0 + * + * @sample {highstock} stock/rangeselector/vertical-align-middle/ Middle + * + * @sample {highstock} stock/rangeselector/vertical-align-bottom/ Bottom + */ + verticalAlign: 'top', + + /** + * A collection of attributes for the buttons. The object takes SVG + * attributes like `fill`, `stroke`, `stroke-width`, as well as `style`, + * a collection of CSS properties for the text. + * + * The object can also be extended with states, so you can set presentational + * options for `hover`, `select` or `disabled` button states. + * + * CSS styles for the text label. + * + * In styled mode, the buttons are styled by the `.highcharts- + * range-selector-buttons .highcharts-button` rule with its different + * states. + * + * @type {Object} + * @sample {highstock} stock/rangeselector/styling/ Styling the buttons and inputs + * @product highstock + */ + buttonTheme: { + 'stroke-width': 0, + width: 28, + height: 18, + padding: 2, + zIndex: 7 // #484, #852 + }, + + /** + * When the rangeselector is floating, the plot area does not reserve + * space for it. This opens for positioning anywhere on the chart. + * + * @sample {highstock} stock/rangeselector/floating/ + * Placing the range selector between the plot area and the + * navigator + * @since 6.0.0 + * @product highstock + */ + floating: false, + + /** + * The x offset of the range selector relative to its horizontal + * alignment within `chart.spacingLeft` and `chart.spacingRight`. + * + * @since 6.0.0 + * @product highstock + */ + x: 0, + + /** + * The y offset of the range selector relative to its horizontal + * alignment within `chart.spacingLeft` and `chart.spacingRight`. + * + * @since 6.0.0 + * @product highstock + */ + y: 0, + + /** + * Deprecated. The height of the range selector. Currently it is + * calculated dynamically. + * + * @type {Number} + * @default undefined + * @since 2.1.9 + * @product highstock + * @deprecated true + */ + height: undefined, // reserved space for buttons and input + + /** + * Positioning for the input boxes. Allowed properties are `align`, + * `x` and `y`. + * + * @type {Object} + * @default { align: "right" } + * @since 1.2.4 + * @product highstock + */ + inputPosition: { + /** + * The alignment of the input box. Allowed properties are `left`, + * `center`, `right`. + * @validvalue ["left", "center", "right"] + * @sample {highstock} stock/rangeselector/input-button-position/ + * Alignment + * @since 6.0.0 + */ + align: 'right', + x: 0, + y: 0 + }, + + /** + * Positioning for the button row. + * + * @since 1.2.4 + * @product highstock + */ + buttonPosition: { + /** + * The alignment of the input box. Allowed properties are `left`, + * `center`, `right`. + * + * @validvalue ["left", "center", "right"] + * @sample {highstock} stock/rangeselector/input-button-position/ + * Alignment + * @since 6.0.0 + */ + align: 'left', + /** + * X offset of the button row. + */ + x: 0, + /** + * Y offset of the button row. + */ + y: 0 + }, + // inputDateFormat: '%b %e, %Y', + // inputEditDateFormat: '%Y-%m-%d', + // inputEnabled: true, + // selected: undefined, + + // inputStyle: {}, + + /** + * CSS styles for the labels - the Zoom, From and To texts. + * + * In styled mode, the labels are styled by the `.highcharts-range-label` class. + * + * @type {CSSObject} + * @sample {highstock} stock/rangeselector/styling/ Styling the buttons and inputs + * @product highstock + */ + labelStyle: { + color: '#666666' + } + + } +}); + +defaultOptions.lang = merge( + defaultOptions.lang, + /** + * Language object. The language object is global and it can't be set + * on each chart initiation. Instead, use `Highcharts.setOptions` to + * set it before any chart is initialized. + * + *
Highcharts.setOptions({
+	 *     lang: {
+	 *         months: [
+	 *             'Janvier', 'Février', 'Mars', 'Avril',
+	 *             'Mai', 'Juin', 'Juillet', 'Août',
+	 *             'Septembre', 'Octobre', 'Novembre', 'Décembre'
+	 *         ],
+	 *         weekdays: [
+	 *             'Dimanche', 'Lundi', 'Mardi', 'Mercredi',
+	 *             'Jeudi', 'Vendredi', 'Samedi'
+	 *         ]
+	 *     }
+	 * });
+ * + * @optionparent lang + * @product highstock + */ + { + + /** + * The text for the label for the range selector buttons. + * + * @type {String} + * @default Zoom + * @product highstock + */ + rangeSelectorZoom: 'Zoom', + + /** + * The text for the label for the "from" input box in the range + * selector. + * + * @type {String} + * @default From + * @product highstock + */ + rangeSelectorFrom: 'From', + + /** + * The text for the label for the "to" input box in the range selector. + * + * @type {String} + * @default To + * @product highstock + */ + rangeSelectorTo: 'To' + } +); + +/** + * The range selector. + * @class + * @param {Object} chart + */ +function RangeSelector(chart) { + + // Run RangeSelector + this.init(chart); +} + +RangeSelector.prototype = { + /** + * The method to run when one of the buttons in the range selectors is clicked + * @param {Number} i The index of the button + * @param {Object} rangeOptions + * @param {Boolean} redraw + */ + clickButton: function (i, redraw) { + var rangeSelector = this, + chart = rangeSelector.chart, + rangeOptions = rangeSelector.buttonOptions[i], + baseAxis = chart.xAxis[0], + unionExtremes = (chart.scroller && chart.scroller.getUnionExtremes()) || baseAxis || {}, + dataMin = unionExtremes.dataMin, + dataMax = unionExtremes.dataMax, + newMin, + newMax = baseAxis && Math.round(Math.min(baseAxis.max, pick(dataMax, baseAxis.max))), // #1568 + type = rangeOptions.type, + baseXAxisOptions, + range = rangeOptions._range, + rangeMin, + minSetting, + rangeSetting, + ctx, + ytdExtremes, + dataGrouping = rangeOptions.dataGrouping; + + if (dataMin === null || dataMax === null) { // chart has no data, base series is removed + return; + } - (c) 2009-2014 Torstein Honsi + // Set the fixed range before range is altered + chart.fixedRange = range; + + // Apply dataGrouping associated to button + if (dataGrouping) { + this.forcedDataGrouping = true; + Axis.prototype.setDataGrouping.call(baseAxis || { chart: this.chart }, dataGrouping, false); + } - License: www.highcharts.com/license -*/ -(function(){function s(a,b){var c;a||(a={});for(c in b)a[c]=b[c];return a}function y(){var a,b=arguments,c,d={},e=function(a,b){var c,d;typeof a!=="object"&&(a={});for(d in b)b.hasOwnProperty(d)&&(c=b[d],a[d]=c&&typeof c==="object"&&Object.prototype.toString.call(c)!=="[object Array]"&&d!=="renderTo"&&typeof c.nodeType!=="number"?e(a[d]||{},c):b[d]);return a};b[0]===!0&&(d=b[1],b=Array.prototype.slice.call(b,2));c=b.length;for(a=0;a3?k.length%3:0;return e!==Da?e(a,b,c,d):j+(l?k.substr(0,l)+f:"")+k.substr(l).replace(/(\d{3})(?=\d)/g,"$1"+f)+(h?i+P(g-k).toFixed(h).slice(2):"")}function Ta(a,b){return Array((b||2)+1-String(a).length).join(0)+a}function Q(a,b,c){var d=a[b];a[b]=function(){var a=Array.prototype.slice.call(arguments); -a.unshift(d);return c.apply(this,a)}}function Ma(a,b){for(var c="{",d=!1,e,f,g,h,i,j=[];(c=a.indexOf(c))!==-1;){e=a.slice(0,c);if(d){f=e.split(":");g=f.shift().split(".");i=g.length;e=b;for(h=0;h-1?h.thousandsSep:""))):e=xa(f,e)}j.push(e);a=a.slice(c+1);c=(d=!d)?"}":"{"}j.push(a);return j.join("")}function vb(a){return X.pow(10,V(X.log(a)/ -X.LN10))}function wb(a,b,c,d){var e,c=q(c,1);e=a/c;b||(b=[1,2,2.5,5,10],d===!1&&(c===1?b=[1,2,5,10]:c<=0.1&&(b=[1/c])));for(d=0;dc&&(c=a[b]);return c} -function Na(a,b){for(var c in a)a[c]&&a[c]!==b&&a[c].destroy&&a[c].destroy(),delete a[c]}function Va(a){kb||(kb=aa(Wa));a&&kb.appendChild(a);kb.innerHTML=""}function ja(a){return parseFloat(a.toPrecision(14))}function ab(a,b){Fa=q(a,b.animation)}function Mb(){var a=F.global.useUTC,b=a?"getUTC":"get",c=a?"setUTC":"set";ca=F.global.Date||window.Date;Oa=(a&&F.global.timezoneOffset||0)*6E4;lb=a?ca.UTC:function(a,b,c,g,h,i){return(new ca(a,b,q(c,1),q(g,0),q(h,0),q(i,0))).getTime()};yb=b+"Minutes";zb=b+ -"Hours";Ab=b+"Day";Xa=b+"Date";mb=b+"Month";nb=b+"FullYear";Nb=c+"Minutes";Ob=c+"Hours";Bb=c+"Date";Pb=c+"Month";Qb=c+"FullYear"}function Z(){}function bb(a,b,c,d){this.axis=a;this.pos=b;this.type=c||"";this.isNew=!0;!c&&!d&&this.addLabel()}function D(){this.init.apply(this,arguments)}function ya(){this.init.apply(this,arguments)}function Rb(a,b,c,d,e){var f=a.chart.inverted;this.axis=a;this.isNegative=c;this.options=b;this.x=d;this.total=null;this.points={};this.stack=e;this.alignOptions={align:b.align|| -(f?c?"left":"right":"center"),verticalAlign:b.verticalAlign||(f?"middle":c?"bottom":"top"),y:q(b.y,f?4:c?14:-6),x:q(b.x,f?c?-6:6:0)};this.textAlign=b.textAlign||(f?c?"right":"left":"center")}function Cb(a){var b=a.options,c=b.navigator,d=c.enabled,b=b.scrollbar,e=b.enabled,f=d?c.height:0,g=e?b.height:0;this.handles=[];this.scrollbarButtons=[];this.elementsToDestroy=[];this.chart=a;this.setBaseSeries();this.height=f;this.scrollbarHeight=g;this.scrollbarEnabled=e;this.navigatorEnabled=d;this.navigatorOptions= -c;this.scrollbarOptions=b;this.outlineHeight=f+g;this.init()}function Db(a){this.init(a)}var r,E=document,S=window,X=Math,t=X.round,V=X.floor,Ya=X.ceil,v=X.max,A=X.min,P=X.abs,fa=X.cos,ka=X.sin,ua=X.PI,Pa=ua*2/360,Ga=navigator.userAgent,Sb=S.opera,La=/msie/i.test(Ga)&&!Sb,ob=E.documentMode===8,Eb=/AppleWebKit/.test(Ga),cb=/Firefox/.test(Ga),fb=/(Mobile|Android|Windows Phone)/.test(Ga),Ha="http://www.w3.org/2000/svg",ea=!!E.createElementNS&&!!E.createElementNS(Ha,"svg").createSVGRect,Yb=cb&&parseInt(Ga.split("Firefox/")[1], -10)<4,la=!ea&&!La&&!!E.createElement("canvas").getContext,Za,db,Tb={},Fb=0,kb,F,xa,Fa,Gb,G,ma,ra=function(){return r},ba=[],gb=0,Wa="div",$="none",Zb=/^[0-9]+$/,$b="stroke-width",ca,lb,Oa,yb,zb,Ab,Xa,mb,nb,Nb,Ob,Bb,Pb,Qb,B={},N;S.Highcharts?ma(16,!0):N=S.Highcharts={};xa=function(a,b,c){if(!u(b)||isNaN(b))return"Invalid date";var a=q(a,"%Y-%m-%d %H:%M:%S"),d=new ca(b-Oa),e,f=d[zb](),g=d[Ab](),h=d[Xa](),i=d[mb](),j=d[nb](),k=F.lang,l=k.weekdays,d=s({a:l[g].substr(0,3),A:l[g],d:Ta(h),e:h,b:k.shortMonths[i], -B:k.months[i],m:Ta(i+1),y:j.toString().substr(2,2),Y:j,H:Ta(f),I:Ta(f%12||12),l:f%12||12,M:Ta(d[yb]()),p:f<12?"AM":"PM",P:f<12?"am":"pm",S:Ta(d.getSeconds()),L:Ta(t(b%1E3),3)},N.dateFormats);for(e in d)for(;a.indexOf("%"+e)!==-1;)a=a.replace("%"+e,typeof d[e]==="function"?d[e](b):d[e]);return c?a.substr(0,1).toUpperCase()+a.substr(1):a};ma=function(a,b){var c="Highcharts error #"+a+": www.highcharts.com/errors/"+a;if(b)throw c;S.console&&console.log(c)};G={millisecond:1,second:1E3,minute:6E4,hour:36E5, -day:864E5,week:6048E5,month:26784E5,year:31556952E3};Gb={init:function(a,b,c){var b=b||"",d=a.shift,e=b.indexOf("C")>-1,f=e?7:3,g,b=b.split(" "),c=[].concat(c),h,i,j=function(a){for(g=a.length;g--;)a[g]==="M"&&a.splice(g+1,0,a[g+1],a[g+2],a[g+1],a[g+2])};e&&(j(b),j(c));a.isArea&&(h=b.splice(b.length-6,6),i=c.splice(c.length-6,6));if(d<=c.length/f&&b.length===c.length)for(;d--;)c=[].concat(c).splice(0,f).concat(c);a.shift=0;if(b.length)for(a=c.length;b.length{point.key}
',pointFormat:' {series.name}: {point.y}
',shadow:!0,snap:fb?25:10,style:{color:"#333333",cursor:"default",fontSize:"12px",padding:"8px",whiteSpace:"nowrap"}},credits:{enabled:!0,text:"Highcharts.com",href:"http://www.highcharts.com",position:{align:"right",x:-10,verticalAlign:"bottom", -y:-5},style:{cursor:"pointer",color:"#909090",fontSize:"9px"}}};var W=F.plotOptions,L=W.line;Mb();var dc=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/,ec=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,fc=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/,Ia=function(a){var b=[],c,d;(function(a){a&&a.stops?d=za(a.stops,function(a){return Ia(a[1])}):(c=dc.exec(a))?b=[H(c[1]),H(c[2]),H(c[3]),parseFloat(c[4],10)]:(c=ec.exec(a))?b=[H(c[1], -16),H(c[2],16),H(c[3],16),1]:(c=fc.exec(a))&&(b=[H(c[1]),H(c[2]),H(c[3]),1])})(a);return{get:function(c){var f;d?(f=y(a),f.stops=[].concat(f.stops),p(d,function(a,b){f.stops[b]=[f.stops[b][0],a.get(c)]})):f=b&&!isNaN(b[0])?c==="rgb"?"rgb("+b[0]+","+b[1]+","+b[2]+")":c==="a"?b[3]:"rgba("+b.join(",")+")":a;return f},brighten:function(a){if(d)p(d,function(b){b.brighten(a)});else if(pa(a)&&a!==0){var c;for(c=0;c<3;c++)b[c]+=H(a*255),b[c]<0&&(b[c]=0),b[c]>255&&(b[c]=255)}return this},rgba:b,setOpacity:function(a){b[3]= -a;return this}}};Z.prototype={opacity:1,textProps:"fontSize,fontWeight,fontFamily,color,lineHeight,width,textDecoration,textShadow,HcTextStroke".split(","),init:function(a,b){this.element=b==="span"?aa(b):E.createElementNS(Ha,b);this.renderer=a},animate:function(a,b,c){b=q(b,Fa,!0);hb(this);if(b){b=y(b,{});if(c)b.complete=c;rb(this,a,b)}else this.attr(a),c&&c();return this},colorGradient:function(a,b,c){var d=this.renderer,e,f,g,h,i,j,k,l,m,n,o=[];a.linearGradient?f="linearGradient":a.radialGradient&& -(f="radialGradient");if(f){g=a[f];h=d.gradients;j=a.stops;m=c.radialReference;Ja(g)&&(a[f]=g={x1:g[0],y1:g[1],x2:g[2],y2:g[3],gradientUnits:"userSpaceOnUse"});f==="radialGradient"&&m&&!u(g.gradientUnits)&&(g=y(g,{cx:m[0]-m[2]/2+g.cx*m[2],cy:m[1]-m[2]/2+g.cy*m[2],r:g.r*m[2],gradientUnits:"userSpaceOnUse"}));for(n in g)n!=="id"&&o.push(n,g[n]);for(n in j)o.push(j[n]);o=o.join(",");h[o]?a=h[o].attr("id"):(g.id=a="highcharts-"+Fb++,h[o]=i=d.createElement(f).attr(g).add(d.defs),i.stops=[],p(j,function(a){a[1].indexOf("rgba")=== -0?(e=Ia(a[1]),k=e.get("rgb"),l=e.get("a")):(k=a[1],l=1);a=d.createElement("stop").attr({offset:a[0],"stop-color":k,"stop-opacity":l}).add(i);i.stops.push(a)}));c.setAttribute(b,"url("+d.url+"#"+a+")")}},attr:function(a,b){var c,d,e=this.element,f,g=this,h;typeof a==="string"&&b!==r&&(c=a,a={},a[c]=b);if(typeof a==="string")g=(this[a+"Getter"]||this._defaultGetter).call(this,a,e);else{for(c in a){d=a[c];h=!1;this.symbolName&&/^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(c)&&(f||(this.symbolAttr(a), -f=!0),h=!0);if(this.rotation&&(c==="x"||c==="y"))this.doTransform=!0;h||(this[c+"Setter"]||this._defaultSetter).call(this,d,c,e);this.shadows&&/^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(c)&&this.updateShadows(c,d)}if(this.doTransform)this.updateTransform(),this.doTransform=!1}return g},updateShadows:function(a,b){for(var c=this.shadows,d=c.length;d--;)c[d].setAttribute(a,a==="height"?v(b-(c[d].cutHeight||0),0):a==="d"?this.d:b)},addClass:function(a){var b=this.element,c=Y(b,"class")|| -"";c.indexOf(a)===-1&&Y(b,"class",c+" "+a);return this},symbolAttr:function(a){var b=this;p("x,y,r,start,end,width,height,innerR,anchorX,anchorY".split(","),function(c){b[c]=q(a[c],b[c])});b.attr({d:b.renderer.symbols[b.symbolName](b.x,b.y,b.width,b.height,b)})},clip:function(a){return this.attr("clip-path",a?"url("+this.renderer.url+"#"+a.id+")":$)},crisp:function(a){var b,c={},d,e=a.strokeWidth||this.strokeWidth||0;d=t(e)%2/2;a.x=V(a.x||this.x||0)+d;a.y=V(a.y||this.y||0)+d;a.width=V((a.width||this.width|| -0)-2*d);a.height=V((a.height||this.height||0)-2*d);a.strokeWidth=e;for(b in a)this[b]!==a[b]&&(this[b]=c[b]=a[b]);return c},css:function(a){var b=this.styles,c={},d=this.element,e,f,g="";e=!b;if(a&&a.color)a.fill=a.color;if(b)for(f in a)a[f]!==b[f]&&(c[f]=a[f],e=!0);if(e){e=this.textWidth=a&&a.width&&d.nodeName.toLowerCase()==="text"&&H(a.width);b&&(a=s(b,c));this.styles=a;e&&(la||!ea&&this.renderer.forExport)&&delete a.width;if(La&&!ea)K(this.element,a);else{b=function(a,b){return"-"+b.toLowerCase()}; -for(f in a)g+=f.replace(/([A-Z])/g,b)+":"+a[f]+";";Y(d,"style",g)}e&&this.added&&this.renderer.buildText(this)}return this},on:function(a,b){var c=this,d=c.element;db&&a==="click"?(d.ontouchstart=function(a){c.touchEventFired=ca.now();a.preventDefault();b.call(d,a)},d.onclick=function(a){(Ga.indexOf("Android")===-1||ca.now()-(c.touchEventFired||0)>1100)&&b.call(d,a)}):d["on"+a]=b;return this},setRadialReference:function(a){this.element.radialReference=a;return this},translate:function(a,b){return this.attr({translateX:a, -translateY:b})},invert:function(){this.inverted=!0;this.updateTransform();return this},updateTransform:function(){var a=this.translateX||0,b=this.translateY||0,c=this.scaleX,d=this.scaleY,e=this.inverted,f=this.rotation,g=this.element;e&&(a+=this.attr("width"),b+=this.attr("height"));a=["translate("+a+","+b+")"];e?a.push("rotate(90) scale(-1,1)"):f&&a.push("rotate("+f+" "+(g.getAttribute("x")||0)+" "+(g.getAttribute("y")||0)+")");(u(c)||u(d))&&a.push("scale("+q(c,1)+" "+q(d,1)+")");a.length&&g.setAttribute("transform", -a.join(" "))},toFront:function(){var a=this.element;a.parentNode.appendChild(a);return this},align:function(a,b,c){var d,e,f,g,h={};e=this.renderer;f=e.alignedObjects;if(a){if(this.alignOptions=a,this.alignByTranslate=b,!c||Sa(c))this.alignTo=d=c||"renderer",ta(f,this),f.push(this),c=null}else a=this.alignOptions,b=this.alignByTranslate,d=this.alignTo;c=q(c,e[d],e);d=a.align;e=a.verticalAlign;f=(c.x||0)+(a.x||0);g=(c.y||0)+(a.y||0);if(d==="right"||d==="center")f+=(c.width-(a.width||0))/{right:1,center:2}[d]; -h[b?"translateX":"x"]=t(f);if(e==="bottom"||e==="middle")g+=(c.height-(a.height||0))/({bottom:1,middle:2}[e]||1);h[b?"translateY":"y"]=t(g);this[this.placed?"animate":"attr"](h);this.placed=!0;this.alignAttr=h;return this},getBBox:function(){var a=this.bBox,b=this.renderer,c,d,e=this.rotation;c=this.element;var f=this.styles,g=e*Pa;d=this.textStr;var h;if(d===""||Zb.test(d))h="num."+d.toString().length+(f?"|"+f.fontSize+"|"+f.fontFamily:"");h&&(a=b.cache[h]);if(!a){if(c.namespaceURI===Ha||b.forExport){try{a= -c.getBBox?s({},c.getBBox()):{width:c.offsetWidth,height:c.offsetHeight}}catch(i){}if(!a||a.width<0)a={width:0,height:0}}else a=this.htmlGetBBox();if(b.isSVG){c=a.width;d=a.height;if(La&&f&&f.fontSize==="11px"&&d.toPrecision(3)==="16.9")a.height=d=14;if(e)a.width=P(d*ka(g))+P(c*fa(g)),a.height=P(d*fa(g))+P(c*ka(g))}this.bBox=a;h&&(b.cache[h]=a)}return a},show:function(a){a&&this.element.namespaceURI===Ha?this.element.removeAttribute("visibility"):this.attr({visibility:a?"inherit":"visible"});return this}, -hide:function(){return this.attr({visibility:"hidden"})},fadeOut:function(a){var b=this;b.animate({opacity:0},{duration:a||150,complete:function(){b.attr({y:-9999})}})},add:function(a){var b=this.renderer,c=a||b,d=c.element||b.box,e=this.element,f=this.zIndex,g,h;if(a)this.parentGroup=a;this.parentInverted=a&&a.inverted;this.textStr!==void 0&&b.buildText(this);if(f)c.handleZ=!0,f=H(f);if(c.handleZ){a=d.childNodes;for(g=0;gf||!u(f)&&u(c))){d.insertBefore(e, -b);h=!0;break}}h||d.appendChild(e);this.added=!0;if(this.onAdd)this.onAdd();return this},safeRemoveChild:function(a){var b=a.parentNode;b&&b.removeChild(a)},destroy:function(){var a=this,b=a.element||{},c=a.shadows,d=a.renderer.isSVG&&b.nodeName==="SPAN"&&a.parentGroup,e,f;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=b.point=null;hb(a);if(a.clipPath)a.clipPath=a.clipPath.destroy();if(a.stops){for(f=0;f]*>/g,"")},textSetter:function(a){if(a!==this.textStr)delete this.bBox,this.textStr=a,this.added&&this.renderer.buildText(this)},fillSetter:function(a,b, -c){typeof a==="string"?c.setAttribute(b,a):a&&this.colorGradient(a,b,c)},zIndexSetter:function(a,b,c){c.setAttribute(b,a);this[b]=a},_defaultSetter:function(a,b,c){c.setAttribute(b,a)}};Z.prototype.yGetter=Z.prototype.xGetter;Z.prototype.translateXSetter=Z.prototype.translateYSetter=Z.prototype.rotationSetter=Z.prototype.verticalAlignSetter=Z.prototype.scaleXSetter=Z.prototype.scaleYSetter=function(a,b){this[b]=a;this.doTransform=!0};Z.prototype["stroke-widthSetter"]=Z.prototype.strokeSetter=function(a, -b,c){this[b]=a;if(this.stroke&&this["stroke-width"])this.strokeWidth=this["stroke-width"],Z.prototype.fillSetter.call(this,this.stroke,"stroke",c),c.setAttribute("stroke-width",this["stroke-width"]),this.hasStroke=!0;else if(b==="stroke-width"&&a===0&&this.hasStroke)c.removeAttribute("stroke"),this.hasStroke=!1};var na=function(){this.init.apply(this,arguments)};na.prototype={Element:Z,init:function(a,b,c,d,e){var f=location,g,d=this.createElement("svg").attr({version:"1.1"}).css(this.getStyle(d)); -g=d.element;a.appendChild(g);a.innerHTML.indexOf("xmlns")===-1&&Y(g,"xmlns",Ha);this.isSVG=!0;this.box=g;this.boxWrapper=d;this.alignedObjects=[];this.url=(cb||Eb)&&E.getElementsByTagName("base").length?f.href.replace(/#.*?$/,"").replace(/([\('\)])/g,"\\$1").replace(/ /g,"%20"):"";this.createElement("desc").add().element.appendChild(E.createTextNode("Created with Highstock 2.0.4"));this.defs=this.createElement("defs").add();this.forExport=e;this.gradients={};this.cache={};this.setSize(b,c,!1);var h; -if(cb&&a.getBoundingClientRect)this.subPixelFix=b=function(){K(a,{left:0,top:0});h=a.getBoundingClientRect();K(a,{left:Ya(h.left)-h.left+"px",top:Ya(h.top)-h.top+"px"})},b(),z(S,"resize",b)},getStyle:function(a){return this.style=s({fontFamily:'"Lucida Grande", "Lucida Sans Unicode", Arial, Helvetica, sans-serif',fontSize:"12px"},a)},isHidden:function(){return!this.boxWrapper.getBBox().width},destroy:function(){var a=this.defs;this.box=null;this.boxWrapper=this.boxWrapper.destroy();Na(this.gradients|| -{});this.gradients=null;if(a)this.defs=a.destroy();this.subPixelFix&&U(S,"resize",this.subPixelFix);return this.alignedObjects=null},createElement:function(a){var b=new this.Element;b.init(this,a);return b},draw:function(){},buildText:function(a){for(var b=a.element,c=this,d=c.forExport,e=q(a.textStr,"").toString(),f=e.indexOf("<")!==-1,g=b.childNodes,h,i,j=Y(b,"x"),k=a.styles,l=a.textWidth,m=k&&k.lineHeight,n=k&&k.HcTextStroke,o=g.length,x=function(a){return m?H(m):c.fontMetrics(/(px|em)$/.test(a&& -a.style.fontSize)?a.style.fontSize:k&&k.fontSize||c.style.fontSize||12,a).h};o--;)b.removeChild(g[o]);!f&&!n&&e.indexOf(" ")===-1?b.appendChild(E.createTextNode(e)):(h=/<.*style="([^"]+)".*>/,i=/<.*href="(http[^"]+)".*>/,l&&!a.added&&this.box.appendChild(b),e=f?e.replace(/<(b|strong)>/g,'').replace(/<(i|em)>/g,'').replace(//g,"").split(//g):[e],e[e.length-1]===""&&e.pop(),p(e,function(e, -f){var g,m=0,e=e.replace(//g,"|||");g=e.split("|||");p(g,function(e){if(e!==""||g.length===1){var n={},o=E.createElementNS(Ha,"tspan"),I;h.test(e)&&(I=e.match(h)[1].replace(/(;| |^)color([ :])/,"$1fill$2"),Y(o,"style",I));i.test(e)&&!d&&(Y(o,"onclick",'location.href="'+e.match(i)[1]+'"'),K(o,{cursor:"pointer"}));e=(e.replace(/<(.|\n)*?>/g,"")||" ").replace(/</g,"<").replace(/>/g,">");if(e!==" "){o.appendChild(E.createTextNode(e));if(m)n.dx=0;else if(f&& -j!==null)n.x=j;Y(o,n);b.appendChild(o);!m&&f&&(!ea&&d&&K(o,{display:"block"}),Y(o,"dy",x(o)));if(l)for(var e=e.replace(/([^\^])-/g,"$1- ").split(" "),n=g.length>1||e.length>1&&k.whiteSpace!=="nowrap",q,p,r=k.HcHeight,t=[],u=x(o),Ub=1;n&&(e.length||t.length);)delete a.bBox,q=a.getBBox(),p=q.width,!ea&&c.forExport&&(p=c.measureSpanWidth(o.firstChild.data,a.styles)),q=p>l,!q||e.length===1?(e=t,t=[],e.length&&(Ub++,r&&Ub*u>r?(e=["..."],a.attr("title",a.textStr)):(o=E.createElementNS(Ha,"tspan"),Y(o,{dy:u, -x:j}),I&&Y(o,"style",I),b.appendChild(o))),p>l&&(l=p)):(o.removeChild(o.firstChild),t.unshift(e.pop())),e.length&&o.appendChild(E.createTextNode(e.join(" ").replace(/- /g,"-")));m++}}})}))},button:function(a,b,c,d,e,f,g,h,i){var j=this.label(a,b,c,i,null,null,null,null,"button"),k=0,l,m,n,o,x,I,a={x1:0,y1:0,x2:0,y2:1},e=y({"stroke-width":1,stroke:"#CCCCCC",fill:{linearGradient:a,stops:[[0,"#FEFEFE"],[1,"#F6F6F6"]]},r:2,padding:5,style:{color:"black"}},e);n=e.style;delete e.style;f=y(e,{stroke:"#68A", -fill:{linearGradient:a,stops:[[0,"#FFF"],[1,"#ACF"]]}},f);o=f.style;delete f.style;g=y(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#9BD"],[1,"#CDF"]]}},g);x=g.style;delete g.style;h=y(e,{style:{color:"#CCC"}},h);I=h.style;delete h.style;z(j.element,La?"mouseover":"mouseenter",function(){k!==3&&j.attr(f).css(o)});z(j.element,La?"mouseout":"mouseleave",function(){k!==3&&(l=[e,f,g][k],m=[n,o,x][k],j.attr(l).css(m))});j.setState=function(a){(j.state=k=a)?a===2?j.attr(g).css(x):a===3&&j.attr(h).css(I): -j.attr(e).css(n)};return j.on("click",function(){k!==3&&d.call(j)}).attr(e).css(s({cursor:"default"},n))},crispLine:function(a,b){a[1]===a[4]&&(a[1]=a[4]=t(a[1])-b%2/2);a[2]===a[5]&&(a[2]=a[5]=t(a[2])+b%2/2);return a},path:function(a){var b={fill:$};Ja(a)?b.d=a:ha(a)&&s(b,a);return this.createElement("path").attr(b)},circle:function(a,b,c){a=ha(a)?a:{x:a,y:b,r:c};b=this.createElement("circle");b.xSetter=function(a){this.element.setAttribute("cx",a)};b.ySetter=function(a){this.element.setAttribute("cy", -a)};return b.attr(a)},arc:function(a,b,c,d,e,f){if(ha(a))b=a.y,c=a.r,d=a.innerR,e=a.start,f=a.end,a=a.x;a=this.symbol("arc",a||0,b||0,c||0,c||0,{innerR:d||0,start:e||0,end:f||0});a.r=c;return a},rect:function(a,b,c,d,e,f){var e=ha(a)?a.r:e,g=this.createElement("rect"),a=ha(a)?a:a===r?{}:{x:a,y:b,width:v(c,0),height:v(d,0)};if(f!==r)a.strokeWidth=f,a=g.crisp(a);if(e)a.r=e;g.rSetter=function(a){Y(this.element,{rx:a,ry:a})};return g.attr(a)},setSize:function(a,b,c){var d=this.alignedObjects,e=d.length; -this.width=a;this.height=b;for(this.boxWrapper[q(c,!0)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b=this.createElement("g");return u(a)?b.attr({"class":"highcharts-"+a}):b},image:function(a,b,c,d,e){var f={preserveAspectRatio:$};arguments.length>1&&s(f,{x:b,y:c,width:d,height:e});f=this.createElement("image").attr(f);f.element.setAttributeNS?f.element.setAttributeNS("http://www.w3.org/1999/xlink","href",a):f.element.setAttribute("hc-svg-href",a);return f},symbol:function(a, -b,c,d,e,f){var g,h=this.symbols[a],h=h&&h(t(b),t(c),d,e,f),i=/^url\((.*?)\)$/,j,k;if(h)g=this.path(h),s(g,{symbolName:a,x:b,y:c,width:d,height:e}),f&&s(g,f);else if(i.test(a))k=function(a,b){a.element&&(a.attr({width:b[0],height:b[1]}),a.alignByTranslate||a.translate(t((d-b[0])/2),t((e-b[1])/2)))},j=a.match(i)[1],a=Tb[j]||f&&f.width&&f.height&&[f.width,f.height],g=this.image(j).attr({x:b,y:c}),g.isImg=!0,a?k(g,a):(g.attr({width:0,height:0}),aa("img",{onload:function(){k(g,Tb[j]=[this.width,this.height])}, -src:j}));return g},symbols:{circle:function(a,b,c,d){var e=0.166*c;return["M",a+c/2,b,"C",a+c+e,b,a+c+e,b+d,a+c/2,b+d,"C",a-e,b+d,a-e,b,a+c/2,b,"Z"]},square:function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c,b+d,a,b+d,"Z"]},triangle:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d,a,b+d,"Z"]},"triangle-down":function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c/2,b+d,"Z"]},diamond:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d/2,a+c/2,b+d,a,b+d/2,"Z"]},arc:function(a,b,c,d,e){var f=e.start,c=e.r||c||d,g=e.end- -0.001,d=e.innerR,h=e.open,i=fa(f),j=ka(f),k=fa(g),g=ka(g),e=e.end-fc&&i>b+g&&ib+g&&id&&h>a+g&&ha+g&&hl&&/[ \-]/.test(b.textContent||b.innerText))K(b,{width:l+"px",display:"block",whiteSpace:"normal"}),i=l;this.getSpanCorrection(i,k,h,j,g)}K(b,{left:e+(this.xCorr||0)+"px",top:f+(this.yCorr||0)+"px"});if(Eb)k=b.offsetHeight;this.cTT=m}}else this.alignOnAdd=!0},setSpanRotation:function(a,b,c){var d={},e=La?"-ms-transform":Eb?"-webkit-transform":cb?"MozTransform":Sb?"-o-transform":"";d[e]=d.transform="rotate("+a+"deg)";d[e+(cb?"Origin":"-origin")]=d.transformOrigin= -b*100+"% "+c+"px";K(this.element,d)},getSpanCorrection:function(a,b,c){this.xCorr=-a*c;this.yCorr=-b}});s(na.prototype,{html:function(a,b,c){var d=this.createElement("span"),e=d.element,f=d.renderer;d.textSetter=function(a){a!==e.innerHTML&&delete this.bBox;e.innerHTML=this.textStr=a};d.xSetter=d.ySetter=d.alignSetter=d.rotationSetter=function(a,b){b==="align"&&(b="textAlign");d[b]=a;d.htmlUpdateTransform()};d.attr({text:a,x:t(b),y:t(c)}).css({position:"absolute",whiteSpace:"nowrap",fontFamily:this.style.fontFamily, -fontSize:this.style.fontSize});d.css=d.htmlCss;if(f.isSVG)d.add=function(a){var b,c=f.box.parentNode,j=[];if(this.parentGroup=a){if(b=a.div,!b){for(;a;)j.push(a),a=a.parentGroup;p(j.reverse(),function(a){var d;b=a.div=a.div||aa(Wa,{className:Y(a.element,"class")},{position:"absolute",left:(a.translateX||0)+"px",top:(a.translateY||0)+"px"},b||c);d=b.style;s(a,{translateXSetter:function(b,c){d.left=b+"px";a[c]=b;a.doTransform=!0},translateYSetter:function(b,c){d.top=b+"px";a[c]=b;a.doTransform=!0}, -visibilitySetter:function(a,b){d[b]=a}})})}}else b=c;b.appendChild(e);d.added=!0;d.alignOnAdd&&d.htmlUpdateTransform();return d};return d}});var ib,ga;if(!ea&&!la)ga={init:function(a,b){var c=["<",b,' filled="f" stroked="f"'],d=["position: ","absolute",";"],e=b===Wa;(b==="shape"||e)&&d.push("left:0;top:0;width:1px;height:1px;");d.push("visibility: ",e?"hidden":"visible");c.push(' style="',d.join(""),'"/>');if(b)c=e||b==="span"||b==="img"?c.join(""):a.prepVML(c),this.element=aa(c);this.renderer=a}, -add:function(a){var b=this.renderer,c=this.element,d=b.box,d=a?a.element||a:d;a&&a.inverted&&b.invertChild(c,d);d.appendChild(c);this.added=!0;this.alignOnAdd&&!this.deferUpdateTransform&&this.updateTransform();if(this.onAdd)this.onAdd();return this},updateTransform:Z.prototype.htmlUpdateTransform,setSpanRotation:function(){var a=this.rotation,b=fa(a*Pa),c=ka(a*Pa);K(this.element,{filter:a?["progid:DXImageTransform.Microsoft.Matrix(M11=",b,", M12=",-c,", M21=",c,", M22=",b,", sizingMethod='auto expand')"].join(""): -$})},getSpanCorrection:function(a,b,c,d,e){var f=d?fa(d*Pa):1,g=d?ka(d*Pa):0,h=q(this.elemHeight,this.element.offsetHeight),i;this.xCorr=f<0&&-a;this.yCorr=g<0&&-h;i=f*g<0;this.xCorr+=g*b*(i?1-c:c);this.yCorr-=f*b*(d?i?c:1-c:1);e&&e!=="left"&&(this.xCorr-=a*c*(f<0?-1:1),d&&(this.yCorr-=h*c*(g<0?-1:1)),K(this.element,{textAlign:e}))},pathToVML:function(a){for(var b=a.length,c=[];b--;)if(pa(a[b]))c[b]=t(a[b]*10)-5;else if(a[b]==="Z")c[b]="x";else if(c[b]=a[b],a.isArc&&(a[b]==="wa"||a[b]==="at"))c[b+ -5]===c[b+7]&&(c[b+7]+=a[b+7]>a[b+5]?1:-1),c[b+6]===c[b+8]&&(c[b+8]+=a[b+8]>a[b+6]?1:-1);return c.join(" ")||"x"},clip:function(a){var b=this,c;a?(c=a.members,ta(c,b),c.push(b),b.destroyClip=function(){ta(c,b)},a=a.getCSS(b)):(b.destroyClip&&b.destroyClip(),a={clip:ob?"inherit":"rect(auto)"});return b.css(a)},css:Z.prototype.htmlCss,safeRemoveChild:function(a){a.parentNode&&Va(a)},destroy:function(){this.destroyClip&&this.destroyClip();return Z.prototype.destroy.apply(this)},on:function(a,b){this.element["on"+ -a]=function(){var a=S.event;a.target=a.srcElement;b(a)};return this},cutOffPath:function(a,b){var c,a=a.split(/[ ,]/);c=a.length;if(c===9||c===11)a[c-4]=a[c-2]=H(a[c-2])-10*b;return a.join(" ")},shadow:function(a,b,c){var d=[],e,f=this.element,g=this.renderer,h,i=f.style,j,k=f.path,l,m,n,o;k&&typeof k.value!=="string"&&(k="x");m=k;if(a){n=q(a.width,3);o=(a.opacity||0.15)/n;for(e=1;e<=3;e++){l=n*2+1-2*e;c&&(m=this.cutOffPath(k.value,l+0.5));j=[''];h=aa(g.prepVML(j),null,{left:H(i.left)+q(a.offsetX,1),top:H(i.top)+q(a.offsetY,1)});if(c)h.cutOff=l+1;j=[''];aa(g.prepVML(j),null,null,h);b?b.element.appendChild(h):f.parentNode.insertBefore(h,f);d.push(h)}this.shadows=d}return this},updateShadows:ra,setAttr:function(a,b){ob?this.element[a]=b:this.element.setAttribute(a,b)},classSetter:function(a){this.element.className=a},dashstyleSetter:function(a, -b,c){(c.getElementsByTagName("stroke")[0]||aa(this.renderer.prepVML([""]),null,null,c))[b]=a||"solid";this[b]=a},dSetter:function(a,b,c){var d=this.shadows,a=a||[];this.d=a.join&&a.join(" ");c.path=a=this.pathToVML(a);if(d)for(c=d.length;c--;)d[c].path=d[c].cutOff?this.cutOffPath(a,d[c].cutOff):a;this.setAttr(b,a)},fillSetter:function(a,b,c){var d=c.nodeName;if(d==="SPAN")c.style.color=a;else if(d!=="IMG")c.filled=a!==$,this.setAttr("fillcolor",this.renderer.color(a,c,b,this))},opacitySetter:ra, -rotationSetter:function(a,b,c){c=c.style;this[b]=c[b]=a;c.left=-t(ka(a*Pa)+1)+"px";c.top=t(fa(a*Pa))+"px"},strokeSetter:function(a,b,c){this.setAttr("strokecolor",this.renderer.color(a,c,b))},"stroke-widthSetter":function(a,b,c){c.stroked=!!a;this[b]=a;pa(a)&&(a+="px");this.setAttr("strokeweight",a)},titleSetter:function(a,b){this.setAttr(b,a)},visibilitySetter:function(a,b,c){a==="inherit"&&(a="visible");this.shadows&&p(this.shadows,function(c){c.style[b]=a});c.nodeName==="DIV"&&(a=a==="hidden"? -"-999em":0,ob||(c.style[b]=a?"visible":"hidden"),b="top");c.style[b]=a},xSetter:function(a,b,c){this[b]=a;b==="x"?b="left":b==="y"&&(b="top");this.updateClipping?(this[b]=a,this.updateClipping()):c.style[b]=a},zIndexSetter:function(a,b,c){c.style[b]=a}},N.VMLElement=ga=ia(Z,ga),ga.prototype.ySetter=ga.prototype.widthSetter=ga.prototype.heightSetter=ga.prototype.xSetter,ga={Element:ga,isIE8:Ga.indexOf("MSIE 8.0")>-1,init:function(a,b,c,d){var e;this.alignedObjects=[];d=this.createElement(Wa).css(s(this.getStyle(d), -{position:"relative"}));e=d.element;a.appendChild(d.element);this.isVML=!0;this.box=e;this.boxWrapper=d;this.cache={};this.setSize(b,c,!1);if(!E.namespaces.hcv){E.namespaces.add("hcv","urn:schemas-microsoft-com:vml");try{E.createStyleSheet().cssText="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "}catch(f){E.styleSheets[0].cssText+="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "}}}, -isHidden:function(){return!this.box.offsetWidth},clipRect:function(a,b,c,d){var e=this.createElement(),f=ha(a);return s(e,{members:[],left:(f?a.x:a)+1,top:(f?a.y:b)+1,width:(f?a.width:c)-1,height:(f?a.height:d)-1,getCSS:function(a){var b=a.element,c=b.nodeName,a=a.inverted,d=this.top-(c==="shape"?b.offsetTop:0),e=this.left,b=e+this.width,f=d+this.height,d={clip:"rect("+t(a?e:d)+"px,"+t(a?f:b)+"px,"+t(a?b:f)+"px,"+t(a?d:e)+"px)"};!a&&ob&&c==="DIV"&&s(d,{width:b+"px",height:f+"px"});return d},updateClipping:function(){p(e.members, -function(a){a.element&&a.css(e.getCSS(a))})}})},color:function(a,b,c,d){var e=this,f,g=/^rgba/,h,i,j=$;a&&a.linearGradient?i="gradient":a&&a.radialGradient&&(i="pattern");if(i){var k,l,m=a.linearGradient||a.radialGradient,n,o,x,I,q,w="",a=a.stops,r,t=[],v=function(){h=[''];aa(e.prepVML(h),null,null,b)};n=a[0];r=a[a.length-1];n[0]>0&&a.unshift([0,n[1]]);r[0]<1&&a.push([1,r[1]]);p(a,function(a, -b){g.test(a[1])?(f=Ia(a[1]),k=f.get("rgb"),l=f.get("a")):(k=a[1],l=1);t.push(a[0]*100+"% "+k);b?(x=l,I=k):(o=l,q=k)});if(c==="fill")if(i==="gradient")c=m.x1||m[0]||0,a=m.y1||m[1]||0,n=m.x2||m[2]||0,m=m.y2||m[3]||0,w='angle="'+(90-X.atan((m-a)/(n-c))*180/ua)+'"',v();else{var j=m.r,u=j*2,s=j*2,y=m.cx,C=m.cy,R=b.radialReference,A,j=function(){R&&(A=d.getBBox(),y+=(R[0]-A.x)/A.width-0.5,C+=(R[1]-A.y)/A.height-0.5,u*=R[2]/A.width,s*=R[2]/A.height);w='src="'+F.global.VMLRadialGradientURL+'" size="'+u+","+ -s+'" origin="0.5,0.5" position="'+y+","+C+'" color2="'+q+'" ';v()};d.added?j():d.onAdd=j;j=I}else j=k}else if(g.test(a)&&b.tagName!=="IMG")f=Ia(a),h=["<",c,' opacity="',f.get("a"),'"/>'],aa(this.prepVML(h),null,null,b),j=f.get("rgb");else{j=b.getElementsByTagName(c);if(j.length)j[0].opacity=1,j[0].type="solid";j=a}return j},prepVML:function(a){var b=this.isIE8,a=a.join("");b?(a=a.replace("/>",' xmlns="urn:schemas-microsoft-com:vml" />'),a=a.indexOf('style="')===-1?a.replace("/>",' style="display:inline-block;behavior:url(#default#VML);" />'): -a.replace('style="','style="display:inline-block;behavior:url(#default#VML);')):a=a.replace("<","1&&f.attr({x:b,y:c,width:d,height:e});return f},createElement:function(a){return a==="rect"?this.symbol(a):na.prototype.createElement.call(this,a)},invertChild:function(a,b){var c=this,d=b.style,e=a.tagName==="IMG"&&a.style;K(a,{flip:"x",left:H(d.width)-(e?H(e.top):1),top:H(d.height)-(e?H(e.left):1),rotation:-90});p(a.childNodes,function(b){c.invertChild(b,a)})},symbols:{arc:function(a,b,c,d,e){var f=e.start,g=e.end,h=e.r||c|| -d,c=e.innerR,d=fa(f),i=ka(f),j=fa(g),k=ka(g);if(g-f===0)return["x"];f=["wa",a-h,b-h,a+h,b+h,a+h*d,b+h*i,a+h*j,b+h*k];e.open&&!c&&f.push("e","M",a,b);f.push("at",a-c,b-c,a+c,b+c,a+c*j,b+c*k,a+c*d,b+c*i,"x","e");f.isArc=!0;return f},circle:function(a,b,c,d,e){e&&(c=d=2*e.r);e&&e.isCircle&&(a-=c/2,b-=d/2);return["wa",a,b,a+c,b+d,a+c,b+d/2,a+c,b+d/2,"e"]},rect:function(a,b,c,d,e){return na.prototype.symbols[!u(e)||!e.r?"square":"callout"].call(0,a,b,c,d,e)}}},N.VMLRenderer=ib=function(){this.init.apply(this, -arguments)},ib.prototype=y(na.prototype,ga),Za=ib;na.prototype.measureSpanWidth=function(a,b){var c=E.createElement("span"),d;d=E.createTextNode(a);c.appendChild(d);K(c,b);this.box.appendChild(c);d=c.offsetWidth;Va(c);return d};var Vb;if(la)N.CanVGRenderer=ga=function(){Ha="http://www.w3.org/1999/xhtml"},ga.prototype.symbols={},Vb=function(){function a(){var a=b.length,d;for(d=0;dm[l]?m[l]=g+j:n||(c=!1);if(n){l=(m=d.justifyToPlot)?d.pos:0;m=m?l+d.len:d.chart.chartWidth;do a+=e?1:-1,n=d.ticks[i[a]];while(i[a]&&(!n||!n.label||n.label.line!==o));d=n&&n.label.xy&&n.label.xy.x+n.getLabelSides()[e?0:1];e&&!h||f&&h?g+kd&&(c=!1)):g+j>m&&(g=m-j,n&&g+k0&&b.height>0){f=y({align:c&&k&&"center",x:c?!k&&4:10,verticalAlign:!c&&k&&"middle",y:c?k?16:10:k?6:-4,rotation:c&&!k&&90},f);if(!g){r={align:f.textAlign||f.align,rotation:f.rotation};if(u(q))r.zIndex=q;a.label=g=t.text(f.text,0,0,f.useHTML).attr(r).css(f.style).add()}b=[o[1],o[4],k?o[6]:o[1]];k=[o[2],o[5],k?o[7]:o[2]];o=Ua(b);c=Ua(k);g.align(f, -!1,{x:o,y:c,width:Ea(b)-o,height:Ea(k)-c});g.show()}else g&&g.hide();return a},destroy:function(){ta(this.axis.plotLinesAndBands,this);delete this.axis;Na(this)}};D.prototype={defaultOptions:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L",second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%e. %b",week:"%e. %b",month:"%b '%y",year:"%Y"},endOnTick:!1,gridLineColor:"#C0C0C0",labels:T,lineColor:"#C0D0E0",lineWidth:1,minPadding:0.01,maxPadding:0.01,minorGridLineColor:"#E0E0E0",minorGridLineWidth:1,minorTickColor:"#A0A0A0", -minorTickLength:2,minorTickPosition:"outside",startOfWeek:1,startOnTick:!1,tickColor:"#C0D0E0",tickLength:10,tickmarkPlacement:"between",tickPixelInterval:100,tickPosition:"outside",tickWidth:1,title:{align:"middle",style:{color:"#707070"}},type:"linear"},defaultYAxisOptions:{endOnTick:!0,gridLineWidth:1,tickPixelInterval:72,showLastLabel:!0,labels:{x:-8,y:3},lineWidth:0,maxPadding:0.05,minPadding:0.05,startOnTick:!0,tickWidth:0,title:{rotation:270,text:"Values"},stackLabels:{enabled:!1,formatter:function(){return Da(this.total, --1)},style:T.style}},defaultLeftAxisOptions:{labels:{x:-15,y:null},title:{rotation:270}},defaultRightAxisOptions:{labels:{x:15,y:null},title:{rotation:90}},defaultBottomAxisOptions:{labels:{x:0,y:null},title:{rotation:0}},defaultTopAxisOptions:{labels:{x:0,y:-15},title:{rotation:0}},init:function(a,b){var c=b.isX;this.horiz=a.inverted?!c:c;this.coll=(this.isXAxis=c)?"xAxis":"yAxis";this.opposite=b.opposite;this.side=b.side||(this.horiz?this.opposite?0:2:this.opposite?1:3);this.setOptions(b);var d= -this.options,e=d.type;this.labelFormatter=d.labels.formatter||this.defaultLabelFormatter;this.userOptions=b;this.minPixelPadding=0;this.chart=a;this.reversed=d.reversed;this.zoomEnabled=d.zoomEnabled!==!1;this.categories=d.categories||e==="category";this.names=[];this.isLog=e==="logarithmic";this.isDatetimeAxis=e==="datetime";this.isLinked=u(d.linkedTo);this.tickmarkOffset=this.categories&&d.tickmarkPlacement==="between"&&q(d.tickInterval,1)===1?0.5:0;this.ticks={};this.labelEdge=[];this.minorTicks= -{};this.plotLinesAndBands=[];this.alternateBands={};this.len=0;this.minRange=this.userMinRange=d.minRange||d.maxZoom;this.range=d.range;this.offset=d.offset||0;this.stacks={};this.oldStacks={};this.min=this.max=null;this.crosshair=q(d.crosshair,qa(a.options.tooltip.crosshairs)[c?0:1],!1);var f,d=this.options.events;Qa(this,a.axes)===-1&&(c&&!this.isColorAxis?a.axes.splice(a.xAxis.length,0,this):a.axes.push(this),a[this.coll].push(this));this.series=this.series||[];if(a.inverted&&c&&this.reversed=== -r)this.reversed=!0;this.removePlotLine=this.removePlotBand=this.removePlotBandOrLine;for(f in d)z(this,f,d[f]);if(this.isLog)this.val2lin=Ka,this.lin2val=sa},setOptions:function(a){this.options=y(this.defaultOptions,this.isXAxis?{}:this.defaultYAxisOptions,[this.defaultTopAxisOptions,this.defaultRightAxisOptions,this.defaultBottomAxisOptions,this.defaultLeftAxisOptions][this.side],y(F[this.coll],a))},defaultLabelFormatter:function(){var a=this.axis,b=this.value,c=a.categories,d=this.dateTimeLabelFormat, -e=F.lang.numericSymbols,f=e&&e.length,g,h=a.options.labels.format,a=a.isLog?b:a.tickInterval;if(h)g=Ma(h,this);else if(c)g=b;else if(d)g=xa(d,b);else if(f&&a>=1E3)for(;f--&&g===r;)c=Math.pow(1E3,f+1),a>=c&&e[f]!==null&&(g=Da(b/c,-1)+e[f]);g===r&&(g=P(b)>=1E4?Da(b,0):Da(b,-1,r,""));return g},getSeriesExtremes:function(){var a=this,b=a.chart;a.hasVisibleSeries=!1;a.dataMin=a.dataMax=a.ignoreMinPadding=a.ignoreMaxPadding=null;a.buildStacks&&a.buildStacks();p(a.series,function(c){if(c.visible||!b.options.chart.ignoreHiddenSeries){var d; -d=c.options.threshold;var e;a.hasVisibleSeries=!0;a.isLog&&d<=0&&(d=null);if(a.isXAxis){if(d=c.xData,d.length)a.dataMin=A(q(a.dataMin,d[0]),Ua(d)),a.dataMax=v(q(a.dataMax,d[0]),Ea(d))}else{c.getExtremes();e=c.dataMax;c=c.dataMin;if(u(c)&&u(e))a.dataMin=A(q(a.dataMin,c),c),a.dataMax=v(q(a.dataMax,e),e);if(u(d))if(a.dataMin>=d)a.dataMin=d,a.ignoreMinPadding=!0;else if(a.dataMaxg+this.width)m=!0}else if(a=g,c=l-this.right,ih+this.height)m=!0;return m&&!d?null:f.renderer.crispLine(["M",a,i,"L",c,j],b||1)},getLinearTickPositions:function(a,b,c){var d,e=ja(V(b/a)*a),f=ja(Ya(c/ -a)*a),g=[];if(b===c&&pa(b))return[b];for(b=e;b<=f;){g.push(b);b=ja(b+a);if(b===d)break;d=b}return g},getMinorTickPositions:function(){var a=this.options,b=this.tickPositions,c=this.minorTickInterval,d=[],e;if(this.isLog){e=b.length;for(a=1;a=this.minRange,f,g,h,i,j;if(this.isXAxis&&this.minRange===r&&!this.isLog)u(a.min)||u(a.max)?this.minRange=null:(p(this.series,function(a){i=a.xData;for(g=j=a.xIncrement?1:i.length-1;g>0;g--)if(h=i[g]-i[g-1],f===r||hc&&(h=0);d=v(d,h);f=v(f,Sa(j)? -0:h/2);g=v(g,j==="on"?0:h);!a.noSharedTooltip&&u(n)&&(e=u(e)?A(e,n):n)}),h=b.ordinalSlope&&e?b.ordinalSlope/e:1,b.minPointOffset=f*=h,b.pointRangePadding=g*=h,b.pointRange=A(d,c),b.closestPointRange=e;if(a)b.oldTransA=j;b.translationSlope=b.transA=j=b.len/(c+g||1);b.transB=b.horiz?b.left:b.bottom;b.minPixelPadding=j*f},setTickPositions:function(a){var b=this,c=b.chart,d=b.options,e=d.startOnTick,f=d.endOnTick,g=b.isLog,h=b.isDatetimeAxis,i=b.isXAxis,j=b.isLinked,k=b.options.tickPositioner,l=d.maxPadding, -m=d.minPadding,n=d.tickInterval,o=d.minTickInterval,x=d.tickPixelInterval,I,oa=b.categories;j?(b.linkedParent=c[b.coll][d.linkedTo],c=b.linkedParent.getExtremes(),b.min=q(c.min,c.dataMin),b.max=q(c.max,c.dataMax),d.type!==b.linkedParent.options.type&&ma(11,1)):(b.min=q(b.userMin,d.min,b.dataMin),b.max=q(b.userMax,d.max,b.dataMax));if(g)!a&&A(b.min,q(b.dataMin,b.min))<=0&&ma(10,1),b.min=ja(Ka(b.min)),b.max=ja(Ka(b.max));if(b.range&&u(b.max))b.userMin=b.min=v(b.min,b.max-b.range),b.userMax=b.max,b.range= -null;b.beforePadding&&b.beforePadding();b.adjustForMinRange();if(!oa&&!b.axisPointRange&&!b.usePercentage&&!j&&u(b.min)&&u(b.max)&&(c=b.max-b.min)){if(!u(d.min)&&!u(b.userMin)&&m&&(b.dataMin<0||!b.ignoreMinPadding))b.min-=c*m;if(!u(d.max)&&!u(b.userMax)&&l&&(b.dataMax>0||!b.ignoreMaxPadding))b.max+=c*l}if(pa(d.floor))b.min=v(b.min,d.floor);if(pa(d.ceiling))b.max=A(b.max,d.ceiling);b.min===b.max||b.min===void 0||b.max===void 0?b.tickInterval=1:j&&!n&&x===b.linkedParent.options.tickPixelInterval?b.tickInterval= -b.linkedParent.tickInterval:(b.tickInterval=q(n,oa?1:(b.max-b.min)*x/v(b.len,x)),!u(n)&&b.len1&&b.tickInterval<5&&b.max>1E3&&b.max<9999)));b.minorTickInterval=d.minorTickInterval==="auto"&&b.tickInterval?b.tickInterval/5:d.minorTickInterval;b.tickPositions=a=d.tickPositions?[].concat(d.tickPositions):k&&k.apply(b,[b.min,b.max]);if(!a)!b.ordinalPositions&&(b.max-b.min)/b.tickInterval>v(2*b.len,200)&&ma(19,!0),a=h?b.getTimeTicks(b.normalizeTimeTickInterval(b.tickInterval,d.units),b.min, -b.max,d.startOfWeek,b.ordinalPositions,b.closestPointRange,!0):g?b.getLogTickPositions(b.tickInterval,b.min,b.max):b.getLinearTickPositions(b.tickInterval,b.min,b.max),I&&a.splice(1,a.length-2),b.tickPositions=a;if(!j)d=a[0],g=a[a.length-1],h=b.minPointOffset||0,e?b.min=d:b.min-h>d&&a.shift(),f?b.max=g:b.max+h1E13?1:0.001,b.min-=e,b.max+=e)},setMaxTicks:function(){var a=this.chart,b=a.maxTicks||{},c=this.tickPositions,d=this._maxTicksKey= -[this.coll,this.pos,this.len].join("-");if(!this.isLinked&&!this.isDatetimeAxis&&c&&c.length>(b[d]||0)&&this.options.alignTicks!==!1)b[d]=c.length;a.maxTicks=b},adjustTickAmount:function(){var a=this._maxTicksKey,b=this.tickPositions,c=this.chart.maxTicks;if(c&&c[a]&&!this.isDatetimeAxis&&!this.categories&&!this.isLinked&&this.options.alignTicks!==!1&&this.min!==r){var d=this.tickAmount,e=b.length;this.tickAmount=a=c[a];if(e=v(d,q(e.max,d))&&(b=r));this.displayBtn=a!==r||b!==r;this.setExtremes(a,b,!1,r,{trigger:"zoom"});return!0},setAxisSize:function(){var a=this.chart,b=this.options,c=b.offsetLeft||0,d=this.horiz,e=q(b.width,a.plotWidth-c+(b.offsetRight||0)),f=q(b.height,a.plotHeight),g=q(b.top,a.plotTop),b=q(b.left,a.plotLeft+c),c=/%$/;c.test(f)&&(f=parseInt(f,10)/ -100*a.plotHeight);c.test(g)&&(g=parseInt(g,10)/100*a.plotHeight+a.plotTop);this.left=b;this.top=g;this.width=e;this.height=f;this.bottom=a.chartHeight-f-g;this.right=a.chartWidth-e-b;this.len=v(d?e:f,0);this.pos=d?b:g},getExtremes:function(){var a=this.isLog;return{min:a?ja(sa(this.min)):this.min,max:a?ja(sa(this.max)):this.max,dataMin:this.dataMin,dataMax:this.dataMax,userMin:this.userMin,userMax:this.userMax}},getThreshold:function(a){var b=this.isLog,c=b?sa(this.min):this.min,b=b?sa(this.max): -this.max;c>a||a===null?a=c:b15&&a<165?"right":a>195&&a<345?"left":"center"},getOffset:function(){var a=this,b=a.chart,c=b.renderer,d=a.options,e=a.tickPositions,f=a.ticks,g=a.horiz,h=a.side,i=b.inverted?[1,0,3,2][h]:h,j,k,l=0,m,n=0,o=d.title,x=d.labels,I=0,oa=b.axisOffset,b=b.clipOffset,w=[-1,1,1,-1][h],t,s=1,y=q(x.maxStaggerLines,5),O,A,z,C,R;a.hasData=j=a.hasVisibleSeries||u(a.min)&&u(a.max)&& -!!e;a.showAxis=k=j||q(d.showEmpty,!0);a.staggerLines=a.horiz&&x.staggerLines;if(!a.axisGroup)a.gridGroup=c.g("grid").attr({zIndex:d.gridZIndex||1}).add(),a.axisGroup=c.g("axis").attr({zIndex:d.zIndex||2}).add(),a.labelGroup=c.g("axis-labels").attr({zIndex:x.zIndex||7}).addClass("highcharts-"+a.coll.toLowerCase()+"-labels").add();if(j||a.isLinked){a.labelAlign=q(x.align||a.autoLabelAlign(x.rotation));p(e,function(b){f[b]?f[b].addLabel():f[b]=new bb(a,b)});if(a.horiz&&!a.staggerLines&&y&&!x.rotation){for(j= -a.reversed?[].concat(e).reverse():e;s1)a.staggerLines=s}p(e,function(b){if(h===0||h===2||{1:"left",3:"right"}[h]===a.labelAlign)I=v(f[b].getLabelSize(),I)});if(a.staggerLines)I*=a.staggerLines,a.labelOffset=I}else for(t in f)f[t].destroy(),delete f[t];if(o&&o.text&&o.enabled!==!1){if(!a.axisTitle)a.axisTitle=c.text(o.text, -0,0,o.useHTML).attr({zIndex:7,rotation:o.rotation||0,align:o.textAlign||{low:"left",middle:"center",high:"right"}[o.align]}).addClass("highcharts-"+this.coll.toLowerCase()+"-title").css(o.style).add(a.axisGroup),a.axisTitle.isNew=!0;if(k)l=a.axisTitle.getBBox()[g?"height":"width"],m=o.offset,n=u(m)?0:q(o.margin,g?5:10);a.axisTitle[k?"show":"hide"]()}a.offset=w*q(d.offset,oa[h]);c=h===2?a.tickBaseline:0;g=I+n+(I&&w*(g?q(x.y,a.tickBaseline+8):x.x)-c);a.axisTitleMargin=q(m,g);oa[h]=v(oa[h],a.axisTitleMargin+ -l+w*a.offset,g);b[i]=v(b[i],V(d.lineWidth/2)*2)},getLinePath:function(a){var b=this.chart,c=this.opposite,d=this.offset,e=this.horiz,f=this.left+(c?this.width:0)+d,d=b.chartHeight-this.bottom-(c?this.height:0)+d;c&&(a*=-1);return b.renderer.crispLine(["M",e?this.left:f,e?d:this.top,"L",e?b.chartWidth-this.right:f,e?d:b.chartHeight-this.bottom],a)},getTitlePosition:function(){var a=this.horiz,b=this.left,c=this.top,d=this.len,e=this.options.title,f=a?b:c,g=this.opposite,h=this.offset,i=H(e.style.fontSize|| -12),d={low:f+(a?0:d),middle:f+d/2,high:f+(a?d:0)}[e.align],b=(a?c+this.height:b)+(a?1:-1)*(g?-1:1)*this.axisTitleMargin+(this.side===2?i:0);return{x:a?d:b+(g?this.width:0)+h+(e.x||0),y:a?b-(g?this.height:0)+h:d+(e.y||0)}},render:function(){var a=this,b=a.horiz,c=a.reversed,d=a.chart,e=d.renderer,f=a.options,g=a.isLog,h=a.isLinked,i=a.tickPositions,j,k=a.axisTitle,l=a.ticks,m=a.minorTicks,n=a.alternateBands,o=f.stackLabels,x=f.alternateGridColor,q=a.tickmarkOffset,oa=f.lineWidth,w=d.hasRendered&&u(a.oldMin)&& -!isNaN(a.oldMin),t=a.hasData,v=a.showAxis,s,y=f.labels.overflow,A=a.justifyLabels=b&&y!==!1,z;a.labelEdge.length=0;a.justifyToPlot=y==="justify";p([l,m,n],function(a){for(var b in a)a[b].isActive=!1});if(t||h)if(a.minorTickInterval&&!a.categories&&p(a.getMinorTickPositions(),function(b){m[b]||(m[b]=new bb(a,b,"minor"));w&&m[b].isNew&&m[b].render(null,!0);m[b].render(null,!1,1)}),i.length&&(j=i.slice(),(b&&c||!b&&!c)&&j.reverse(),A&&(j=j.slice(1).concat([j[0]])),p(j,function(b,c){A&&(c=c===j.length- -1?0:c+1);if(!h||b>=a.min&&b<=a.max)l[b]||(l[b]=new bb(a,b)),w&&l[b].isNew&&l[b].render(c,!0,0.1),l[b].render(c)}),q&&a.min===0&&(l[-1]||(l[-1]=new bb(a,-1,null,!0)),l[-1].render(-1))),x&&p(i,function(b,c){if(c%2===0&&b=G.second&&(i.setMilliseconds(0),i.setSeconds(j>=G.minute?0:k*V(i.getSeconds()/k)));if(j>=G.minute)i[Nb](j>=G.hour?0:k*V(i[yb]()/k));if(j>=G.hour)i[Ob](j>=G.day?0:k*V(i[zb]()/k));if(j>=G.day)i[Bb](j>=G.month?1:k*V(i[Xa]()/k));j>=G.month&&(i[Pb](j>=G.year?0:k*V(i[mb]()/k)),h=i[nb]());j>=G.year&&(h-=h%k,i[Qb](h));if(j===G.week)i[Bb](i[Xa]()-i[Ab]()+q(d,1));b=1;Oa&&(i=new ca(i.getTime()+Oa));h=i[nb]();for(var d=i.getTime(), -l=i[mb](),m=i[Xa](),n=(G.day+(g?Oa:i.getTimezoneOffset()*6E4))%G.day;d=0.5)a=t(a),g=this.getLinearTickPositions(a, -b,c);else if(a>=0.08)for(var f=V(b),h,i,j,k,l,e=a>0.3?[1,2,4]:a>0.15?[1,2,4,6,8]:[1,2,3,4,5,6,7,8,9];fb&&(!d||k<=c)&&k!==r&&g.push(k),k>c&&(l=!0),k=j}else if(b=sa(b),c=sa(c),a=e[d?"minorTickInterval":"tickInterval"],a=q(a==="auto"?null:a,this._minorAutoInterval,(c-b)*(e.tickPixelInterval/(d?5:1))/((d?f/this.tickPositions.length:f)||1)),a=wb(a,null,vb(a)),g=za(this.getLinearTickPositions(a,b,c),Ka),!d)this._minorAutoInterval=a/5;if(!d)this.tickInterval= -a;return g};var Jb=N.Tooltip=function(){this.init.apply(this,arguments)};Jb.prototype={init:function(a,b){var c=b.borderWidth,d=b.style,e=H(d.padding);this.chart=a;this.options=b;this.crosshairs=[];this.now={x:0,y:0};this.isHidden=!0;this.label=a.renderer.label("",0,0,b.shape||"callout",null,null,b.useHTML,null,"tooltip").attr({padding:e,fill:b.backgroundColor,"stroke-width":c,r:b.borderRadius,zIndex:8}).css(d).css({padding:0}).add().attr({y:-9999});la||this.label.shadow(b.shadow);this.shared=b.shared}, -destroy:function(){if(this.label)this.label=this.label.destroy();clearTimeout(this.hideTimer);clearTimeout(this.tooltipTimeout)},move:function(a,b,c,d){var e=this,f=e.now,g=e.options.animation!==!1&&!e.isHidden&&(P(a-f.x)>1||P(b-f.y)>1),h=e.followPointer||e.len>1;s(f,{x:g?(2*f.x+a)/3:a,y:g?(f.y+b)/2:b,anchorX:h?r:g?(2*f.anchorX+c)/3:c,anchorY:h?r:g?(f.anchorY+d)/2:d});e.label.attr(f);if(g)clearTimeout(this.tooltipTimeout),this.tooltipTimeout=setTimeout(function(){e&&e.move(a,b,c,d)},32)},hide:function(a){var b= -this,c;clearTimeout(this.hideTimer);if(!this.isHidden)c=this.chart.hoverPoints,this.hideTimer=setTimeout(function(){b.label.fadeOut();b.isHidden=!0},q(a,this.options.hideDelay,500)),c&&p(c,function(a){a.setState()}),this.chart.hoverPoints=null},getAnchor:function(a,b){var c,d=this.chart,e=d.inverted,f=d.plotTop,g=0,h=0,i,a=qa(a);c=a[0].tooltipPos;this.followPointer&&b&&(b.chartX===r&&(b=d.pointer.normalize(b)),c=[b.chartX-d.plotLeft,b.chartY-f]);c||(p(a,function(a){i=a.series.yAxis;g+=a.plotX;h+= -(a.plotLow?(a.plotLow+a.plotHigh)/2:a.plotY)+(!e&&i?i.top-f:0)}),g/=a.length,h/=a.length,c=[e?d.plotWidth-h:g,this.shared&&!e&&a.length>1&&b?b.chartY-f:e?d.plotHeight-g:h]);return za(c,t)},getPosition:function(a,b,c){var d=this.chart,e=this.distance,f={},g,h=["y",d.chartHeight,b,c.plotY+d.plotTop],i=["x",d.chartWidth,a,c.plotX+d.plotLeft],j=c.ttBelow||d.inverted&&!c.negative||!d.inverted&&c.negative,k=function(a,b,c,d){var g=cb-e)return!1;else f[a]=db-c/2?b-c-2:d-c/2},m=function(a){var b=h;h=i;i=b;g=a},n=function(){k.apply(0,h)!==!1?l.apply(0,i)===!1&&!g&&(m(!0),n()):g?f.x=f.y=0:(m(!0),n())};(d.inverted||this.len>1)&&m();n();return f},defaultFormatter:function(a){var b=this.points||qa(this),c=b[0].series,d;d=[a.tooltipHeaderFormatter(b[0])];p(b,function(a){c=a.series;d.push(c.tooltipFormatter&&c.tooltipFormatter(a)||a.point.tooltipFormatter(c.tooltipOptions.pointFormat))}); -d.push(a.options.footerFormat||"");return d.join("")},refresh:function(a,b){var c=this.chart,d=this.label,e=this.options,f,g,h={},i,j=[];i=e.formatter||this.defaultFormatter;var h=c.hoverPoints,k,l=this.shared;clearTimeout(this.hideTimer);this.followPointer=qa(a)[0].series.tooltipOptions.followPointer;g=this.getAnchor(a,b);f=g[0];g=g[1];l&&(!a.series||!a.series.noSharedTooltip)?(c.hoverPoints=a,h&&p(h,function(a){a.setState()}),p(a,function(a){a.setState("hover");j.push(a.getLabelConfig())}),h={x:a[0].category, -y:a[0].y},h.points=j,this.len=j.length,a=a[0]):h=a.getLabelConfig();i=i.call(h,this);h=a.series;this.distance=q(h.tooltipOptions.distance,16);i===!1?this.hide():(this.isHidden&&(hb(d),d.attr("opacity",1).show()),d.attr({text:i}),k=e.borderColor||a.color||h.color||"#606060",d.attr({stroke:k}),this.updatePosition({plotX:f,plotY:g,negative:a.negative,ttBelow:a.ttBelow}),this.isHidden=!1);J(c,"tooltipRefresh",{text:i,x:f+c.plotLeft,y:g+c.plotTop,borderColor:k})},updatePosition:function(a){var b=this.chart, -c=this.label,c=(this.options.positioner||this.getPosition).call(this,c.width,c.height,a);this.move(t(c.x),t(c.y),a.plotX+b.plotLeft,a.plotY+b.plotTop)},tooltipHeaderFormatter:function(a){var b=a.series,c=b.tooltipOptions,d=c.dateTimeLabelFormats,e=c.xDateFormat,f=b.xAxis,g=f&&f.options.type==="datetime"&&pa(a.key),c=c.headerFormat,f=f&&f.closestPointRange,h;if(g&&!e){if(f)for(h in G){if(G[h]>=f||G[h]<=G.day&&a.key%G[h]>0){e=d[h];break}}else e=d.day;e=e||d.year}g&&e&&(c=c.replace("{point.key}","{point.key:"+ -e+"}"));return Ma(c,{point:a,series:b})}};var va;db=E.documentElement.ontouchstart!==r;var $a=N.Pointer=function(a,b){this.init(a,b)};$a.prototype={init:function(a,b){var c=b.chart,d=c.events,e=la?"":c.zoomType,c=a.inverted,f;this.options=b;this.chart=a;this.zoomX=f=/x/.test(e);this.zoomY=e=/y/.test(e);this.zoomHor=f&&!c||e&&c;this.zoomVert=e&&!c||f&&c;this.hasZoom=f||e;this.runChartClick=d&&!!d.click;this.pinchDown=[];this.lastValidTouch={};if(N.Tooltip&&b.tooltip.enabled)a.tooltip=new Jb(a,b.tooltip), -this.followTouchMove=b.tooltip.followTouchMove;this.setDOMEvents()},normalize:function(a,b){var c,d,a=a||window.event,a=cc(a);if(!a.target)a.target=a.srcElement;d=a.touches?a.touches.length?a.touches.item(0):a.changedTouches[0]:a;if(!b)this.chartPosition=b=bc(this.chart.container);d.pageX===r?(c=v(a.x,a.clientX-b.left),d=a.y):(c=d.pageX-b.left,d=d.pageY-b.top);return s(a,{chartX:t(c),chartY:t(d)})},getCoordinates:function(a){var b={xAxis:[],yAxis:[]};p(this.chart.axes,function(c){b[c.isXAxis?"xAxis": -"yAxis"].push({axis:c,value:c.toValue(a[c.horiz?"chartX":"chartY"])})});return b},getIndex:function(a){var b=this.chart;return b.inverted?b.plotHeight+b.plotTop-a.chartY:a.chartX-b.plotLeft},runPointActions:function(a){var b=this.chart,c=b.series,d=b.tooltip,e,f,g=b.hoverPoint,h=b.hoverSeries,i,j,k=b.chartWidth,l=this.getIndex(a);if(d&&this.options.tooltip.shared&&(!h||!h.noSharedTooltip)){f=[];i=c.length;for(j=0;jk&&f.splice(i,1);if(f.length&&f[0].clientX!==this.hoverX)d.refresh(f,a),this.hoverX=f[0].clientX}c=h&&h.tooltipOptions.followPointer;if(h&&h.tracker&&!c){if((e=h.tooltipPoints[l])&&e!==g)e.onMouseOver(a)}else d&&c&&!d.isHidden&&(h=d.getAnchor([{}],a),d.updatePosition({plotX:h[0],plotY:h[1]}));if(d&&!this._onDocumentMouseMove)this._onDocumentMouseMove= -function(a){if(ba[va])ba[va].pointer.onDocumentMouseMove(a)},z(E,"mousemove",this._onDocumentMouseMove);p(b.axes,function(b){b.drawCrosshair(a,q(e,g))})},reset:function(a,b){var c=this.chart,d=c.hoverSeries,e=c.hoverPoint,f=c.tooltip,g=f&&f.shared?c.hoverPoints:e;(a=a&&f&&g)&&qa(g)[0].plotX===r&&(a=!1);if(a)f.refresh(g),e&&e.setState(e.state,!0);else{if(e)e.onMouseOut();if(d)d.onMouseOut();f&&f.hide(b);if(this._onDocumentMouseMove)U(E,"mousemove",this._onDocumentMouseMove),this._onDocumentMouseMove= -null;p(c.axes,function(a){a.hideCrosshair()});this.hoverX=null}},scaleGroups:function(a,b){var c=this.chart,d;p(c.series,function(e){d=a||e.getPlotBox();e.xAxis&&e.xAxis.zoomEnabled&&(e.group.attr(d),e.markerGroup&&(e.markerGroup.attr(d),e.markerGroup.clip(b?c.clipRect:null)),e.dataLabelsGroup&&e.dataLabelsGroup.attr(d))});c.clipRect.attr(b||c.clipBox)},dragStart:function(a){var b=this.chart;b.mouseIsDown=a.type;b.cancelClick=!1;b.mouseDownX=this.mouseDownX=a.chartX;b.mouseDownY=this.mouseDownY=a.chartY}, -drag:function(a){var b=this.chart,c=b.options.chart,d=a.chartX,e=a.chartY,f=this.zoomHor,g=this.zoomVert,h=b.plotLeft,i=b.plotTop,j=b.plotWidth,k=b.plotHeight,l,m=this.mouseDownX,n=this.mouseDownY,o=c.panKey&&a[c.panKey+"Key"];dh+j&&(d=h+j);ei+k&&(e=i+k);this.hasDragged=Math.sqrt(Math.pow(m-d,2)+Math.pow(n-e,2));if(this.hasDragged>10){l=b.isInsidePlot(m-h,n-i);if(b.hasCartesianSeries&&(this.zoomX||this.zoomY)&&l&&!o&&!this.selectionMarker)this.selectionMarker=b.renderer.rect(h,i, -f?1:j,g?1:k,0).attr({fill:c.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add();this.selectionMarker&&f&&(d-=m,this.selectionMarker.attr({width:P(d),x:(d>0?0:d)+m}));this.selectionMarker&&g&&(d=e-n,this.selectionMarker.attr({height:P(d),y:(d>0?0:d)+n}));l&&!this.selectionMarker&&c.panning&&b.pan(a,c.panning)}},drop:function(a){var b=this.chart,c=this.hasPinched;if(this.selectionMarker){var d={xAxis:[],yAxis:[],originalEvent:a.originalEvent||a},e=this.selectionMarker,f=e.attr?e.attr("x"): -e.x,g=e.attr?e.attr("y"):e.y,h=e.attr?e.attr("width"):e.width,i=e.attr?e.attr("height"):e.height,j;if(this.hasDragged||c)p(b.axes,function(b){if(b.zoomEnabled){var c=b.horiz,e=a.type==="touchend"?b.minPixelPadding:0,n=b.toValue((c?f:g)+e),c=b.toValue((c?f+h:g+i)-e);!isNaN(n)&&!isNaN(c)&&(d[b.coll].push({axis:b,min:A(n,c),max:v(n,c)}),j=!0)}}),j&&J(b,"selection",d,function(a){b.zoom(s(a,c?{animation:!1}:null))});this.selectionMarker=this.selectionMarker.destroy();c&&this.scaleGroups()}if(b)K(b.container, -{cursor:b._cursor}),b.cancelClick=this.hasDragged>10,b.mouseIsDown=this.hasDragged=this.hasPinched=!1,this.pinchDown=[]},onContainerMouseDown:function(a){a=this.normalize(a);a.preventDefault&&a.preventDefault();this.dragStart(a)},onDocumentMouseUp:function(a){ba[va]&&ba[va].pointer.drop(a)},onDocumentMouseMove:function(a){var b=this.chart,c=this.chartPosition,d=b.hoverSeries,a=this.normalize(a,c);c&&d&&!this.inClass(a.target,"highcharts-tracker")&&!b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&& -this.reset()},onContainerMouseLeave:function(){var a=ba[va];if(a)a.pointer.reset(),a.pointer.chartPosition=null},onContainerMouseMove:function(a){var b=this.chart;va=b.index;a=this.normalize(a);a.returnValue=!1;b.mouseIsDown==="mousedown"&&this.drag(a);(this.inClass(a.target,"highcharts-tracker")||b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop))&&!b.openMenu&&this.runPointActions(a)},inClass:function(a,b){for(var c;a;){if(c=Y(a,"class"))if(c.indexOf(b)!==-1)return!0;else if(c.indexOf("highcharts-container")!== --1)return!1;a=a.parentNode}},onTrackerMouseOut:function(a){var b=this.chart.hoverSeries,c=(a=a.relatedTarget||a.toElement)&&a.point&&a.point.series;if(b&&!b.options.stickyTracking&&!this.inClass(a,"highcharts-tooltip")&&c!==b)b.onMouseOut()},onContainerClick:function(a){var b=this.chart,c=b.hoverPoint,d=b.plotLeft,e=b.plotTop,a=this.normalize(a);a.cancelBubble=!0;b.cancelClick||(c&&this.inClass(a.target,"highcharts-tracker")?(J(c.series,"click",s(a,{point:c})),b.hoverPoint&&c.firePointEvent("click", -a)):(s(a,this.getCoordinates(a)),b.isInsidePlot(a.chartX-d,a.chartY-e)&&J(b,"click",a)))},setDOMEvents:function(){var a=this,b=a.chart.container;b.onmousedown=function(b){a.onContainerMouseDown(b)};b.onmousemove=function(b){a.onContainerMouseMove(b)};b.onclick=function(b){a.onContainerClick(b)};z(b,"mouseleave",a.onContainerMouseLeave);gb===1&&z(E,"mouseup",a.onDocumentMouseUp);if(db)b.ontouchstart=function(b){a.onContainerTouchStart(b)},b.ontouchmove=function(b){a.onContainerTouchMove(b)},gb===1&& -z(E,"touchend",a.onDocumentTouchEnd)},destroy:function(){var a;U(this.chart.container,"mouseleave",this.onContainerMouseLeave);gb||(U(E,"mouseup",this.onDocumentMouseUp),U(E,"touchend",this.onDocumentTouchEnd));clearInterval(this.tooltipTimeout);for(a in this)this[a]=null}};s(N.Pointer.prototype,{pinchTranslate:function(a,b,c,d,e,f){(this.zoomHor||this.pinchHor)&&this.pinchTranslateDirection(!0,a,b,c,d,e,f);(this.zoomVert||this.pinchVert)&&this.pinchTranslateDirection(!1,a,b,c,d,e,f)},pinchTranslateDirection:function(a, -b,c,d,e,f,g,h){var i=this.chart,j=a?"x":"y",k=a?"X":"Y",l="chart"+k,m=a?"width":"height",n=i["plot"+(a?"Left":"Top")],o,x,q=h||1,p=i.inverted,w=i.bounds[a?"h":"v"],r=b.length===1,t=b[0][l],v=c[0][l],s=!r&&b[1][l],u=!r&&c[1][l],y,c=function(){!r&&P(t-s)>20&&(q=h||P(v-u)/P(t-s));x=(n-v)/q+t;o=i["plot"+(a?"Width":"Height")]/q};c();b=x;bw.max&&(b=w.max-o,y=!0);y?(v-=0.8*(v-g[j][0]),r||(u-=0.8*(u-g[j][1])),c()):g[j]=[v,u];p||(f[j]=x-n,f[m]=o);f=p?1/q:q;e[m]=o;e[j]=b;d[p?a?"scaleY": -"scaleX":"scale"+k]=q;d["translate"+k]=f*n+(v-f*t)},pinch:function(a){var b=this,c=b.chart,d=b.pinchDown,e=b.followTouchMove,f=a.touches,g=f.length,h=b.lastValidTouch,i=b.hasZoom,j=b.selectionMarker,k={},l=g===1&&(b.inClass(a.target,"highcharts-tracker")&&c.runTrackerClick||b.runChartClick),m={};(i||e)&&!l&&a.preventDefault();za(f,function(a){return b.normalize(a)});if(a.type==="touchstart")p(f,function(a,b){d[b]={chartX:a.chartX,chartY:a.chartY}}),h.x=[d[0].chartX,d[1]&&d[1].chartX],h.y=[d[0].chartY, -d[1]&&d[1].chartY],p(c.axes,function(a){if(a.zoomEnabled){var b=c.bounds[a.horiz?"h":"v"],d=a.minPixelPadding,e=a.toPixels(q(a.options.min,a.dataMin)),f=a.toPixels(q(a.options.max,a.dataMax)),g=A(e,f),e=v(e,f);b.min=A(a.pos,g-d);b.max=v(a.pos+a.len,e+d)}}),b.res=!0;else if(d.length){if(!j)b.selectionMarker=j=s({destroy:ra},c.plotBox);b.pinchTranslate(d,f,k,j,m,h);b.hasPinched=i;b.scaleGroups(k,m);if(!i&&e&&g===1)this.runPointActions(b.normalize(a));else if(b.res)b.res=!1,this.reset(!1,0)}},onContainerTouchStart:function(a){var b= -this.chart;va=b.index;a.touches.length===1?(a=this.normalize(a),b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)?(this.runPointActions(a),this.pinch(a)):this.reset()):a.touches.length===2&&this.pinch(a)},onContainerTouchMove:function(a){(a.touches.length===1||a.touches.length===2)&&this.pinch(a)},onDocumentTouchEnd:function(a){ba[va]&&ba[va].pointer.drop(a)}});if(S.PointerEvent||S.MSPointerEvent){var Aa={},Kb=!!S.PointerEvent,gc=function(){var a,b=[];b.item=function(a){return this[a]};for(a in Aa)Aa.hasOwnProperty(a)&& -b.push({pageX:Aa[a].pageX,pageY:Aa[a].pageY,target:Aa[a].target});return b},Lb=function(a,b,c,d){a=a.originalEvent||a;if((a.pointerType==="touch"||a.pointerType===a.MSPOINTER_TYPE_TOUCH)&&ba[va])d(a),d=ba[va].pointer,d[b]({type:c,target:a.currentTarget,preventDefault:ra,touches:gc()})};s($a.prototype,{onContainerPointerDown:function(a){Lb(a,"onContainerTouchStart","touchstart",function(a){Aa[a.pointerId]={pageX:a.pageX,pageY:a.pageY,target:a.currentTarget}})},onContainerPointerMove:function(a){Lb(a, -"onContainerTouchMove","touchmove",function(a){Aa[a.pointerId]={pageX:a.pageX,pageY:a.pageY};if(!Aa[a.pointerId].target)Aa[a.pointerId].target=a.currentTarget})},onDocumentPointerUp:function(a){Lb(a,"onContainerTouchEnd","touchend",function(a){delete Aa[a.pointerId]})},batchMSEvents:function(a){a(this.chart.container,Kb?"pointerdown":"MSPointerDown",this.onContainerPointerDown);a(this.chart.container,Kb?"pointermove":"MSPointerMove",this.onContainerPointerMove);a(E,Kb?"pointerup":"MSPointerUp",this.onDocumentPointerUp)}}); -Q($a.prototype,"init",function(a,b,c){a.call(this,b,c);(this.hasZoom||this.followTouchMove)&&K(b.container,{"-ms-touch-action":$,"touch-action":$})});Q($a.prototype,"setDOMEvents",function(a){a.apply(this);(this.hasZoom||this.followTouchMove)&&this.batchMSEvents(z)});Q($a.prototype,"destroy",function(a){this.batchMSEvents(U);a.call(this)})}var sb=N.Legend=function(a,b){this.init(a,b)};sb.prototype={init:function(a,b){var c=this,d=b.itemStyle,e=q(b.padding,8),f=b.itemMarginTop||0;this.options=b;if(b.enabled)c.itemStyle= -d,c.itemHiddenStyle=y(d,b.itemHiddenStyle),c.itemMarginTop=f,c.padding=e,c.initialItemX=e,c.initialItemY=e-5,c.maxItemWidth=0,c.chart=a,c.itemHeight=0,c.lastLineHeight=0,c.symbolWidth=q(b.symbolWidth,16),c.pages=[],c.render(),z(c.chart,"endResize",function(){c.positionCheckboxes()})},colorizeItem:function(a,b){var c=this.options,d=a.legendItem,e=a.legendLine,f=a.legendSymbol,g=this.itemHiddenStyle.color,c=b?c.itemStyle.color:g,h=b?a.legendColor||a.color||"#CCC":g,g=a.options&&a.options.marker,i={fill:h}, -j;d&&d.css({fill:c,color:c});e&&e.attr({stroke:h});if(f){if(g&&f.isMarker)for(j in i.stroke=h,g=a.convertAttribs(g),g)d=g[j],d!==r&&(i[j]=d);f.attr(i)}},positionItem:function(a){var b=this.options,c=b.symbolPadding,b=!b.rtl,d=a._legendItemPos,e=d[0],d=d[1],f=a.checkbox;a.legendGroup&&a.legendGroup.translate(b?e:this.legendWidth-e-2*c-4,d);if(f)f.x=e,f.y=d},destroyItem:function(a){var b=a.checkbox;p(["legendItem","legendLine","legendSymbol","legendGroup"],function(b){a[b]&&(a[b]=a[b].destroy())}); -b&&Va(a.checkbox)},destroy:function(){var a=this.group,b=this.box;if(b)this.box=b.destroy();if(a)this.group=a.destroy()},positionCheckboxes:function(a){var b=this.group.alignAttr,c,d=this.clipHeight||this.legendHeight;if(b)c=b.translateY,p(this.allItems,function(e){var f=e.checkbox,g;f&&(g=c+f.y+(a||0)+3,K(f,{left:b.translateX+e.checkboxOffset+f.x-20+"px",top:g+"px",display:g>c-6&&g(m||b.chartWidth-2*j-x-d.x))this.itemX=x,this.itemY+=o+this.lastLineHeight+n,this.lastLineHeight=0;this.maxItemWidth=v(this.maxItemWidth,f);this.lastItemY=o+this.itemY+ -n;this.lastLineHeight=v(g,this.lastLineHeight);a._legendItemPos=[this.itemX,this.itemY];e?this.itemX+=f:(this.itemY+=o+g+n,this.lastLineHeight=g);this.offsetWidth=m||v((e?this.itemX-x-k:f)+j,this.offsetWidth)},getAllItems:function(){var a=[];p(this.chart.series,function(b){var c=b.options;if(q(c.showInLegend,!u(c.linkedTo)?r:!1,!0))a=a.concat(b.legendItems||(c.legendType==="point"?b.data:b))});return a},render:function(){var a=this,b=a.chart,c=b.renderer,d=a.group,e,f,g,h,i=a.box,j=a.options,k=a.padding, -l=j.borderWidth,m=j.backgroundColor;a.itemX=a.initialItemX;a.itemY=a.initialItemY;a.offsetWidth=0;a.lastItemY=0;if(!d)a.group=d=c.g("legend").attr({zIndex:7}).add(),a.contentGroup=c.g().attr({zIndex:1}).add(d),a.scrollGroup=c.g().add(a.contentGroup);a.renderTitle();e=a.getAllItems();xb(e,function(a,b){return(a.options&&a.options.legendIndex||0)-(b.options&&b.options.legendIndex||0)});j.reversed&&e.reverse();a.allItems=e;a.display=f=!!e.length;p(e,function(b){a.renderItem(b)});g=j.width||a.offsetWidth; -h=a.lastItemY+a.lastLineHeight+a.titleHeight;h=a.handleOverflow(h);if(l||m){g+=k;h+=k;if(i){if(g>0&&h>0)i[i.isNew?"attr":"animate"](i.crisp({width:g,height:h})),i.isNew=!1}else a.box=i=c.rect(0,0,g,h,j.borderRadius,l||0).attr({stroke:j.borderColor,"stroke-width":l||0,fill:m||$}).add(d).shadow(j.shadow),i.isNew=!0;i[f?"show":"hide"]()}a.legendWidth=g;a.legendHeight=h;p(e,function(b){a.positionItem(b)});f&&d.align(s({width:g,height:h},j),!0,"spacingBox");b.isResizing||this.positionCheckboxes()},handleOverflow:function(a){var b= -this,c=this.chart,d=c.renderer,e=this.options,f=e.y,f=c.spacingBox.height+(e.verticalAlign==="top"?-f:f)-this.padding,g=e.maxHeight,h,i=this.clipRect,j=e.navigation,k=q(j.animation,!0),l=j.arrowSize||12,m=this.nav,n=this.pages,o,x=this.allItems;e.layout==="horizontal"&&(f/=2);g&&(f=A(f,g));n.length=0;if(a>f&&!e.useHTML){this.clipHeight=h=v(f-20-this.titleHeight-this.padding,0);this.currentPage=q(this.currentPage,1);this.fullHeight=a;p(x,function(a,b){var c=a._legendItemPos[1],d=t(a.legendItem.getBBox().height), -e=n.length;if(!e||c-n[e-1]>h&&(o||c)!==n[e-1])n.push(o||c),e++;b===x.length-1&&c+d-n[e-1]>h&&n.push(c);c!==o&&(o=c)});if(!i)i=b.clipRect=d.clipRect(0,this.padding,9999,0),b.contentGroup.clip(i);i.attr({height:h});if(!m)this.nav=m=d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle",0,0,l,l).on("click",function(){b.scroll(-1,k)}).add(m),this.pager=d.text("",15,10).css(j.style).add(m),this.down=d.symbol("triangle-down",0,0,l,l).on("click",function(){b.scroll(1,k)}).add(m);b.scroll(0); -a=f}else if(m)i.attr({height:c.chartHeight}),m.hide(),this.scrollGroup.attr({translateY:1}),this.clipHeight=0;return a},scroll:function(a,b){var c=this.pages,d=c.length,e=this.currentPage+a,f=this.clipHeight,g=this.options.navigation,h=g.activeColor,g=g.inactiveColor,i=this.pager,j=this.padding;e>d&&(e=d);if(e>0)b!==r&&ab(b,this.chart),this.nav.attr({translateX:j,translateY:f+this.padding+7+this.titleHeight,visibility:"visible"}),this.up.attr({fill:e===1?g:h}).css({cursor:e===1?"default":"pointer"}), -i.attr({text:e+"/"+d}),this.down.attr({x:18+this.pager.getBBox().width,fill:e===d?g:h}).css({cursor:e===d?"default":"pointer"}),c=-c[e-1]+this.initialItemY,this.scrollGroup.animate({translateY:c}),this.currentPage=e,this.positionCheckboxes(c)}};T=N.LegendSymbolMixin={drawRectangle:function(a,b){var c=a.options.symbolHeight||12;b.legendSymbol=this.chart.renderer.rect(0,a.baseline-5-c/2,a.symbolWidth,c,a.options.symbolRadius||0).attr({zIndex:3}).add(b.legendGroup)},drawLineMarker:function(a){var b= -this.options,c=b.marker,d;d=a.symbolWidth;var e=this.chart.renderer,f=this.legendGroup,a=a.baseline-t(e.fontMetrics(a.options.itemStyle.fontSize,this.legendItem).b*0.3),g;if(b.lineWidth){g={"stroke-width":b.lineWidth};if(b.dashStyle)g.dashstyle=b.dashStyle;this.legendLine=e.path(["M",0,a,"L",d,a]).attr(g).add(f)}if(c&&c.enabled!==!1)b=c.radius,this.legendSymbol=d=e.symbol(this.symbol,d/2-b,a-b,2*b,2*b).add(f),d.isMarker=!0}};(/Trident\/7\.0/.test(Ga)||cb)&&Q(sb.prototype,"positionItem",function(a, -b){var c=this,d=function(){b._legendItemPos&&a.call(c,b)};d();setTimeout(d)});ya.prototype={init:function(a,b){var c,d=a.series;a.series=null;c=y(F,a);c.series=a.series=d;this.userOptions=a;d=c.chart;this.margin=this.splashArray("margin",d);this.spacing=this.splashArray("spacing",d);var e=d.events;this.bounds={h:{},v:{}};this.callback=b;this.isResizing=0;this.options=c;this.axes=[];this.series=[];this.hasCartesianSeries=d.showAxes;var f=this,g;f.index=ba.length;ba.push(f);gb++;d.reflow!==!1&&z(f, -"load",function(){f.initReflow()});if(e)for(g in e)z(f,g,e[g]);f.xAxis=[];f.yAxis=[];f.animation=la?!1:q(d.animation,!0);f.pointCount=f.colorCounter=f.symbolCounter=0;f.firstRender()},initSeries:function(a){var b=this.options.chart;(b=B[a.type||b.type||b.defaultSeriesType])||ma(17,!0);b=new b;b.init(this,a);return b},isInsidePlot:function(a,b,c){var d=c?b:a,a=c?a:b;return d>=0&&d<=this.plotWidth&&a>=0&&a<=this.plotHeight},adjustTickAmounts:function(){this.options.chart.alignTicks!==!1&&p(this.axes, -function(a){a.adjustTickAmount()});this.maxTicks=null},redraw:function(a){var b=this.axes,c=this.series,d=this.pointer,e=this.legend,f=this.isDirtyLegend,g,h,i=this.hasCartesianSeries,j=this.isDirtyBox,k=c.length,l=k,m=this.renderer,n=m.isHidden(),o=[];ab(a,this);n&&this.cloneRenderTo();for(this.layOutTitles();l--;)if(a=c[l],a.options.stacking&&(g=!0,a.isDirty)){h=!0;break}if(h)for(l=k;l--;)if(a=c[l],a.options.stacking)a.isDirty=!0;p(c,function(a){a.isDirty&&a.options.legendType==="point"&&(f=!0)}); -if(f&&e.options.enabled)e.render(),this.isDirtyLegend=!1;g&&this.getStacks();if(i){if(!this.isResizing)this.maxTicks=null,p(b,function(a){a.setScale()});this.adjustTickAmounts()}this.getMargins();i&&(p(b,function(a){a.isDirty&&(j=!0)}),p(b,function(a){if(a.isDirtyExtremes)a.isDirtyExtremes=!1,o.push(function(){J(a,"afterSetExtremes",s(a.eventArgs,a.getExtremes()));delete a.eventArgs});(j||g)&&a.redraw()}));j&&this.drawChartBox();p(c,function(a){a.isDirty&&a.visible&&(!a.isCartesian||a.xAxis)&&a.redraw()}); -d&&d.reset(!0);m.draw();J(this,"redraw");n&&this.cloneRenderTo(!0);p(o,function(a){a.call()})},get:function(a){var b=this.axes,c=this.series,d,e;for(d=0;d19?this.containerHeight:400))},cloneRenderTo:function(a){var b=this.renderToClone,c=this.container;a?b&&(this.renderTo.appendChild(c),Va(b),delete this.renderToClone):(c&& -c.parentNode===this.renderTo&&this.renderTo.removeChild(c),this.renderToClone=b=this.renderTo.cloneNode(0),K(b,{position:"absolute",top:"-9999px",display:"block"}),b.style.setProperty&&b.style.setProperty("display","block","important"),E.body.appendChild(b),c&&b.appendChild(c))},getContainer:function(){var a,b=this.options.chart,c,d,e;this.renderTo=a=b.renderTo;e="highcharts-"+Fb++;if(Sa(a))this.renderTo=a=E.getElementById(a);a||ma(13,!0);c=H(Y(a,"data-highcharts-chart"));!isNaN(c)&&ba[c]&&ba[c].hasRendered&& -ba[c].destroy();Y(a,"data-highcharts-chart",this.index);a.innerHTML="";!b.skipClone&&!a.offsetWidth&&this.cloneRenderTo();this.getChartSize();c=this.chartWidth;d=this.chartHeight;this.container=a=aa(Wa,{className:"highcharts-container"+(b.className?" "+b.className:""),id:e},s({position:"relative",overflow:"hidden",width:c+"px",height:d+"px",textAlign:"left",lineHeight:"normal",zIndex:0,"-webkit-tap-highlight-color":"rgba(0,0,0,0)"},b.style),this.renderToClone||a);this._cursor=a.style.cursor;this.renderer= -b.forExport?new na(a,c,d,b.style,!0):new Za(a,c,d,b.style);la&&this.renderer.create(this,a,c,d)},getMargins:function(){var a=this.spacing,b,c=this.legend,d=this.margin,e=this.options.legend,f=q(e.margin,20),g=e.x,h=e.y,i=e.align,j=e.verticalAlign,k=this.titleOffset;this.resetMargins();b=this.axisOffset;if(k&&!u(d[0]))this.plotTop=v(this.plotTop,k+this.options.title.margin+a[0]);if(c.display&&!e.floating)if(i==="right"){if(!u(d[1]))this.marginRight=v(this.marginRight,c.legendWidth-g+f+a[1])}else if(i=== -"left"){if(!u(d[3]))this.plotLeft=v(this.plotLeft,c.legendWidth+g+f+a[3])}else if(j==="top"){if(!u(d[0]))this.plotTop=v(this.plotTop,c.legendHeight+h+f+a[0])}else if(j==="bottom"&&!u(d[2]))this.marginBottom=v(this.marginBottom,c.legendHeight-h+f+a[2]);this.extraBottomMargin&&(this.marginBottom+=this.extraBottomMargin);this.extraTopMargin&&(this.plotTop+=this.extraTopMargin);this.hasCartesianSeries&&p(this.axes,function(a){a.getOffset()});u(d[3])||(this.plotLeft+=b[3]);u(d[0])||(this.plotTop+=b[0]); -u(d[2])||(this.marginBottom+=b[2]);u(d[1])||(this.marginRight+=b[1]);this.setChartSize()},reflow:function(a){var b=this,c=b.options.chart,d=b.renderTo,e=c.width||pb(d,"width"),f=c.height||pb(d,"height"),c=a?a.target:S,d=function(){if(b.container)b.setSize(e,f,!1),b.hasUserSize=null};if(!b.hasUserSize&&e&&f&&(c===S||c===E)){if(e!==b.containerWidth||f!==b.containerHeight)clearTimeout(b.reflowTimeout),a?b.reflowTimeout=setTimeout(d,100):d();b.containerWidth=e;b.containerHeight=f}},initReflow:function(){var a= -this,b=function(b){a.reflow(b)};z(S,"resize",b);z(a,"destroy",function(){U(S,"resize",b)})},setSize:function(a,b,c){var d=this,e,f,g;d.isResizing+=1;g=function(){d&&J(d,"endResize",null,function(){d.isResizing-=1})};ab(c,d);d.oldChartHeight=d.chartHeight;d.oldChartWidth=d.chartWidth;if(u(a))d.chartWidth=e=v(0,t(a)),d.hasUserSize=!!e;if(u(b))d.chartHeight=f=v(0,t(b));(Fa?rb:K)(d.container,{width:e+"px",height:f+"px"},Fa);d.setChartSize(!0);d.renderer.setSize(e,f,c);d.maxTicks=null;p(d.axes,function(a){a.isDirty= -!0;a.setScale()});p(d.series,function(a){a.isDirty=!0});d.isDirtyLegend=!0;d.isDirtyBox=!0;d.layOutTitles();d.getMargins();d.redraw(c);d.oldChartHeight=null;J(d,"resize");Fa===!1?g():setTimeout(g,Fa&&Fa.duration||500)},setChartSize:function(a){var b=this.inverted,c=this.renderer,d=this.chartWidth,e=this.chartHeight,f=this.options.chart,g=this.spacing,h=this.clipOffset,i,j,k,l;this.plotLeft=i=t(this.plotLeft);this.plotTop=j=t(this.plotTop);this.plotWidth=k=v(0,t(d-i-this.marginRight));this.plotHeight= -l=v(0,t(e-j-this.marginBottom));this.plotSizeX=b?l:k;this.plotSizeY=b?k:l;this.plotBorderWidth=f.plotBorderWidth||0;this.spacingBox=c.spacingBox={x:g[3],y:g[0],width:d-g[3]-g[1],height:e-g[0]-g[2]};this.plotBox=c.plotBox={x:i,y:j,width:k,height:l};d=2*V(this.plotBorderWidth/2);b=Ya(v(d,h[3])/2);c=Ya(v(d,h[0])/2);this.clipBox={x:b,y:c,width:V(this.plotSizeX-v(d,h[1])/2-b),height:v(0,V(this.plotSizeY-v(d,h[2])/2-c))};a||p(this.axes,function(a){a.setAxisSize();a.setAxisTranslation()})},resetMargins:function(){var a= -this.spacing,b=this.margin;this.plotTop=q(b[0],a[0]);this.marginRight=q(b[1],a[1]);this.marginBottom=q(b[2],a[2]);this.plotLeft=q(b[3],a[3]);this.axisOffset=[0,0,0,0];this.clipOffset=[0,0,0,0]},drawChartBox:function(){var a=this.options.chart,b=this.renderer,c=this.chartWidth,d=this.chartHeight,e=this.chartBackground,f=this.plotBackground,g=this.plotBorder,h=this.plotBGImage,i=a.borderWidth||0,j=a.backgroundColor,k=a.plotBackgroundColor,l=a.plotBackgroundImage,m=a.plotBorderWidth||0,n,o=this.plotLeft, -x=this.plotTop,q=this.plotWidth,p=this.plotHeight,w=this.plotBox,r=this.clipRect,t=this.clipBox;n=i+(a.shadow?8:0);if(i||j)if(e)e.animate(e.crisp({width:c-n,height:d-n}));else{e={fill:j||$};if(i)e.stroke=a.borderColor,e["stroke-width"]=i;this.chartBackground=b.rect(n/2,n/2,c-n,d-n,a.borderRadius,i).attr(e).addClass("highcharts-background").add().shadow(a.shadow)}if(k)f?f.animate(w):this.plotBackground=b.rect(o,x,q,p,0).attr({fill:k}).add().shadow(a.plotShadow);if(l)h?h.animate(w):this.plotBGImage= -b.image(l,o,x,q,p).add();r?r.animate({width:t.width,height:t.height}):this.clipRect=b.clipRect(t);if(m)g?g.animate(g.crisp({x:o,y:x,width:q,height:p,strokeWidth:-m})):this.plotBorder=b.rect(o,x,q,p,0,-m).attr({stroke:a.plotBorderColor,"stroke-width":m,fill:$,zIndex:1}).add();this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart,c,d=a.options.series,e,f;p(["inverted","angular","polar"],function(g){c=B[b.type||b.defaultSeriesType];f=a[g]||b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&& -e--;)(c=B[d[e].type])&&c.prototype[g]&&(f=!0);a[g]=f})},linkSeries:function(){var a=this,b=a.series;p(b,function(a){a.linkedSeries.length=0});p(b,function(b){var d=b.options.linkedTo;if(Sa(d)&&(d=d===":previous"?a.series[b.index-1]:a.get(d)))d.linkedSeries.push(b),b.linkedParent=d})},renderSeries:function(){p(this.series,function(a){a.translate();a.setTooltipPoints&&a.setTooltipPoints();a.render()})},renderLabels:function(){var a=this,b=a.options.labels;b.items&&p(b.items,function(c){var d=s(b.style, -c.style),e=H(d.left)+a.plotLeft,f=H(d.top)+a.plotTop+12;delete d.left;delete d.top;a.renderer.text(c.html,e,f).attr({zIndex:2}).css(d).add()})},render:function(){var a=this.axes,b=this.renderer,c=this.options;this.setTitle();this.legend=new sb(this,c.legend);this.getStacks();p(a,function(a){a.setScale()});this.getMargins();this.maxTicks=null;p(a,function(a){a.setTickPositions(!0);a.setMaxTicks()});this.adjustTickAmounts();this.getMargins();this.drawChartBox();this.hasCartesianSeries&&p(a,function(a){a.render()}); -if(!this.seriesGroup)this.seriesGroup=b.g("series-group").attr({zIndex:3}).add();this.renderSeries();this.renderLabels();this.showCredits(c.credits);this.hasRendered=!0},showCredits:function(a){if(a.enabled&&!this.credits)this.credits=this.renderer.text(a.text,0,0).on("click",function(){if(a.href)location.href=a.href}).attr({align:a.position.align,zIndex:8}).css(a.style).add().align(a.position)},destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;J(a,"destroy");ba[a.index]= -r;gb--;a.renderTo.removeAttribute("data-highcharts-chart");U(a);for(e=b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();p("title,subtitle,chartBackground,plotBackground,plotBGImage,plotBorder,seriesGroup,clipRect,credits,pointer,scroller,rangeSelector,legend,resetZoomButton,tooltip,renderer".split(","),function(b){var c=a[b];c&&c.destroy&&(a[b]=c.destroy())});if(d)d.innerHTML="",U(d),f&&Va(d);for(e in a)delete a[e]},isReadyToRender:function(){var a=this;return!ea&&S==S.top&& -E.readyState!=="complete"||la&&!S.canvg?(la?Vb.push(function(){a.firstRender()},a.options.global.canvasToolsURL):E.attachEvent("onreadystatechange",function(){E.detachEvent("onreadystatechange",a.firstRender);E.readyState==="complete"&&a.firstRender()}),!1):!0},firstRender:function(){var a=this,b=a.options,c=a.callback;if(a.isReadyToRender()){a.getContainer();J(a,"init");a.resetMargins();a.setChartSize();a.propFromSeries();a.getAxes();p(b.series||[],function(b){a.initSeries(b)});a.linkSeries();J(a, -"beforeRender");if(N.Pointer)a.pointer=new $a(a,b);a.render();a.renderer.draw();c&&c.apply(a,[a]);p(a.callbacks,function(b){b.apply(a,[a])});a.cloneRenderTo(!0);J(a,"load")}},splashArray:function(a,b){var c=b[a],c=ha(c)?c:[c,c,c,c];return[q(b[a+"Top"],c[0]),q(b[a+"Right"],c[1]),q(b[a+"Bottom"],c[2]),q(b[a+"Left"],c[3])]}};ya.prototype.callbacks=[];ga=N.CenteredSeriesMixin={getCenter:function(){var a=this.options,b=this.chart,c=2*(a.slicedOffset||0),d,e=b.plotWidth-2*c,f=b.plotHeight-2*c,b=a.center, -a=[q(b[0],"50%"),q(b[1],"50%"),a.size||"100%",a.innerSize||0],g=A(e,f),h;return za(a,function(a,b){h=/%$/.test(a);d=b<2||b===2&&h;return(h?[e,f,g,g][b]*H(a)/100:a)+(d?c:0)})}};var Ba=function(){};Ba.prototype={init:function(a,b,c){this.series=a;this.applyOptions(b,c);this.pointAttr={};if(a.options.colorByPoint&&(b=a.options.colors||a.chart.options.colors,this.color=this.color||b[a.colorCounter++],a.colorCounter===b.length))a.colorCounter=0;a.chart.pointCount++;return this},applyOptions:function(a, -b){var c=this.series,d=c.options.pointValKey||c.pointValKey,a=Ba.prototype.optionsToObject.call(this,a);s(this,a);this.options=this.options?s(this.options,a):a;if(d)this.y=this[d];if(this.x===r&&c)this.x=b===r?c.autoIncrement():b;return this},optionsToObject:function(a){var b={},c=this.series,d=c.pointArrayMap||["y"],e=d.length,f=0,g=0;if(typeof a==="number"||a===null)b[d[0]]=a;else if(Ja(a)){if(a.length>e){c=typeof a[0];if(c==="string")b.name=a[0];else if(c==="number")b.x=a[0];f++}for(;ga+1&&b.push(d.slice(a+1,g)),a=g):g===e-1&&b.push(d.slice(a+1,g+1))});this.segments=b},setOptions:function(a){var b=this.chart,c=b.options.plotOptions,b=b.userOptions||{},d=b.plotOptions||{},e=c[this.type];this.userOptions=a;c=y(e,c.series,a);this.tooltipOptions=y(F.tooltip,F.plotOptions[this.type].tooltip,b.tooltip,d.series&&d.series.tooltip,d[this.type]&& -d[this.type].tooltip,a.tooltip);e.marker===null&&delete c.marker;return c},getCyclic:function(a,b,c){var d=this.userOptions,e="_"+a+"Index",f=a+"Counter";b||(u(d[e])?b=d[e]:(d[e]=b=this.chart[f]%c.length,this.chart[f]+=1),b=c[b]);this[a]=b},getColor:function(){this.options.colorByPoint||this.getCyclic("color",this.options.color||W[this.type].color,this.chart.options.colors)},getSymbol:function(){var a=this.options.marker;this.getCyclic("symbol",a.symbol,this.chart.options.symbols);if(/^url/.test(this.symbol))a.radius= -0},drawLegendSymbol:T.drawLineMarker,setData:function(a,b,c,d){var e=this,f=e.points,g=f&&f.length||0,h,i=e.options,j=e.chart,k=null,l=e.xAxis,m=l&&!!l.categories,n=e.tooltipPoints,o=i.turboThreshold,x=this.xData,I=this.yData,t=(h=e.pointArrayMap)&&h.length,a=a||[];h=a.length;b=q(b,!0);if(d!==!1&&h&&g===h&&!e.cropped&&!e.hasGroupedData)p(a,function(a,b){f[b].update(a,!1,null,!1)});else{e.xIncrement=null;e.pointRange=m?1:i.pointRange;e.colorCounter=0;p(this.parallelArrays,function(a){e[a+"Data"].length= -0});if(o&&h>o){for(c=0;k===null&&ci||this.forceCrop))if(b[d-1]m)b=[],c=[];else if(b[0]m)e= -this.cropData(this.xData,this.yData,n,m),b=e.xData,c=e.yData,e=e.start,f=!0,k=b.length;for(i=b.length-1;i>=0;i--)d=b[i]-b[i-1],!f&&b[i]>n&&b[i]0&&(g===r||d=c){f=v(0,i-h);break}for(;i< -e;i++)if(a[i]>d){g=i+h;break}return{xData:a.slice(f,g),yData:b.slice(f,g),start:f,end:g}},generatePoints:function(){var a=this.options.data,b=this.data,c,d=this.processedXData,e=this.processedYData,f=this.pointClass,g=d.length,h=this.cropStart||0,i,j=this.hasGroupedData,k,l=[],m;if(!b&&!j)b=[],b.length=a.length,b=this.data=b;for(m=0;m0),j=this.getExtremesFromAll||this.cropped||(c[l+1]||j)>=g&&(c[l-1]||j)<=h,i&&j)if(i=k.length)for(;i--;)k[i]!==null&&(e[f++]=k[i]);else e[f++]= -k;this.dataMin=q(void 0,Ua(e));this.dataMax=q(void 0,Ea(e))},translate:function(){this.processedXData||this.processData();this.generatePoints();for(var a=this.options,b=a.stacking,c=this.xAxis,d=c.categories,e=this.yAxis,f=this.points,g=f.length,h=!!this.modifyValue,i=a.pointPlacement,j=i==="between"||pa(i),k=a.threshold,a=0;a0||j))g.graphic=c.renderer.symbol(i,d-h,e-h,2*h,2*h,o?n:l).attr(a).add(p)}else if(k)g.graphic=k.destroy()},convertAttribs:function(a,b,c,d){var e=this.pointAttrToOptions,f,g,h={},a=a||{},b=b||{},c=c||{},d=d||{};for(f in e)g=e[f],h[f]=q(a[g],b[f],c[f],d[f]);return h},getAttribs:function(){var a=this,b=a.options,c=W[a.type].marker?b.marker:b,d=c.states,e=d.hover,f,g=a.color;f={stroke:g,fill:g};var h=a.points||[],i,j=[],k,l=a.pointAttrToOptions;k=a.hasPointSpecificOptions;var m=b.negativeColor,n= -c.lineColor,o=c.fillColor;i=b.turboThreshold;var q;b.marker?(e.radius=e.radius||c.radius+e.radiusPlus,e.lineWidth=e.lineWidth||c.lineWidth+e.lineWidthPlus):e.color=e.color||Ia(e.color||g).brighten(e.brightness).get();j[""]=a.convertAttribs(c,f);p(["hover","select"],function(b){j[b]=a.convertAttribs(d[b],j[""])});a.pointAttr=j;g=h.length;if(!i||g1?b=b.concat(c):d.push(e[0])});a.singlePoints=d;return a.graphPath= -b},drawGraph:function(){var a=this,b=this.options,c=[["graph",b.lineColor||this.color]],d=b.lineWidth,e=b.dashStyle,f=b.linecap!=="square",g=this.getGraphPath(),h=b.negativeColor;h&&c.push(["graphNeg",h]);p(c,function(c,h){var k=c[0],l=a[k];if(l)hb(l),l.animate({d:g});else if(d&&g.length)l={stroke:c[1],"stroke-width":d,fill:$,zIndex:1},e?l.dashstyle=e:f&&(l["stroke-linecap"]=l["stroke-linejoin"]="round"),a[k]=a.chart.renderer.path(g).attr(l).add(a.group).shadow(!h&&b.shadow)})},clipNeg:function(){var a= -this.options,b=this.chart,c=b.renderer,d=a.negativeColor||a.negativeFillColor,e,f=this.graph,g=this.area,h=this.posClip,i=this.negClip;e=b.chartWidth;var j=b.chartHeight,k=v(e,j),l=this.yAxis;if(d&&(f||g)){d=t(l.toPixels(a.threshold||0,!0));d<0&&(k-=d);a={x:0,y:0,width:k,height:d};k={x:0,y:d,width:k,height:k};if(b.inverted)a.height=k.y=b.plotWidth-d,c.isVML&&(a={x:b.plotWidth-d-b.plotLeft,y:0,width:e,height:j},k={x:d+b.plotLeft-e,y:0,width:b.plotLeft+d,height:e});l.reversed?(b=k,e=a):(b=a,e=k);h? -(h.animate(b),i.animate(e)):(this.posClip=h=c.clipRect(b),this.negClip=i=c.clipRect(e),f&&this.graphNeg&&(f.clip(h),this.graphNeg.clip(i)),g&&(g.clip(h),this.areaNeg.clip(i)))}},invertGroups:function(){function a(){var a={width:b.yAxis.len,height:b.xAxis.len};p(["group","markerGroup"],function(c){b[c]&&b[c].attr(a).invert()})}var b=this,c=b.chart;if(b.xAxis)z(c,"resize",a),z(b,"destroy",function(){U(c,"resize",a)}),a(),b.invertGroups=a},plotGroup:function(a,b,c,d,e){var f=this[a],g=!f;g&&(this[a]= -f=this.chart.renderer.g(b).attr({visibility:c,zIndex:d||0.1}).add(e));f[g?"attr":"animate"](this.getPlotBox());return f},getPlotBox:function(){var a=this.chart,b=this.xAxis,c=this.yAxis;if(a.inverted)b=c,c=this.xAxis;return{translateX:b?b.left:a.plotLeft,translateY:c?c.top:a.plotTop,scaleX:1,scaleY:1}},render:function(){var a=this,b=a.chart,c,d=a.options,e=(c=d.animation)&&!!a.animate&&b.renderer.isSVG&&q(c.duration,500)||0,f=a.visible?"visible":"hidden",g=d.zIndex,h=a.hasRendered,i=b.seriesGroup; -c=a.plotGroup("group","series",f,g,i);a.markerGroup=a.plotGroup("markerGroup","markers",f,g,i);e&&a.animate(!0);a.getAttribs();c.inverted=a.isCartesian?b.inverted:!1;a.drawGraph&&(a.drawGraph(),a.clipNeg());p(a.points,function(a){a.redraw&&a.redraw()});a.drawDataLabels&&a.drawDataLabels();a.visible&&a.drawPoints();a.drawTracker&&a.options.enableMouseTracking!==!1&&a.drawTracker();b.inverted&&a.invertGroups();d.clip!==!1&&!a.sharedClipKey&&!h&&c.clip(b.clipRect);e&&a.animate();if(!h)e?a.animationTimeout= -setTimeout(function(){a.afterAnimate()},e):a.afterAnimate();a.isDirty=a.isDirtyData=!1;a.hasRendered=!0},redraw:function(){var a=this.chart,b=this.isDirtyData,c=this.group,d=this.xAxis,e=this.yAxis;c&&(a.inverted&&c.attr({width:a.plotWidth,height:a.plotHeight}),c.animate({translateX:q(d&&d.left,a.plotLeft),translateY:q(e&&e.top,a.plotTop)}));this.translate();this.setTooltipPoints&&this.setTooltipPoints(!0);this.render();b&&J(this,"updatedData")}};Rb.prototype={destroy:function(){Na(this,this.axis)}, -render:function(a){var b=this.options,c=b.format,c=c?Ma(c,this):b.formatter.call(this);this.label?this.label.attr({text:c,visibility:"hidden"}):this.label=this.axis.chart.renderer.text(c,null,null,b.useHTML).css(b.style).attr({align:this.textAlign,rotation:b.rotation,visibility:"hidden"}).add(a)},setOffset:function(a,b){var c=this.axis,d=c.chart,e=d.inverted,f=this.isNegative,g=c.translate(c.usePercentage?100:this.total,0,0,0,1),c=c.translate(0),c=P(g-c),h=d.xAxis[0].translate(this.x)+a,i=d.plotHeight, -f={x:e?f?g:g-c:h,y:e?i-h-b:f?i-g-c:i-g,width:e?c:b,height:e?b:c};if(e=this.label)e.align(this.alignOptions,null,f),f=e.alignAttr,e[this.options.crop===!1||d.isInsidePlot(f.x,f.y)?"show":"hide"](!0)}};D.prototype.buildStacks=function(){var a=this.series,b=q(this.options.reversedStacks,!0),c=a.length;if(!this.isXAxis){for(this.usePercentage=!1;c--;)a[b?c:a.length-c-1].setStackedPoints();if(this.usePercentage)for(c=0;cg;)h--;this.updateParallelArrays(d,"splice",h,0,0);this.updateParallelArrays(d,h);if(j&&d.name)j[g]=d.name;l.splice(h,0,a);m&&(this.data.splice(h,0,null),this.processData());e.legendType==="point"&&this.generatePoints();c&&(f[0]&&f[0].remove?f[0].remove(!1): -(f.shift(),this.updateParallelArrays(d,"shift"),l.shift()));this.isDirtyData=this.isDirty=!0;b&&(this.getAttribs(),i.redraw())},remove:function(a,b){var c=this,d=c.chart,a=q(a,!0);if(!c.isRemoving)c.isRemoving=!0,J(c,"remove",null,function(){c.destroy();d.isDirtyLegend=d.isDirtyBox=!0;d.linkSeries();a&&d.redraw(b)});c.isRemoving=!1},update:function(a,b){var c=this,d=this.chart,e=this.userOptions,f=this.type,g=B[f].prototype,h=["group","markerGroup","dataLabelsGroup"],i;p(h,function(a){h[a]=c[a];delete c[a]}); -a=y(e,{animation:!1,index:this.index,pointStart:this.xData[0]},{data:this.options.data},a);this.remove(!1);for(i in g)g.hasOwnProperty(i)&&(this[i]=r);s(this,B[a.type||f].prototype);p(h,function(a){c[a]=h[a]});this.init(d,a);d.linkSeries();q(b,!0)&&d.redraw(!1)}});s(D.prototype,{update:function(a,b){var c=this.chart,a=c.options[this.coll][this.options.index]=y(this.userOptions,a);this.destroy(!0);this._addedPlotLB=r;this.init(c,s(a,{events:r}));c.isDirtyBox=!0;q(b,!0)&&c.redraw()},remove:function(a){for(var b= -this.chart,c=this.coll,d=this.series,e=d.length;e--;)d[e]&&d[e].remove(!1);ta(b.axes,this);ta(b[c],this);b.options[c].splice(this.options.index,1);p(b[c],function(a,b){a.options.index=b});this.destroy();b.isDirtyBox=!0;q(a,!0)&&b.redraw()},setTitle:function(a,b){this.update({title:a},b)},setCategories:function(a,b){this.update({categories:a},b)}});var Ca=ia(M);B.line=Ca;W.area=y(L,{threshold:0});var wa=ia(M,{type:"area",getSegments:function(){var a=this,b=[],c=[],d=[],e=this.xAxis,f=this.yAxis,g= -f.stacks[this.stackKey],h={},i,j,k=this.points,l=this.options.connectNulls,m,n;if(this.options.stacking&&!this.cropped){for(m=0;m=0;d--)g=q(a[d].yBottom,f),da&&i>e?(i=v(a,e),k=2*e-i):ig&&k>e?(k=v(g,e),i=2*e-k):k0.5*a.xAxis.len?0:1),e=a.yAxis,f=a.translatedThreshold=e.getThreshold(c.threshold),g=q(c.minPointLength,5),h=a.getColumnMetrics(),i=h.width,j=a.barW=v(i,1+2*d),k=a.pointXOffset=h.offset,l=-(d%2?0.5:0),m=d%2?0.5:1;b.renderer.isVML&&b.inverted&&(m+=1);c.pointPadding&&(j=Ya(j));M.prototype.translate.apply(a);p(a.points,function(c){var d=q(c.yBottom,f),h=A(v(-999- -d,c.plotY),e.len+999+d),p=c.plotX+k,r=j,w=A(h,d),s;s=v(h,d)-w;P(s)g?d-g:f-(e.translate(c.y,0,1,0,1)<=f?g:0)));c.barX=p;c.pointWidth=i;c.tooltipPos=b.inverted?[e.len-h,a.xAxis.len-p-r/2]:[p+r/2,h+e.pos-b.plotTop];r=t(p+r)+l;p=t(p)+l;r-=p;d=P(w)<0.5;s=t(w+s)+m;w=t(w)+m;s-=w;d&&(w-=1,s+=1);c.shapeType="rect";c.shapeArgs={x:p,y:w,width:r,height:s}})},getSymbol:ra,drawLegendSymbol:T.drawRectangle,drawGraph:ra,drawPoints:function(){var a=this,b=this.chart,c=a.options,d=b.renderer, -e=c.animationLimit||250,f,g;p(a.points,function(h){var i=h.plotY,j=h.graphic;if(i!==r&&!isNaN(i)&&h.y!==null)f=h.shapeArgs,i=u(a.borderWidth)?{"stroke-width":a.borderWidth}:{},g=h.pointAttr[h.selected?"select":""]||a.pointAttr[""],j?(hb(j),j.attr(i)[b.pointCount {series.name}
',pointFormat:"x: {point.x}
y: {point.y}
"},stickyTracking:!1});wa=ia(M,{type:"scatter",sorted:!1,requireSorting:!1,noSharedTooltip:!0,trackerGroups:["markerGroup","dataLabelsGroup"],takeOrdinalPosition:!1,singularTooltips:!0,drawGraph:function(){this.options.lineWidth&&M.prototype.drawGraph.call(this)}});B.scatter=wa;W.pie=y(L,{borderColor:"#FFFFFF",borderWidth:1, -center:[null,null],clip:!1,colorByPoint:!0,dataLabels:{distance:30,enabled:!0,formatter:function(){return this.point.name}},ignoreHiddenPoint:!0,legendType:"point",marker:null,size:null,showInLegend:!1,slicedOffset:10,states:{hover:{brightness:0.1,shadow:!1}},stickyTracking:!1,tooltip:{followPointer:!0}});L={type:"pie",isCartesian:!1,pointClass:ia(Ba,{init:function(){Ba.prototype.init.apply(this,arguments);var a=this,b;if(a.y<0)a.y=null;s(a,{visible:a.visible!==!1,name:q(a.name,"Slice")});b=function(b){a.slice(b.type=== -"select")};z(a,"select",b);z(a,"unselect",b);return a},setVisible:function(a){var b=this,c=b.series,d=c.chart;b.visible=b.options.visible=a=a===r?!b.visible:a;c.options.data[Qa(b,c.data)]=b.options;p(["graphic","dataLabel","connector","shadowGroup"],function(c){if(b[c])b[c][a?"show":"hide"](!0)});b.legendItem&&d.legend.colorizeItem(b,a);if(!c.isDirty&&c.options.ignoreHiddenPoint)c.isDirty=!0,d.redraw()},slice:function(a,b,c){var d=this.series;ab(c,d.chart);q(b,!0);this.sliced=this.options.sliced= -a=u(a)?a:!this.sliced;d.options.data[Qa(this,d.data)]=this.options;a=a?this.slicedTranslation:{translateX:0,translateY:0};this.graphic.animate(a);this.shadowGroup&&this.shadowGroup.animate(a)},haloPath:function(a){var b=this.shapeArgs,c=this.series.chart;return this.sliced||!this.visible?[]:this.series.chart.renderer.symbols.arc(c.plotLeft+b.x,c.plotTop+b.y,b.r+a,b.r+a,{innerR:this.shapeArgs.r,start:b.start,end:b.end})}}),requireSorting:!1,noSharedTooltip:!0,trackerGroups:["group","dataLabelsGroup"], -axisTypes:[],pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color"},singularTooltips:!0,getColor:ra,animate:function(a){var b=this,c=b.points,d=b.startAngleRad;if(!a)p(c,function(a){var c=a.graphic,a=a.shapeArgs;c&&(c.attr({r:b.center[3]/2,start:d,end:d}),c.animate({r:a.r,start:a.start,end:a.end},b.options.animation))}),b.animate=null},setData:function(a,b,c,d){M.prototype.setData.call(this,a,!1,c,d);this.processData();this.generatePoints();q(b,!0)&&this.chart.redraw(c)}, -generatePoints:function(){var a,b=0,c,d,e,f=this.options.ignoreHiddenPoint;M.prototype.generatePoints.call(this);c=this.points;d=c.length;for(a=0;a0?e.y/b*100:0,e.total=b},translate:function(a){this.generatePoints();var b=0,c=this.options,d=c.slicedOffset,e=d+c.borderWidth,f,g,h,i=c.startAngle||0,j=this.startAngleRad=ua/180*(i-90),i=(this.endAngleRad=ua/180*(q(c.endAngle,i+360)-90))-j,k=this.points,l=c.dataLabels.distance, -c=c.ignoreHiddenPoint,m,n=k.length,o;if(!a)this.center=a=this.getCenter();this.getX=function(b,c){h=X.asin(A((b-a[1])/(a[2]/2+l),1));return a[0]+(c?-1:1)*fa(h)*(a[2]/2+l)};for(m=0;m1.5*ua?h-=2*ua:h<-ua/2&&(h+=2*ua);o.slicedTranslation={translateX:t(fa(h)*d),translateY:t(ka(h)*d)};f=fa(h)*a[2]/2;g=ka(h)*a[2]/2;o.tooltipPos= -[a[0]+f*0.7,a[1]+g*0.7];o.half=h<-ua/2||h>ua/2?1:0;o.angle=h;e=A(e,l/2);o.labelPos=[a[0]+f+fa(h)*l,a[1]+g+ka(h)*l,a[0]+f+fa(h)*e,a[1]+g+ka(h)*e,a[0]+f,a[1]+g,l<0?"center":o.half?"right":"left",h]}},drawGraph:null,drawPoints:function(){var a=this,b=a.chart.renderer,c,d,e=a.options.shadow,f,g;if(e&&!a.shadowGroup)a.shadowGroup=b.g("shadow").add(a.group);p(a.points,function(h){d=h.graphic;g=h.shapeArgs;f=h.shadowGroup;if(e&&!f)f=h.shadowGroup=b.g("shadow").add(a.shadowGroup);c=h.sliced?h.slicedTranslation: -{translateX:0,translateY:0};f&&f.attr(c);d?d.animate(s(g,c)):h.graphic=d=b[h.shapeType](g).setRadialReference(a.center).attr(h.pointAttr[h.selected?"select":""]).attr({"stroke-linejoin":"round"}).attr(c).add(a.group).shadow(e,f);h.visible!==void 0&&h.setVisible(h.visible)})},sortByAngle:function(a,b){a.sort(function(a,d){return a.angle!==void 0&&(d.angle-a.angle)*b})},drawLegendSymbol:T.drawRectangle,getCenter:ga.getCenter,getSymbol:ra};L=ia(M,L);B.pie=L;M.prototype.drawDataLabels=function(){var a= -this,b=a.options,c=b.cursor,d=b.dataLabels,e=a.points,f,g,h=a.hasRendered||0,i,j;if(d.enabled||a._hasPointLabels)a.dlProcessOptions&&a.dlProcessOptions(d),j=a.plotGroup("dataLabelsGroup","data-labels",d.defer?"hidden":"visible",d.zIndex||6),q(d.defer,!0)&&(j.attr({opacity:+h}),h||z(a,"afterAnimate",function(){a.visible&&j.show();j[b.animation?"animate":"attr"]({opacity:1},{duration:200})})),g=d,p(e,function(b){var e,h=b.dataLabel,n,o,p=b.connector,t=!0;f=b.options&&b.options.dataLabels;e=q(f&&f.enabled, -g.enabled);if(h&&!e)b.dataLabel=h.destroy();else if(e){d=y(g,f);e=d.rotation;n=b.getLabelConfig();i=d.format?Ma(d.format,n):d.formatter.call(n,d);d.style.color=q(d.color,d.style.color,a.color,"black");if(h)if(u(i))h.attr({text:i}),t=!1;else{if(b.dataLabel=h=h.destroy(),p)b.connector=p.destroy()}else if(u(i)){h={fill:d.backgroundColor,stroke:d.borderColor,"stroke-width":d.borderWidth,r:d.borderRadius||0,rotation:e,padding:d.padding,zIndex:1};for(o in h)h[o]===r&&delete h[o];h=b.dataLabel=a.chart.renderer[e? -"text":"label"](i,0,-999,null,null,null,d.useHTML).attr(h).css(s(d.style,c&&{cursor:c})).add(j).shadow(d.shadow)}h&&a.alignDataLabel(b,h,d,null,t)}})};M.prototype.alignDataLabel=function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=q(a.plotX,-999),i=q(a.plotY,-999),j=b.getBBox();if(a=this.visible&&(a.series.forceDL||f.isInsidePlot(h,t(i),g)||d&&f.isInsidePlot(h,g?d.x+1:d.y+d.height-1,g)))d=s({x:g?f.plotWidth-i:h,y:t(g?f.plotHeight-h:i),width:0,height:0},d),s(c,{width:j.width,height:j.height}),c.rotation? -b[e?"attr":"animate"]({x:d.x+c.x+d.width/2,y:d.y+c.y+d.height/2}).attr({align:c.align}):(b.align(c,null,d),g=b.alignAttr,q(c.overflow,"justify")==="justify"?this.justifyDataLabel(b,c,g,j,d,e):q(c.crop,!0)&&(a=f.isInsidePlot(g.x,g.y)&&f.isInsidePlot(g.x+j.width,g.y+j.height)));if(!a)b.attr({y:-999}),b.placed=!1};M.prototype.justifyDataLabel=function(a,b,c,d,e,f){var g=this.chart,h=b.align,i=b.verticalAlign,j,k;j=c.x;if(j<0)h==="right"?b.align="left":b.x=-j,k=!0;j=c.x+d.width;if(j>g.plotWidth)h==="left"? -b.align="right":b.x=g.plotWidth-j,k=!0;j=c.y;if(j<0)i==="bottom"?b.verticalAlign="top":b.y=-j,k=!0;j=c.y+d.height;if(j>g.plotHeight)i==="top"?b.verticalAlign="bottom":b.y=g.plotHeight-j,k=!0;if(k)a.placed=!f,a.align(b,null,e)};if(B.pie)B.pie.prototype.drawDataLabels=function(){var a=this,b=a.data,c,d=a.chart,e=a.options.dataLabels,f=q(e.connectorPadding,10),g=q(e.connectorWidth,1),h=d.plotWidth,i=d.plotHeight,j,k,l=q(e.softConnector,!0),m=e.distance,n=a.center,o=n[2]/2,x=n[1],r=m>0,s,w,u,y=[[],[]], -z,O,E,G,C,R=[0,0,0,0],H=function(a,b){return b.y-a.y};if(a.visible&&(e.enabled||a._hasPointLabels)){M.prototype.drawDataLabels.apply(a);p(b,function(a){a.dataLabel&&a.visible&&y[a.half].push(a)});for(G=2;G--;){var B=[],K=[],F=y[G],J=F.length,D;if(J){a.sortByAngle(F,G-0.5);for(C=b=0;!b&&F[C];)b=F[C]&&F[C].dataLabel&&(F[C].dataLabel.getBBox().height||21),C++;if(m>0){w=A(x+o+m,d.plotHeight);for(C=v(0,x-o-m);C<=w;C+=b)B.push(C);w=B.length;if(J>w){c=[].concat(F);c.sort(H);for(C=J;C--;)c[C].rank=C;for(C= -J;C--;)F[C].rank>=w&&F.splice(C,1);J=F.length}for(C=0;C0){if(w=K.pop(),D=w.i,O=w.y,c>O&&B[D+1]!==null||ch-f&&(R[1]=v(t(z+w-h+f),R[1])),O-b/2<0?R[0]=v(t(-O+b/2),R[0]):O+b/2>i&&(R[2]=v(t(O+b/2-i),R[2]))}}}if(Ea(R)===0||this.verifyDataLabelOverflow(R))this.placeDataLabels(),r&&g&&p(this.points,function(b){j=b.connector;u=b.labelPos;if((s=b.dataLabel)&&s._pos)E=s._attr.visibility,z=s.connX,O=s.connY,k= -l?["M",z+(u[6]==="left"?5:-5),O,"C",z,O,2*u[2]-u[4],2*u[3]-u[5],u[2],u[3],"L",u[4],u[5]]:["M",z+(u[6]==="left"?5:-5),O,"L",u[2],u[3],"L",u[4],u[5]],j?(j.animate({d:k}),j.attr("visibility",E)):b.connector=j=a.chart.renderer.path(k).attr({"stroke-width":g,stroke:e.connectorColor||b.color||"#606060",visibility:E}).add(a.dataLabelsGroup);else if(j)b.connector=j.destroy()})}},B.pie.prototype.placeDataLabels=function(){p(this.points,function(a){var a=a.dataLabel,b;if(a)(b=a._pos)?(a.attr(a._attr),a[a.moved? -"animate":"attr"](b),a.moved=!0):a&&a.attr({y:-999})})},B.pie.prototype.alignDataLabel=ra,B.pie.prototype.verifyDataLabelOverflow=function(a){var b=this.center,c=this.options,d=c.center,e=c=c.minSize||80,f;d[0]!==null?e=v(b[2]-v(a[1],a[3]),c):(e=v(b[2]-a[1]-a[3],c),b[0]+=(a[3]-a[1])/2);d[1]!==null?e=v(A(e,b[2]-v(a[0],a[2])),c):(e=v(A(e,b[2]-a[0]-a[2]),c),b[1]+=(a[0]-a[2])/2);eq(this.translatedThreshold,f.plotSizeY),j=q(c.inside,!!this.options.stacking);if(h&&(d=y(h),g&&(d={x:f.plotWidth-d.y-d.height,y:f.plotHeight-d.x-d.width,width:d.height,height:d.width}),!j))g?(d.x+=i?0:d.width,d.width=0):(d.y+=i?d.height:0,d.height=0);c.align=q(c.align,!g||j?"center":i?"right":"left");c.verticalAlign=q(c.verticalAlign,g||j?"middle": -i?"top":"bottom");M.prototype.alignDataLabel.call(this,a,b,c,d,e)};var jb=N.TrackerMixin={drawTrackerPoint:function(){var a=this,b=a.chart,c=b.pointer,d=a.options.cursor,e=d&&{cursor:d},f=function(c){var d=c.target,e;if(b.hoverSeries!==a)a.onMouseOver();for(;d&&!e;)e=d.point,d=d.parentNode;if(e!==r&&e!==b.hoverPoint)e.onMouseOver(c)};p(a.points,function(a){if(a.graphic)a.graphic.element.point=a;if(a.dataLabel)a.dataLabel.element.point=a});if(!a._hasTracking)p(a.trackerGroups,function(b){if(a[b]&& -(a[b].addClass("highcharts-tracker").on("mouseover",f).on("mouseout",function(a){c.onTrackerMouseOut(a)}).css(e),db))a[b].on("touchstart",f)}),a._hasTracking=!0},drawTrackerGraph:function(){var a=this,b=a.options,c=b.trackByArea,d=[].concat(c?a.areaPath:a.graphPath),e=d.length,f=a.chart,g=f.pointer,h=f.renderer,i=f.options.tooltip.snap,j=a.tracker,k=b.cursor,l=k&&{cursor:k},k=a.singlePoints,m,n=function(){if(f.hoverSeries!==a)a.onMouseOver()},o="rgba(192,192,192,"+(ea?1.0E-4:0.002)+")";if(e&&!c)for(m= -e+1;m--;)d[m]==="M"&&d.splice(m+1,0,d[m+1]-i,d[m+2],"L"),(m&&d[m]==="M"||m===e)&&d.splice(m,0,"L",d[m-2]+i,d[m-1]);for(m=0;mA(k.dataMin,k.min)&&i=f.min&&c<=f.max){h=b[i+1];c=d===r?0:d+1;for(d=b[i+1]?A(v(0,V((e.clientX+(h?h.wrappedClientX||h.clientX:g))/2)),g):g;c>=0&&c<=d;)j[c++]=e}this.tooltipPoints=j}},show:function(){this.setVisible(!0)},hide:function(){this.setVisible(!1)},select:function(a){this.selected=a=a===r?!this.selected:a;if(this.checkbox)this.checkbox.checked=a; -J(this,a?"select":"unselect")},drawTracker:jb.drawTrackerGraph});Q(M.prototype,"init",function(a){var b;a.apply(this,Array.prototype.slice.call(arguments,1));(b=this.xAxis)&&b.options.ordinal&&z(this,"updatedData",function(){delete b.ordinalIndex})});Q(D.prototype,"getTimeTicks",function(a,b,c,d,e,f,g,h){var i=0,j=0,k,l={},m,n,o,p=[],q=-Number.MAX_VALUE,t=this.options.tickPixelInterval;if(!this.options.ordinal||!f||f.length<3||c===r)return a.call(this,b,c,d,e);for(n=f.length;j -d;f[j]g*5||o){if(f[j]>q){for(k=a.call(this,b,f[i],f[j],e);k.length&&k[0]<=q;)k.shift();k.length&&(q=k[k.length-1]);p=p.concat(k)}i=j+1}if(o)break}a=k.info;if(h&&a.unitRange<=G.hour){j=p.length-1;for(i=1;id?a-1:a;for(w=void 0;h--;)i=j[h],d=w-i,w&&d2){d=b[1]- -b[0];for(g=a-1;g--&&!c;)b[g+1]-b[g]!==d&&(c=!0);if(!this.options.keepOrdinalPadding&&(b[0]-f>d||e-b[b.length-1]>d))c=!0}c?(this.ordinalPositions=b,c=this.val2lin(v(f,b[0]),!0),d=v(this.val2lin(A(e,b[b.length-1]),!0),1),this.ordinalSlope=e=(e-f)/(d-c),this.ordinalOffset=f-c*e):this.ordinalPositions=this.ordinalSlope=this.ordinalOffset=r}this.groupIntervalFactor=null},val2lin:function(a,b){var c=this.ordinalPositions;if(c){var d=c.length,e,f;for(e=d;e--;)if(c[e]===a){f=e;break}for(e=d-1;e--;)if(a>c[e]|| -e===0){c=(a-c[e])/(c[e+1]-c[e]);f=e+c;break}return b?f:this.ordinalSlope*(f||0)+this.ordinalOffset}else return a},lin2val:function(a,b){var c=this.ordinalPositions;if(c){var d=this.ordinalSlope,e=this.ordinalOffset,f=c.length-1,g,h;if(b)a<0?a=c[0]:a>f?a=c[f]:(f=V(a),h=a-f);else for(;f--;)if(g=d*f+e,a>=g){d=d*(f+1)+e;h=(a-g)/(d-g);break}return h!==r&&c[f]!==r?c[f]+(h?h*(c[f+1]-c[f]):0):a}else return a},getExtendedPositions:function(){var a=this.chart,b=this.series[0].currentDataGrouping,c=this.ordinalIndex, -d=b?b.count+b.unitName:"raw",e=this.getExtremes(),f,g;if(!c)c=this.ordinalIndex={};if(!c[d])f={series:[],getExtremes:function(){return{min:e.dataMin,max:e.dataMax}},options:{ordinal:!0},val2lin:D.prototype.val2lin},p(this.series,function(c){g={xAxis:f,xData:c.xData,chart:a,destroyGroupedData:ra};g.options={dataGrouping:b?{enabled:!0,forced:!0,approximation:"open",units:[[b.unitName,[b.count]]]}:{enabled:!1}};c.processData.apply(g);f.series.push(g)}),this.beforeSetTickPositions.apply(f),c[d]=f.ordinalPositions; -return c[d]},getGroupIntervalFactor:function(a,b,c){var d=0,c=c.processedXData,e=c.length,f=[],g=this.groupIntervalFactor;if(!g){for(;d1)k&&p(k,function(a){a.setState()}),f<0?(k=m,o=c.ordinalPositions?c:m):(k=c.ordinalPositions?c:m,o=m),m=o.ordinalPositions,h>m[m.length-1]&&m.push(h),this.fixedRange=j-i,f=c.toFixedRange(null,null,l.apply(k,[n.apply(k,[i,!0])+f,!0]),l.apply(o,[n.apply(o,[j,!0])+ -f,!0])),f.min>=A(g.dataMin,i)&&f.max<=v(h,j)&&c.setExtremes(f.min,f.max,!0,!1,{trigger:"pan"}),this.mouseDownX=d,K(this.container,{cursor:"move"})}else e=!0}else e=!0;e&&a.apply(this,Array.prototype.slice.call(arguments,1))});Q(M.prototype,"getSegments",function(a){var b,c=this.options.gapSize,d=this.xAxis;a.apply(this,Array.prototype.slice.call(arguments,1));if(c)b=this.segments,p(b,function(a,f){for(var g=a.length-1;g--;)a[g+1].x-a[g].x>d.closestPointRange*c&&b.splice(f+1,0,a.splice(g+1,a.length- -g))})});var da=M.prototype,L=Jb.prototype,hc=da.processData,ic=da.generatePoints,jc=da.destroy,kc=L.tooltipHeaderFormatter,lc={approximation:"average",groupPixelWidth:2,dateTimeLabelFormats:{millisecond:["%A, %b %e, %H:%M:%S.%L","%A, %b %e, %H:%M:%S.%L","-%H:%M:%S.%L"],second:["%A, %b %e, %H:%M:%S","%A, %b %e, %H:%M:%S","-%H:%M:%S"],minute:["%A, %b %e, %H:%M","%A, %b %e, %H:%M","-%H:%M"],hour:["%A, %b %e, %H:%M","%A, %b %e, %H:%M","-%H:%M"],day:["%A, %b %e, %Y","%A, %b %e","-%A, %b %e, %Y"],week:["Week from %A, %b %e, %Y", -"%A, %b %e","-%A, %b %e, %Y"],month:["%B %Y","%B","-%B %Y"],year:["%Y","%Y","-%Y"]}},Wb={line:{},spline:{},area:{},areaspline:{},column:{approximation:"sum",groupPixelWidth:10},arearange:{approximation:"range"},areasplinerange:{approximation:"range"},columnrange:{approximation:"range",groupPixelWidth:10},candlestick:{approximation:"ohlc",groupPixelWidth:10},ohlc:{approximation:"ohlc",groupPixelWidth:5}},Xb=[["millisecond",[1,2,5,10,20,25,50,100,200,500]],["second",[1,2,5,10,15,30]],["minute",[1,2, -5,10,15,30]],["hour",[1,2,3,4,6,8,12]],["day",[1]],["week",[1]],["month",[1,3,6]],["year",null]],Ra={sum:function(a){var b=a.length,c;if(!b&&a.hasNulls)c=null;else if(b)for(c=0;b--;)c+=a[b];return c},average:function(a){var b=a.length,a=Ra.sum(a);typeof a==="number"&&b&&(a/=b);return a},open:function(a){return a.length?a[0]:a.hasNulls?null:r},high:function(a){return a.length?Ea(a):a.hasNulls?null:r},low:function(a){return a.length?Ua(a):a.hasNulls?null:r},close:function(a){return a.length?a[a.length- -1]:a.hasNulls?null:r},ohlc:function(a,b,c,d){a=Ra.open(a);b=Ra.high(b);c=Ra.low(c);d=Ra.close(d);if(typeof a==="number"||typeof b==="number"||typeof c==="number"||typeof d==="number")return[a,b,c,d]},range:function(a,b){a=Ra.low(a);b=Ra.high(b);if(typeof a==="number"||typeof b==="number")return[a,b]}};da.groupData=function(a,b,c,d){var e=this.data,f=this.options.data,g=[],h=[],i=a.length,j,k,l=!!b,m=[[],[],[],[]],d=typeof d==="function"?d:Ra[d],n=this.pointArrayMap,o=n&&n.length,p;for(p=0;p<=i;p++)if(a[p]>= -c[0])break;for(;p<=i;p++){for(;c[1]!==r&&a[p]>=c[1]||p===i;)if(j=c.shift(),k=d.apply(0,m),k!==r&&(g.push(j),h.push(k)),m[0]=[],m[1]=[],m[2]=[],m[3]=[],p===i)break;if(p===i)break;if(n){j=this.cropStart+p;j=e&&e[j]||this.pointClass.prototype.applyOptions.apply({series:this},[f[j]]);var q;for(k=0;k0;)f[c]+=h/2;f[0]=d}this.currentDataGrouping=j.info;if(b.pointRange===null)this.pointRange=j.info.totalRange;this.closestPointRange=j.info.totalRange;if(u(f[0])&&f[0]=g.closestPointRange||G[h]<=G.day&&a.key%G[h]>0){e=d[h][0];break}e=xa(e,a.key);f&&(e+=xa(f,a.key+b.totalRange-1));a=c.headerFormat.replace("{point.key}",e)}else a=kc.call(this,a);return a};da.destroy=function(){for(var a=this.groupedData||[],b=a.length;b--;)a[b]&&a[b].destroy();jc.apply(this)};Q(da,"setOptions",function(a,b){var c=a.call(this,b),d=this.type,e=this.chart.options.plotOptions, -f=W[d].dataGrouping;if(Wb[d])f||(f=y(lc,Wb[d])),c.dataGrouping=y(f,e.series&&e.series.dataGrouping,e[d].dataGrouping,b.dataGrouping);if(this.chart.options._stock)this.requireSorting=!0;return c});Q(D.prototype,"setScale",function(a){a.call(this);p(this.series,function(a){a.hasProcessed=!1})});D.prototype.getGroupPixelWidth=function(){var a=this.series,b=a.length,c,d=0,e=!1,f;for(c=b;c--;)(f=a[c].options.dataGrouping)&&(d=v(d,f.groupPixelWidth));for(c=b;c--;)if((f=a[c].options.dataGrouping)&&a[c].hasProcessed)if(b= -(a[c].processedXData||a[c].data).length,a[c].groupPixelWidth||b>this.chart.plotSizeX/d||b&&f.forced)e=!0;return e?d:0};W.ohlc=y(W.column,{lineWidth:1,tooltip:{pointFormat:' {series.name}
Open: {point.open}
High: {point.high}
Low: {point.low}
Close: {point.close}
'},states:{hover:{lineWidth:3}},threshold:null});L=ia(B.column,{type:"ohlc",pointArrayMap:["open","high","low","close"],toYData:function(a){return[a.open,a.high,a.low, -a.close]},pointValKey:"high",pointAttrToOptions:{stroke:"color","stroke-width":"lineWidth"},upColorProp:"stroke",getAttribs:function(){B.column.prototype.getAttribs.apply(this,arguments);var a=this.options,b=a.states,a=a.upColor||this.color,c=y(this.pointAttr),d=this.upColorProp;c[""][d]=a;c.hover[d]=b.hover.upColor||a;c.select[d]=b.select.upColor||a;p(this.points,function(a){if(a.open"}, -threshold:null,y:-30});B.flags=ia(B.column,{type:"flags",sorted:!1,noSharedTooltip:!0,allowDG:!1,takeOrdinalPosition:!1,trackerGroups:["markerGroup"],forceCrop:!0,init:M.prototype.init,pointAttrToOptions:{fill:"fillColor",stroke:"color","stroke-width":"lineWidth",r:"radius"},translate:function(){B.column.prototype.translate.apply(this);var a=this.chart,b=this.points,c=b.length-1,d,e,f=this.options.onSeries,f=(d=f&&a.get(f))&&d.options.step,g=d&&d.points,h=g&&g.length,i=this.xAxis,j=i.getExtremes(), -k,l,m;if(d&&d.visible&&h){d=d.currentDataGrouping;l=g[h-1].x+(d?d.totalRange:0);for(b.sort(function(a,b){return a.x-b.x});h--&&b[c];)if(d=b[c],k=g[h],k.x<=d.x&&k.plotY!==r){if(d.x<=l)d.plotY=k.plotY,k.x=j.min&&c.x<=j.max?c.plotY=a.chartHeight-i.bottom-(i.opposite?i.height:0)+i.offset-a.plotTop:c.shapeArgs={};if((e=b[d-1])&&e.plotX===c.plotX){if(e.stackIndex=== -r)e.stackIndex=0;c.stackIndex=e.stackIndex+1}})},drawPoints:function(){var a,b=this.pointAttr[""],c=this.points,d=this.chart.renderer,e,f,g=this.options,h=g.y,i,j,k,l,m=g.lineWidth%2/2,n,o;for(j=c.length;j--;)if(k=c[j],a=k.plotX>this.xAxis.len,e=k.plotX+(a?m:-m),l=k.stackIndex,i=k.options.shape||g.shape,f=k.plotY,f!==r&&(f=k.plotY+h+m-(l!==r&&l*g.stackDistance)),n=l?r:k.plotX+m,o=l?r:k.plotY,l=k.graphic,f!==r&&e>=0&&!a)a=k.pointAttr[k.selected?"select":""]||b,l?l.attr({x:e,y:f,r:a.r,anchorX:n,anchorY:o}): -k.graphic=d.label(k.options.title||g.title||"A",e,f,i,n,o,g.useHTML).css(y(g.style,k.style)).attr(a).attr({align:i==="flag"?"left":"center",width:g.width,height:g.height}).add(this.markerGroup).shadow(g.shadow),k.tooltipPos=[e,f];else if(l)k.graphic=l.destroy()},drawTracker:function(){var a=this.points;jb.drawTrackerPoint.apply(this);p(a,function(b){var c=b.graphic;c&&z(c.element,"mouseover",function(){if(b.stackIndex>0&&!b.raised)b._y=c.y,c.attr({y:b._y-8}),b.raised=!0;p(a,function(a){if(a!==b&& -a.raised&&a.graphic)a.graphic.attr({y:a._y}),a.raised=!1})})})},animate:ra});tb.flag=function(a,b,c,d,e){var f=e&&e.anchorX||a,e=e&&e.anchorY||b;return["M",f,e,"L",a,b+d,a,b,a+c,b,a+c,b+d,a,b+d,"M",f,e,"Z"]};p(["circle","square"],function(a){tb[a+"pin"]=function(b,c,d,e,f){var g=f&&f.anchorX,f=f&&f.anchorY,b=tb[a](b,c,d,e);g&&f&&b.push("M",g,c>f?c:c+e,"L",g,f);return b}});Za===N.VMLRenderer&&p(["flag","circlepin","squarepin"],function(a){ib.prototype.symbols[a]=tb[a]});var L=[].concat(Xb),ub=function(a){return Math[a].apply(0, -qb(arguments,function(a){return typeof a==="number"}))};L[4]=["day",[1,2,3,4]];L[5]=["week",[1,2,3]];s(F,{navigator:{handles:{backgroundColor:"#ebe7e8",borderColor:"#b2b1b6"},height:40,margin:25,maskFill:"rgba(128,179,236,0.3)",maskInside:!0,outlineColor:"#b2b1b6",outlineWidth:1,series:{type:B.areaspline===r?"line":"areaspline",color:"#4572A7",compare:null,fillOpacity:0.05,dataGrouping:{approximation:"average",enabled:!0,groupPixelWidth:2,smoothed:!0,units:L},dataLabels:{enabled:!1,zIndex:2},id:"highcharts-navigator-series", -lineColor:"#4572A7",lineWidth:1,marker:{enabled:!1},pointRange:0,shadow:!1,threshold:null},xAxis:{tickWidth:0,lineWidth:0,gridLineColor:"#EEE",gridLineWidth:1,tickPixelInterval:200,labels:{align:"left",style:{color:"#888"},x:3,y:-4},crosshair:!1},yAxis:{gridLineWidth:0,startOnTick:!1,endOnTick:!1,minPadding:0.1,maxPadding:0.1,labels:{enabled:!1},crosshair:!1,title:{text:null},tickWidth:0}},scrollbar:{height:fb?20:14,barBackgroundColor:"#bfc8d1",barBorderRadius:0,barBorderWidth:1,barBorderColor:"#bfc8d1", -buttonArrowColor:"#666",buttonBackgroundColor:"#ebe7e8",buttonBorderColor:"#bbb",buttonBorderRadius:0,buttonBorderWidth:1,minWidth:6,rifleColor:"#666",trackBackgroundColor:"#eeeeee",trackBorderColor:"#eeeeee",trackBorderWidth:1,liveRedraw:ea&&!fb}});Cb.prototype={drawHandle:function(a,b){var c=this.chart,d=c.renderer,e=this.elementsToDestroy,f=this.handles,g=this.navigatorOptions.handles,g={fill:g.backgroundColor,stroke:g.borderColor,"stroke-width":1},h;this.rendered||(f[b]=d.g("navigator-handle-"+ -["left","right"][b]).css({cursor:"e-resize"}).attr({zIndex:4-b}).add(),h=d.rect(-4.5,0,9,16,0,1).attr(g).add(f[b]),e.push(h),h=d.path(["M",-1.5,4,"L",-1.5,12,"M",0.5,4,"L",0.5,12]).attr(g).add(f[b]),e.push(h));f[b][c.isResizing?"animate":"attr"]({translateX:this.scrollerLeft+this.scrollbarHeight+parseInt(a,10),translateY:this.top+this.height/2-8})},drawScrollbarButton:function(a){var b=this.chart.renderer,c=this.elementsToDestroy,d=this.scrollbarButtons,e=this.scrollbarHeight,f=this.scrollbarOptions, -g;this.rendered||(d[a]=b.g().add(this.scrollbarGroup),g=b.rect(-0.5,-0.5,e+1,e+1,f.buttonBorderRadius,f.buttonBorderWidth).attr({stroke:f.buttonBorderColor,"stroke-width":f.buttonBorderWidth,fill:f.buttonBackgroundColor}).add(d[a]),c.push(g),g=b.path(["M",e/2+(a?-1:1),e/2-3,"L",e/2+(a?-1:1),e/2+3,e/2+(a?2:-2),e/2]).attr({fill:f.buttonArrowColor}).add(d[a]),c.push(g));a&&d[a].attr({translateX:this.scrollerWidth-e})},render:function(a,b,c,d){var e=this.chart,f=e.renderer,g,h,i,j,k=this.scrollbarGroup, -l=this.navigatorGroup,m=this.scrollbar,l=this.xAxis,n=this.scrollbarTrack,o=this.scrollbarHeight,p=this.scrollbarEnabled,r=this.navigatorOptions,s=this.scrollbarOptions,w=s.minWidth,u=this.height,y=this.top,z=this.navigatorEnabled,O=r.outlineWidth,B=O/2,E=0,C=this.outlineHeight,G=s.barBorderRadius,F=s.barBorderWidth,D=y+B,H;if(!isNaN(a)){this.navigatorLeft=g=q(l.left,e.plotLeft+o);this.navigatorWidth=h=q(l.len,e.plotWidth-2*o);this.scrollerLeft=i=g-o;this.scrollerWidth=j=j=h+2*o;l.getExtremes&&(H= -this.getUnionExtremes(!0))&&(H.dataMin!==l.min||H.dataMax!==l.max)&&l.setExtremes(H.dataMin,H.dataMax,!0,!1);c=q(c,l.translate(a));d=q(d,l.translate(b));if(isNaN(c)||P(c)===Infinity)c=0,d=j;if(!(l.translate(d,!0)-l.translate(c,!0)12?"visible":"hidden"})[e]({d:["M",w-3,o/4,"L",w-3,2*o/3,"M",w,o/4,"L",w,2*o/3,"M",w+3,o/4,"L",w+3,2*o/3]});this.scrollbarPad=E;this.rendered=!0}}},addEvents:function(){var a=this.chart.container,b=this.mouseDownHandler,c=this.mouseMoveHandler, -d=this.mouseUpHandler,e;e=[[a,"mousedown",b],[a,"mousemove",c],[document,"mouseup",d]];db&&e.push([a,"touchstart",b],[a,"touchmove",c],[document,"touchend",d]);p(e,function(a){z.apply(null,a)});this._events=e},removeEvents:function(){p(this._events,function(a){U.apply(null,a)});this._events=r;this.navigatorEnabled&&this.baseSeries&&U(this.baseSeries,"updatedData",this.updatedDataHandler)},init:function(){var a=this,b=a.chart,c,d,e=a.scrollbarHeight,f=a.navigatorOptions,g=a.height,h=a.top,i,j,k=document.body.style, -l,m=a.baseSeries;a.mouseDownHandler=function(d){var d=b.pointer.normalize(d),e=a.zoomedMin,f=a.zoomedMax,h=a.top,j=a.scrollbarHeight,m=a.scrollerLeft,n=a.scrollerWidth,o=a.navigatorLeft,p=a.navigatorWidth,q=a.scrollbarPad,r=a.range,s=d.chartX,t=d.chartY,d=b.xAxis[0],u,v=fb?10:7;if(t>h&&to+e-q&&sm&&sm+n-j?e+r*0.2:s=p)f=p-r,u=c.dataMax;if(f!==e)a.fixedWidth=r,e=c.toFixedRange(f,f+r,null,u),d.setExtremes(e.min,e.max,!0,!1,{trigger:"navigator"})}};a.mouseMoveHandler=function(c){var d=a.scrollbarHeight,e=a.navigatorLeft,f=a.navigatorWidth,g=a.scrollerLeft,h=a.scrollerWidth,k=a.range,l; -if(c.pageX!==0)c=b.pointer.normalize(c),l=c.chartX,lg+h-d&&(l=g+h-d),a.grabbedLeft?(j=!0,a.render(0,0,l-e,a.otherHandlePos)):a.grabbedRight?(j=!0,a.render(0,0,a.otherHandlePos,l-e)):a.grabbedCenter&&(j=!0,lf+i-k&&(l=f+i-k),a.render(0,0,l-i,l-i+k)),j&&a.scrollbarOptions.liveRedraw&&setTimeout(function(){a.mouseUpHandler(c)},0)};a.mouseUpHandler=function(d){var e,f;if(j){if(a.zoomedMin===a.otherHandlePos)e=a.fixedExtreme;else if(a.zoomedMax===a.otherHandlePos)f=a.fixedExtreme;e=c.toFixedRange(a.zoomedMin, -a.zoomedMax,e,f);b.xAxis[0].setExtremes(e.min,e.max,!0,!1,{trigger:"navigator",triggerOp:"navigator-drag",DOMEvent:d})}if(d.type!=="mousemove")a.grabbedLeft=a.grabbedRight=a.grabbedCenter=a.fixedWidth=a.fixedExtreme=a.otherHandlePos=j=i=null,k.cursor=l||""};var n=b.xAxis.length,o=b.yAxis.length;b.extraBottomMargin=a.outlineHeight+f.margin;a.navigatorEnabled?(a.xAxis=c=new D(b,y({ordinal:m&&m.xAxis.options.ordinal},f.xAxis,{id:"navigator-x-axis",isX:!0,type:"datetime",index:n,height:g,offset:0,offsetLeft:e, -offsetRight:-e,keepOrdinalPadding:!0,startOnTick:!1,endOnTick:!1,minPadding:0,maxPadding:0,zoomEnabled:!1})),a.yAxis=d=new D(b,y(f.yAxis,{id:"navigator-y-axis",alignTicks:!1,height:g,offset:0,index:o,zoomEnabled:!1})),m||f.series.data?a.addBaseSeries():b.series.length===0&&Q(b,"redraw",function(c,d){if(b.series.length>0&&!a.series)a.setBaseSeries(),b.redraw=c;c.call(b,d)})):a.xAxis=c={translate:function(a,c){var d=b.xAxis[0],f=d.getExtremes(),g=b.plotWidth-2*e,h=ub("min",d.options.min,f.dataMin), -d=ub("max",d.options.max,f.dataMax)-h;return c?a*d/g+h:g*(a-h)/d},toFixedRange:D.prototype.toFixedRange};Q(b,"getMargins",function(b){var e=this.legend,f=e.options;b.call(this);a.top=h=a.navigatorOptions.top||this.chartHeight-a.height-a.scrollbarHeight-this.spacing[2]-(f.verticalAlign==="bottom"&&f.enabled&&!f.floating?e.legendHeight+q(f.margin,10):0);if(c&&d)c.options.top=d.options.top=h,c.setAxisSize(),d.setAxisSize()});a.addEvents()},getUnionExtremes:function(a){var b=this.chart.xAxis[0],c=this.xAxis, -d=c.options,e=b.options;if(!a||b.dataMin!==null)return{dataMin:ub("min",d&&d.min,e.min,b.dataMin,c.dataMin),dataMax:ub("max",d&&d.max,e.max,b.dataMax,c.dataMax)}},setBaseSeries:function(a){var b=this.chart,a=a||b.options.navigator.baseSeries;this.series&&this.series.remove();this.baseSeries=b.series[a]||typeof a==="string"&&b.get(a)||b.series[0];this.xAxis&&this.addBaseSeries()},addBaseSeries:function(){var a=this.baseSeries,b=a?a.options:{},c=b.data,d=this.navigatorOptions.series,e;e=d.data;this.hasNavigatorData= -!!e;b=y(b,d,{enableMouseTracking:!1,group:"nav",padXAxis:!1,xAxis:"navigator-x-axis",yAxis:"navigator-y-axis",name:"Navigator",showInLegend:!1,isInternal:!0,visible:!0});b.data=e||c;this.series=this.chart.initSeries(b);if(a&&this.navigatorOptions.adaptToUpdatedData!==!1)z(a,"updatedData",this.updatedDataHandler),a.userOptions.events=s(a.userOptions.event,{updatedData:this.updatedDataHandler})},updatedDataHandler:function(){var a=this.chart.scroller,b=a.baseSeries,c=b.xAxis,d=c.getExtremes(),e=d.min, -f=d.max,g=d.dataMin,d=d.dataMax,h=f-e,i,j,k,l,m,n=a.series;i=n.xData;var o=!!c.setExtremes;j=f>=i[i.length-1]-(this.closestPointRange||0);i=e<=g;if(!a.hasNavigatorData)n.options.pointStart=b.xData[0],n.setData(b.options.data,!1),m=!0;i&&(l=g,k=l+h);j&&(k=d,i||(l=v(k-h,n.xData[0])));o&&(i||j)?isNaN(l)||c.setExtremes(l,k,!0,!1,{trigger:"updatedData"}):(m&&this.chart.redraw(!1),a.render(v(e,g),A(f,d)))},destroy:function(){this.removeEvents();p([this.xAxis,this.yAxis,this.leftShade,this.rightShade,this.outline, -this.scrollbarTrack,this.scrollbarRifles,this.scrollbarGroup,this.scrollbar],function(a){a&&a.destroy&&a.destroy()});this.xAxis=this.yAxis=this.leftShade=this.rightShade=this.outline=this.scrollbarTrack=this.scrollbarRifles=this.scrollbarGroup=this.scrollbar=null;p([this.scrollbarButtons,this.handles,this.elementsToDestroy],function(a){Na(a)})}};N.Scroller=Cb;Q(D.prototype,"zoom",function(a,b,c){var d=this.chart,e=d.options,f=e.chart.zoomType,g=e.navigator,e=e.rangeSelector,h;if(this.isXAxis&&(g&& -g.enabled||e&&e.enabled))if(f==="x")d.resetZoomButton="blocked";else if(f==="y")h=!1;else if(f==="xy")d=this.previousZoom,u(b)?this.previousZoom=[this.min,this.max]:d&&(b=d[0],c=d[1],delete this.previousZoom);return h!==r?h:a.call(this,b,c)});Q(ya.prototype,"init",function(a,b,c){z(this,"beforeRender",function(){var a=this.options;if(a.navigator.enabled||a.scrollbar.enabled)this.scroller=new Cb(this)});a.call(this,b,c)});Q(M.prototype,"addPoint",function(a,b,c,d,e){var f=this.options.turboThreshold; -f&&this.xData.length>f&&ha(b)&&!Ja(b)&&this.chart.scroller&&ma(20,!0);a.call(this,b,c,d,e)});s(F,{rangeSelector:{buttonTheme:{width:28,height:18,fill:"#f7f7f7",padding:2,r:0,"stroke-width":0,style:{color:"#444",cursor:"pointer",fontWeight:"normal"},zIndex:7,states:{hover:{fill:"#e7e7e7"},select:{fill:"#e7f0f9",style:{color:"black",fontWeight:"bold"}}}},inputPosition:{align:"right"},labelStyle:{color:"#666"}}});F.lang=y(F.lang,{rangeSelectorZoom:"Zoom",rangeSelectorFrom:"From",rangeSelectorTo:"To"}); -Db.prototype={clickButton:function(a,b){var c=this,d=c.selected,e=c.chart,f=c.buttons,g=c.buttonOptions[a],h=e.xAxis[0],i=e.scroller&&e.scroller.getUnionExtremes()||h||{},j=i.dataMin,k=i.dataMax,l,m=h&&t(A(h.max,q(k,h.max))),n=new ca(m),o=g.type,s=g.count,i=g._range,u;if(!(j===null||k===null||a===c.selected)){if(o==="month"||o==="year")l={month:"Month",year:"FullYear"}[o],n["set"+l](n["get"+l]()-s),l=n.getTime(),j=q(j,Number.MIN_VALUE),isNaN(l)||lg-f,o=e=g-f&&j[c].state!==2,q=a.type==="ytd"&&xa("%Y",f)===xa("%Y",g);e===t(d.max-d.min)&&c!==h?(b.setSelected(c),j[c].setState(2)):!i&&(n||o||p||q)?j[c].setState(3):j[c].state===3&&j[c].setState(0)})},computeButtonRange:function(a){var b=a.type,c=a.count||1,d={millisecond:1,second:1E3,minute:6E4,hour:36E5,day:864E5,week:6048E5};if(d[b])a._range=d[b]*c;else if(b==="month"|| -b==="year")a._range={month:30,year:365}[b]*864E5*c},setInputValue:function(a,b){var c=this.chart.options.rangeSelector;if(u(b))this[a+"Input"].HCTime=b;this[a+"Input"].value=xa(c.inputEditDateFormat||"%Y-%m-%d",this[a+"Input"].HCTime);this[a+"DateBox"].attr({text:xa(c.inputDateFormat||"%b %e, %Y",this[a+"Input"].HCTime)})},drawInput:function(a){var b=this,c=b.chart,d=c.renderer.style,e=c.renderer,f=c.options.rangeSelector,g=b.div,h=a==="min",i,j,k,l=this.inputGroup;this[a+"Label"]=j=e.label(F.lang[h? -"rangeSelectorFrom":"rangeSelectorTo"],this.inputGroup.offset).attr({padding:2}).css(y(d,f.labelStyle)).add(l);l.offset+=j.width+5;this[a+"DateBox"]=k=e.label("",l.offset).attr({padding:2,width:f.inputBoxWidth||90,height:f.inputBoxHeight||17,stroke:f.inputBoxBorderColor||"silver","stroke-width":1}).css(y({textAlign:"center",color:"#444"},d,f.inputStyle)).on("click",function(){b[a+"Input"].focus()}).add(l);l.offset+=k.width+(h?10:0);this[a+"Input"]=i=aa("input",{name:a,className:"highcharts-range-selector", -type:"text"},s({position:"absolute",border:0,width:"1px",height:"1px",padding:0,textAlign:"center",fontSize:d.fontSize,fontFamily:d.fontFamily,top:c.plotTop+"px"},f.inputStyle),g);i.onfocus=function(){K(this,{left:l.translateX+k.x+"px",top:l.translateY+"px",width:k.width-2+"px",height:k.height-2+"px",border:"2px solid silver"})};i.onblur=function(){K(this,{border:0,width:"1px",height:"1px"});b.setInputValue(a)};i.onchange=function(){var a=i.value,d=(f.inputDateParser||ca.parse)(a),e=c.xAxis[0],g= -e.dataMin,j=e.dataMax;isNaN(d)&&(d=a.split("-"),d=ca.UTC(H(d[0]),H(d[1])-1,H(d[2])));isNaN(d)||(F.global.useUTC||(d+=(new ca).getTimezoneOffset()*6E4),h?d>b.maxInput.HCTime?d=r:dj&&(d=j),d!==r&&c.xAxis[0].setExtremes(h?d:e.min,h?e.max:d,r,r,{trigger:"rangeSelectorInput"}))}},render:function(a,b){var c=this,d=c.chart,e=d.renderer,f=d.container,g=d.options,h=g.exporting&&g.navigation&&g.navigation.buttonOptions,i=g.rangeSelector,j=c.buttons,g=F.lang,k=c.div,k=c.inputGroup, -l=i.buttonTheme,m=i.inputEnabled!==!1,n=l&&l.states,o=d.plotLeft,r;if(!c.rendered&&(c.zoomText=e.text(g.rangeSelectorZoom,o,d.plotTop-20).css(i.labelStyle).add(),r=o+c.zoomText.getBBox().width+5,p(c.buttonOptions,function(a,b){j[b]=e.button(a.text,r,d.plotTop-35,function(){c.clickButton(b);c.isActive=!0},l,n&&n.hover,n&&n.select).css({textAlign:"center"}).add();r+=j[b].width+q(i.buttonSpacing,5);c.selected===b&&j[b].setState(2)}),c.updateButtonStates(),m))c.div=k=aa("div",null,{position:"relative", -height:0,zIndex:1}),f.parentNode.insertBefore(k,f),c.inputGroup=k=e.g("input-group").add(),k.offset=0,c.drawInput("min"),c.drawInput("max");m&&(f=d.plotTop-45,k.align(s({y:f,width:k.offset,x:h&&f<(h.y||0)+h.height-d.spacing[0]?-40:0},i.inputPosition),!0,d.spacingBox),c.setInputValue("min",a),c.setInputValue("max",b));c.rendered=!0},destroy:function(){var a=this.minInput,b=this.maxInput,c=this.chart,d=this.blurInputs,e;U(c.container,"mousedown",d);U(c,"resize",d);Na(this.buttons);if(a)a.onfocus=a.onblur= -a.onchange=null;if(b)b.onfocus=b.onblur=b.onchange=null;for(e in this)this[e]&&e!=="chart"&&(this[e].destroy?this[e].destroy():this[e].nodeType&&Va(this[e])),this[e]=null}};D.prototype.toFixedRange=function(a,b,c,d){var e=this.chart&&this.chart.fixedRange,a=q(c,this.translate(a,!0)),b=q(d,this.translate(b,!0)),c=e&&(b-a)/e;c>0.7&&c<1.3&&(d?a=b-e:b=a+e);return{min:a,max:b}};Q(ya.prototype,"init",function(a,b,c){z(this,"init",function(){if(this.options.rangeSelector.enabled)this.rangeSelector=new Db(this)}); -a.call(this,b,c)});N.RangeSelector=Db;ya.prototype.callbacks.push(function(a){function b(){f=a.xAxis[0].getExtremes();g.render(f.min,f.max)}function c(){f=a.xAxis[0].getExtremes();isNaN(f.min)||h.render(f.min,f.max)}function d(a){a.triggerOp!=="navigator-drag"&&g.render(a.min,a.max)}function e(a){h.render(a.min,a.max)}var f,g=a.scroller,h=a.rangeSelector;g&&(z(a.xAxis[0],"afterSetExtremes",d),Q(a,"drawChartBox",function(a){var c=this.isDirtyBox;a.call(this);c&&b()}),b());h&&(z(a.xAxis[0],"afterSetExtremes", -e),z(a,"resize",c),c());z(a,"destroy",function(){g&&U(a.xAxis[0],"afterSetExtremes",d);h&&(U(a,"resize",c),U(a.xAxis[0],"afterSetExtremes",e))})});N.StockChart=function(a,b){var c=a.series,d,e=q(a.navigator&&a.navigator.enabled,!0)?{startOnTick:!1,endOnTick:!1}:null,f={marker:{enabled:!1,radius:2},states:{hover:{lineWidth:2}}},g={shadow:!1,borderWidth:0};a.xAxis=za(qa(a.xAxis||{}),function(a){return y({minPadding:0,maxPadding:0,ordinal:!0,title:{text:null},labels:{overflow:"justify"},showLastLabel:!0}, -a,{type:"datetime",categories:null},e)});a.yAxis=za(qa(a.yAxis||{}),function(a){d=q(a.opposite,!0);return y({labels:{y:-2},opposite:d,showLastLabel:!1,title:{text:null}},a)});a.series=null;a=y({chart:{panning:!0,pinchType:"x"},navigator:{enabled:!0},scrollbar:{enabled:!0},rangeSelector:{enabled:!0},title:{text:null,style:{fontSize:"16px"}},tooltip:{shared:!0,crosshairs:!0},legend:{enabled:!1},plotOptions:{line:f,spline:f,area:f,areaspline:f,arearange:f,areasplinerange:f,column:g,columnrange:g,candlestick:g, -ohlc:g}},a,{_stock:!0,chart:{inverted:!1}});a.series=c;return new ya(a,b)};Q($a.prototype,"init",function(a,b,c){var d=c.chart.pinchType||"";a.call(this,b,c);this.pinchX=this.pinchHor=d.indexOf("x")!==-1;this.pinchY=this.pinchVert=d.indexOf("y")!==-1;this.hasZoom=this.hasZoom||this.pinchHor||this.pinchVert});Q(D.prototype,"autoLabelAlign",function(a){var b=this.chart,c=this.options,b=b._labelPanes=b._labelPanes||{},d=this.options.labels;if(this.chart.options._stock&&this.coll==="yAxis"&&(c=c.top+ -","+c.height,!b[c]&&d.enabled)){if(d.x===15)d.x=0;if(d.align===void 0)d.align="right";b[c]=1;return"right"}return a.call(this,[].slice.call(arguments,1))});D.prototype.getPlotLinePath=function(a,b,c,d,e){var f=this,g=this.isLinked&&!this.series?this.linkedParent.series:this.series,h=f.chart,i=h.renderer,j=f.left,k=f.top,l,m,n,o,r=[],s,v;if(f.coll==="xAxis"||f.coll==="yAxis")s=f.isXAxis?u(f.options.yAxis)?[h.yAxis[f.options.yAxis]]:za(g,function(a){return a.yAxis}):u(f.options.xAxis)?[h.xAxis[f.options.xAxis]]: -za(g,function(a){return a.xAxis});p(f.isXAxis?h.yAxis:h.xAxis,function(a){if(u(a.options.id)?a.options.id.indexOf("navigator")===-1:1){var b=a.isXAxis?"yAxis":"xAxis",b=u(a.options[b])?h[b][a.options[b]]:h[b][0];f===b&&s.push(a)}});v=s.length?[]:[f];p(s,function(a){Qa(a,v)===-1&&v.push(a)});e=q(e,f.translate(a,null,null,c));isNaN(e)||(f.horiz?p(v,function(a){m=a.top;o=m+a.len;l=n=t(e+f.transB);(l>=j&&l<=j+f.width||d)&&r.push("M",l,m,"L",n,o)}):p(v,function(a){l=a.left;n=l+a.width;m=o=t(k+f.height- -e);(m>=k&&m<=k+f.height||d)&&r.push("M",l,m,"L",n,o)}));if(r.length>0)return i.crispPolyLine(r,b||1)};D.prototype.getPlotBandPath=function(a,b){var c=this.getPlotLinePath(b),d=this.getPlotLinePath(a),e=[],f;if(d&&c)for(f=0;fj+this.height)this.hideCrosshair();else{!n&&!f.formatter&&(this.isDatetimeAxis&&(o="%b %d, %Y"),n="{value"+(o?":"+o:"")+"}");k.attr({text:n?Ma(n,{value:c[g]}):f.formatter.call(this,c[g]),x:l,y:m,visibility:"visible"});c=k.getBBox();if(b){if(this.options.tickPosition==="inside"&&!h||this.options.tickPosition!=="inside"&&h)m=k.y-c.height}else m=k.y-c.height/2;b?(d=i-c.x,e=i+this.width-c.x):(d=this.labelAlign==="left"?i: -0,e=this.labelAlign==="right"?i+this.width:a.chartWidth);k.translateX=e&&(l-=k.translateX+c.width-e);k.attr({x:l,y:m,visibility:"visible"})}}});var mc=da.init,nc=da.processData,oc=Ba.prototype.tooltipFormatter;da.init=function(){mc.apply(this,arguments);this.setCompare(this.options.compare)};da.setCompare=function(a){this.modifyValue=a==="value"||a==="percent"?function(b,c){var d=this.compareValue;if(b!==r&&(b=a==="value"?b-d:b=100*(b/d)-100,c))c.change= -b;return b}:null;if(this.chart.hasRendered)this.isDirty=!0};da.processData=function(){var a=0,b,c,d;nc.apply(this,arguments);if(this.xAxis&&this.processedYData){b=this.processedXData;c=this.processedYData;for(d=c.length;a=this.xAxis.min){this.compareValue=c[a];break}}};Q(da,"getExtremes",function(a){a.apply(this,[].slice.call(arguments,1));if(this.modifyValue)this.dataMax=this.modifyValue(this.dataMax),this.dataMin=this.modifyValue(this.dataMin)});D.prototype.setCompare= -function(a,b){this.isXAxis||(p(this.series,function(b){b.setCompare(a)}),q(b,!0)&&this.chart.redraw())};Ba.prototype.tooltipFormatter=function(a){a=a.replace("{point.change}",(this.change>0?"+":"")+Da(this.change,q(this.series.tooltipOptions.changeDecimals,2)));return oc.apply(this,[a])};Q(M.prototype,"render",function(a){if(this.chart.options._stock)!this.clipBox&&this.animate&&this.animate.toString().indexOf("sharedClip")!==-1?(this.clipBox=y(this.chart.clipBox),this.clipBox.width=this.xAxis.len, -this.clipBox.height=this.yAxis.len):this.chart[this.sharedClipKey]&&this.chart[this.sharedClipKey].attr({width:this.xAxis.len,height:this.yAxis.len});a.call(this)});s(N,{Axis:D,Chart:ya,Color:Ia,Point:Ba,Tick:bb,Renderer:Za,Series:M,SVGElement:Z,SVGRenderer:na,arrayMin:Ua,arrayMax:Ea,charts:ba,dateFormat:xa,format:Ma,pathAnim:Gb,getOptions:function(){return F},hasBidiBug:Yb,isTouchDevice:fb,numberFormat:Da,seriesTypes:B,setOptions:function(a){F=y(!0,F,a);Mb();return F},addEvent:z,removeEvent:U,createElement:aa, -discardElement:Va,css:K,each:p,extend:s,map:za,merge:y,pick:q,splat:qa,extendClass:ia,pInt:H,wrap:Q,svg:ea,canvas:la,vml:!ea&&!la,product:"Highstock",version:"2.0.4"})})(); + // Apply range + if (type === 'month' || type === 'year') { + if (!baseAxis) { + // This is set to the user options and picked up later when the axis is instantiated + // so that we know the min and max. + range = rangeOptions; + } else { + ctx = { + range: rangeOptions, + max: newMax, + dataMin: dataMin, + dataMax: dataMax + }; + newMin = baseAxis.minFromRange.call(ctx); + if (isNumber(ctx.newMax)) { + newMax = ctx.newMax; + } + } + + // Fixed times like minutes, hours, days + } else if (range) { + newMin = Math.max(newMax - range, dataMin); + newMax = Math.min(newMin + range, dataMax); + + } else if (type === 'ytd') { + + // On user clicks on the buttons, or a delayed action running from the beforeRender + // event (below), the baseAxis is defined. + if (baseAxis) { + // When "ytd" is the pre-selected button for the initial view, its calculation + // is delayed and rerun in the beforeRender event (below). When the series + // are initialized, but before the chart is rendered, we have access to the xData + // array (#942). + if (dataMax === undefined) { + dataMin = Number.MAX_VALUE; + dataMax = Number.MIN_VALUE; + each(chart.series, function (series) { + var xData = series.xData; // reassign it to the last item + dataMin = Math.min(xData[0], dataMin); + dataMax = Math.max(xData[xData.length - 1], dataMax); + }); + redraw = false; + } + ytdExtremes = rangeSelector.getYTDExtremes( + dataMax, + dataMin, + chart.time.useUTC + ); + newMin = rangeMin = ytdExtremes.min; + newMax = ytdExtremes.max; + + // "ytd" is pre-selected. We don't yet have access to processed point and extremes data + // (things like pointStart and pointInterval are missing), so we delay the process (#942) + } else { + addEvent(chart, 'beforeRender', function () { + rangeSelector.clickButton(i); + }); + return; + } + } else if (type === 'all' && baseAxis) { + newMin = dataMin; + newMax = dataMax; + } + + newMin += rangeOptions._offsetMin; + newMax += rangeOptions._offsetMax; + + rangeSelector.setSelected(i); + + // Update the chart + if (!baseAxis) { + // Axis not yet instanciated. Temporarily set min and range + // options and remove them on chart load (#4317). + baseXAxisOptions = splat(chart.options.xAxis)[0]; + rangeSetting = baseXAxisOptions.range; + baseXAxisOptions.range = range; + minSetting = baseXAxisOptions.min; + baseXAxisOptions.min = rangeMin; + addEvent(chart, 'load', function resetMinAndRange() { + baseXAxisOptions.range = rangeSetting; + baseXAxisOptions.min = minSetting; + }); + } else { + // Existing axis object. Set extremes after render time. + baseAxis.setExtremes( + newMin, + newMax, + pick(redraw, 1), + null, // auto animation + { + trigger: 'rangeSelectorButton', + rangeSelectorButton: rangeOptions + } + ); + } + }, + + /** + * Set the selected option. This method only sets the internal flag, it + * doesn't update the buttons or the actual zoomed range. + */ + setSelected: function (selected) { + this.selected = this.options.selected = selected; + }, + + /** + * The default buttons for pre-selecting time frames + */ + defaultButtons: [{ + type: 'month', + count: 1, + text: '1m' + }, { + type: 'month', + count: 3, + text: '3m' + }, { + type: 'month', + count: 6, + text: '6m' + }, { + type: 'ytd', + text: 'YTD' + }, { + type: 'year', + count: 1, + text: '1y' + }, { + type: 'all', + text: 'All' + }], + + /** + * Initialize the range selector + */ + init: function (chart) { + var rangeSelector = this, + options = chart.options.rangeSelector, + buttonOptions = options.buttons || + [].concat(rangeSelector.defaultButtons), + selectedOption = options.selected, + blurInputs = function () { + var minInput = rangeSelector.minInput, + maxInput = rangeSelector.maxInput; + + // #3274 in some case blur is not defined + if (minInput && minInput.blur) { + fireEvent(minInput, 'blur'); + } + if (maxInput && maxInput.blur) { + fireEvent(maxInput, 'blur'); + } + }; + + rangeSelector.chart = chart; + rangeSelector.options = options; + rangeSelector.buttons = []; + + chart.extraTopMargin = options.height; + rangeSelector.buttonOptions = buttonOptions; + + this.unMouseDown = addEvent(chart.container, 'mousedown', blurInputs); + this.unResize = addEvent(chart, 'resize', blurInputs); + + // Extend the buttonOptions with actual range + each(buttonOptions, rangeSelector.computeButtonRange); + + // zoomed range based on a pre-selected button index + if (selectedOption !== undefined && buttonOptions[selectedOption]) { + this.clickButton(selectedOption, false); + } + + + addEvent(chart, 'load', function () { + // If a data grouping is applied to the current button, release it + // when extremes change + if (chart.xAxis && chart.xAxis[0]) { + addEvent(chart.xAxis[0], 'setExtremes', function (e) { + if ( + this.max - this.min !== chart.fixedRange && + e.trigger !== 'rangeSelectorButton' && + e.trigger !== 'updatedData' && + rangeSelector.forcedDataGrouping + ) { + this.setDataGrouping(false, false); + } + }); + } + }); + }, + + /** + * Dynamically update the range selector buttons after a new range has been + * set + */ + updateButtonStates: function () { + var rangeSelector = this, + chart = this.chart, + baseAxis = chart.xAxis[0], + actualRange = Math.round(baseAxis.max - baseAxis.min), + hasNoData = !baseAxis.hasVisibleSeries, + day = 24 * 36e5, // A single day in milliseconds + unionExtremes = ( + chart.scroller && + chart.scroller.getUnionExtremes() + ) || baseAxis, + dataMin = unionExtremes.dataMin, + dataMax = unionExtremes.dataMax, + ytdExtremes = rangeSelector.getYTDExtremes( + dataMax, + dataMin, + chart.time.useUTC + ), + ytdMin = ytdExtremes.min, + ytdMax = ytdExtremes.max, + selected = rangeSelector.selected, + selectedExists = isNumber(selected), + allButtonsEnabled = rangeSelector.options.allButtonsEnabled, + buttons = rangeSelector.buttons; + + each(rangeSelector.buttonOptions, function (rangeOptions, i) { + var range = rangeOptions._range, + type = rangeOptions.type, + count = rangeOptions.count || 1, + button = buttons[i], + state = 0, + disable, + select, + offsetRange = rangeOptions._offsetMax - rangeOptions._offsetMin, + isSelected = i === selected, + // Disable buttons where the range exceeds what is allowed in + // the current view + isTooGreatRange = range > dataMax - dataMin, + // Disable buttons where the range is smaller than the minimum + // range + isTooSmallRange = range < baseAxis.minRange, + // Do not select the YTD button if not explicitly told so + isYTDButNotSelected = false, + // Disable the All button if we're already showing all + isAllButAlreadyShowingAll = false, + isSameRange = range === actualRange; + // Months and years have a variable range so we check the extremes + if ( + (type === 'month' || type === 'year') && + ( + actualRange + 36e5 >= + { month: 28, year: 365 }[type] * day * count - offsetRange + ) && + ( + actualRange - 36e5 <= + { month: 31, year: 366 }[type] * day * count + offsetRange + ) + ) { + isSameRange = true; + } else if (type === 'ytd') { + isSameRange = (ytdMax - ytdMin + offsetRange) === actualRange; + isYTDButNotSelected = !isSelected; + } else if (type === 'all') { + isSameRange = baseAxis.max - baseAxis.min >= dataMax - dataMin; + isAllButAlreadyShowingAll = ( + !isSelected && + selectedExists && + isSameRange + ); + } + + // The new zoom area happens to match the range for a button - mark + // it selected. This happens when scrolling across an ordinal gap. + // It can be seen in the intraday demos when selecting 1h and scroll + // across the night gap. + disable = ( + !allButtonsEnabled && + ( + isTooGreatRange || + isTooSmallRange || + isAllButAlreadyShowingAll || + hasNoData + ) + ); + select = ( + (isSelected && isSameRange) || + (isSameRange && !selectedExists && !isYTDButNotSelected) + ); + + if (disable) { + state = 3; + } else if (select) { + selectedExists = true; // Only one button can be selected + state = 2; + } + + // If state has changed, update the button + if (button.state !== state) { + button.setState(state); + } + }); + }, + + /** + * Compute and cache the range for an individual button + */ + computeButtonRange: function (rangeOptions) { + var type = rangeOptions.type, + count = rangeOptions.count || 1, + + // these time intervals have a fixed number of milliseconds, as + // opposed to month, ytd and year + fixedTimes = { + millisecond: 1, + second: 1000, + minute: 60 * 1000, + hour: 3600 * 1000, + day: 24 * 3600 * 1000, + week: 7 * 24 * 3600 * 1000 + }; + + // Store the range on the button object + if (fixedTimes[type]) { + rangeOptions._range = fixedTimes[type] * count; + } else if (type === 'month' || type === 'year') { + rangeOptions._range = + { month: 30, year: 365 }[type] * 24 * 36e5 * count; + } + + rangeOptions._offsetMin = pick(rangeOptions.offsetMin, 0); + rangeOptions._offsetMax = pick(rangeOptions.offsetMax, 0); + rangeOptions._range += + rangeOptions._offsetMax - rangeOptions._offsetMin; + }, + + /** + * Set the internal and displayed value of a HTML input for the dates + * @param {String} name + * @param {Number} inputTime + */ + setInputValue: function (name, inputTime) { + var options = this.chart.options.rangeSelector, + time = this.chart.time, + input = this[name + 'Input']; + + if (defined(inputTime)) { + input.previousValue = input.HCTime; + input.HCTime = inputTime; + } + + input.value = time.dateFormat( + options.inputEditDateFormat || '%Y-%m-%d', + input.HCTime + ); + this[name + 'DateBox'].attr({ + text: time.dateFormat( + options.inputDateFormat || '%b %e, %Y', + input.HCTime + ) + }); + }, + + showInput: function (name) { + var inputGroup = this.inputGroup, + dateBox = this[name + 'DateBox']; + + css(this[name + 'Input'], { + left: (inputGroup.translateX + dateBox.x) + 'px', + top: inputGroup.translateY + 'px', + width: (dateBox.width - 2) + 'px', + height: (dateBox.height - 2) + 'px', + border: '2px solid silver' + }); + }, + + hideInput: function (name) { + css(this[name + 'Input'], { + border: 0, + width: '1px', + height: '1px' + }); + this.setInputValue(name); + }, + + /** + * Draw either the 'from' or the 'to' HTML input box of the range selector + * @param {Object} name + */ + drawInput: function (name) { + var rangeSelector = this, + chart = rangeSelector.chart, + chartStyle = chart.renderer.style || {}, + renderer = chart.renderer, + options = chart.options.rangeSelector, + lang = defaultOptions.lang, + div = rangeSelector.div, + isMin = name === 'min', + input, + label, + dateBox, + inputGroup = this.inputGroup; + + function updateExtremes() { + var inputValue = input.value, + value = (options.inputDateParser || Date.parse)(inputValue), + chartAxis = chart.xAxis[0], + dataAxis = chart.scroller && chart.scroller.xAxis ? chart.scroller.xAxis : chartAxis, + dataMin = dataAxis.dataMin, + dataMax = dataAxis.dataMax; + if (value !== input.previousValue) { + input.previousValue = value; + // If the value isn't parsed directly to a value by the browser's Date.parse method, + // like YYYY-MM-DD in IE, try parsing it a different way + if (!isNumber(value)) { + value = inputValue.split('-'); + value = Date.UTC(pInt(value[0]), pInt(value[1]) - 1, pInt(value[2])); + } + + if (isNumber(value)) { + + // Correct for timezone offset (#433) + if (!chart.time.useUTC) { + value = value + new Date().getTimezoneOffset() * 60 * 1000; + } + + // Validate the extremes. If it goes beyound the data min or max, use the + // actual data extreme (#2438). + if (isMin) { + if (value > rangeSelector.maxInput.HCTime) { + value = undefined; + } else if (value < dataMin) { + value = dataMin; + } + } else { + if (value < rangeSelector.minInput.HCTime) { + value = undefined; + } else if (value > dataMax) { + value = dataMax; + } + } + + // Set the extremes + if (value !== undefined) { + chartAxis.setExtremes( + isMin ? value : chartAxis.min, + isMin ? chartAxis.max : value, + undefined, + undefined, + { trigger: 'rangeSelectorInput' } + ); + } + } + } + } + + // Create the text label + this[name + 'Label'] = label = renderer.label(lang[isMin ? 'rangeSelectorFrom' : 'rangeSelectorTo'], this.inputGroup.offset) + .addClass('highcharts-range-label') + .attr({ + padding: 2 + }) + .add(inputGroup); + inputGroup.offset += label.width + 5; + + // Create an SVG label that shows updated date ranges and and records click events that + // bring in the HTML input. + this[name + 'DateBox'] = dateBox = renderer.label('', inputGroup.offset) + .addClass('highcharts-range-input') + .attr({ + padding: 2, + width: options.inputBoxWidth || 90, + height: options.inputBoxHeight || 17, + stroke: options.inputBoxBorderColor || '#cccccc', + 'stroke-width': 1, + 'text-align': 'center' + }) + .on('click', function () { + rangeSelector.showInput(name); // If it is already focused, the onfocus event doesn't fire (#3713) + rangeSelector[name + 'Input'].focus(); + }) + .add(inputGroup); + inputGroup.offset += dateBox.width + (isMin ? 10 : 0); + + + // Create the HTML input element. This is rendered as 1x1 pixel then set to the right size + // when focused. + this[name + 'Input'] = input = createElement('input', { + name: name, + className: 'highcharts-range-selector', + type: 'text' + }, { + top: chart.plotTop + 'px' // prevent jump on focus in Firefox + }, div); + + + // Styles + label.css(merge(chartStyle, options.labelStyle)); + + dateBox.css(merge({ + color: '#333333' + }, chartStyle, options.inputStyle)); + + css(input, extend({ + position: 'absolute', + border: 0, + width: '1px', // Chrome needs a pixel to see it + height: '1px', + padding: 0, + textAlign: 'center', + fontSize: chartStyle.fontSize, + fontFamily: chartStyle.fontFamily, + top: '-9999em' // #4798 + }, options.inputStyle)); + + + // Blow up the input box + input.onfocus = function () { + rangeSelector.showInput(name); + }; + // Hide away the input box + input.onblur = function () { + rangeSelector.hideInput(name); + }; + + // handle changes in the input boxes + input.onchange = updateExtremes; + + input.onkeypress = function (event) { + // IE does not fire onchange on enter + if (event.keyCode === 13) { + updateExtremes(); + } + }; + }, + + /** + * Get the position of the range selector buttons and inputs. This can be overridden from outside for custom positioning. + */ + getPosition: function () { + var chart = this.chart, + options = chart.options.rangeSelector, + top = (options.verticalAlign) === 'top' ? chart.plotTop - chart.axisOffset[0] : 0; // set offset only for varticalAlign top + + return { + buttonTop: top + options.buttonPosition.y, + inputTop: top + options.inputPosition.y - 10 + }; + }, + /** + * Get the extremes of YTD. + * Will choose dataMax if its value is lower than the current timestamp. + * Will choose dataMin if its value is higher than the timestamp for + * the start of current year. + * @param {number} dataMax + * @param {number} dataMin + * @return {object} Returns min and max for the YTD + */ + getYTDExtremes: function (dataMax, dataMin, useUTC) { + var time = this.chart.time, + min, + now = new time.Date(dataMax), + year = now[time.getFullYear](), + startOfYear = useUTC ? time.Date.UTC(year, 0, 1) : +new time.Date(year, 0, 1); // eslint-disable-line new-cap + min = Math.max(dataMin || 0, startOfYear); + now = now.getTime(); + return { + max: Math.min(dataMax || now, now), + min: min + }; + }, + + /** + * Render the range selector including the buttons and the inputs. The first time render + * is called, the elements are created and positioned. On subsequent calls, they are + * moved and updated. + * @param {Number} min X axis minimum + * @param {Number} max X axis maximum + */ + render: function (min, max) { + + var rangeSelector = this, + chart = rangeSelector.chart, + renderer = chart.renderer, + container = chart.container, + chartOptions = chart.options, + navButtonOptions = chartOptions.exporting && chartOptions.exporting.enabled !== false && + chartOptions.navigation && chartOptions.navigation.buttonOptions, + lang = defaultOptions.lang, + div = rangeSelector.div, + options = chartOptions.rangeSelector, + floating = options.floating, + buttons = rangeSelector.buttons, + inputGroup = rangeSelector.inputGroup, + buttonTheme = options.buttonTheme, + buttonPosition = options.buttonPosition, + inputPosition = options.inputPosition, + inputEnabled = options.inputEnabled, + states = buttonTheme && buttonTheme.states, + plotLeft = chart.plotLeft, + buttonLeft, + buttonGroup = rangeSelector.buttonGroup, + group, + groupHeight, + rendered = rangeSelector.rendered, + verticalAlign = rangeSelector.options.verticalAlign, + legend = chart.legend, + legendOptions = legend && legend.options, + buttonPositionY = buttonPosition.y, + inputPositionY = inputPosition.y, + animate = rendered || false, + exportingX = 0, + alignTranslateY, + legendHeight, + minPosition, + translateY = 0, + translateX; + + if (options.enabled === false) { + return; + } + + // create the elements + if (!rendered) { + + rangeSelector.group = group = renderer.g('range-selector-group') + .attr({ + zIndex: 7 + }) + .add(); + + rangeSelector.buttonGroup = buttonGroup = renderer.g('range-selector-buttons').add(group); + + rangeSelector.zoomText = renderer.text(lang.rangeSelectorZoom, pick(plotLeft + buttonPosition.x, plotLeft), 15) + .css(options.labelStyle) + .add(buttonGroup); + + // button start position + buttonLeft = pick(plotLeft + buttonPosition.x, plotLeft) + rangeSelector.zoomText.getBBox().width + 5; + + each(rangeSelector.buttonOptions, function (rangeOptions, i) { + + buttons[i] = renderer.button( + rangeOptions.text, + buttonLeft, + 0, + function () { + + // extract events from button object and call + var buttonEvents = rangeOptions.events && rangeOptions.events.click, + callDefaultEvent; + + if (buttonEvents) { + callDefaultEvent = buttonEvents.call(rangeOptions); + } + + if (callDefaultEvent !== false) { + rangeSelector.clickButton(i); + } + + rangeSelector.isActive = true; + }, + buttonTheme, + states && states.hover, + states && states.select, + states && states.disabled + ) + .attr({ + 'text-align': 'center' + }) + .add(buttonGroup); + + // increase button position for the next button + buttonLeft += buttons[i].width + pick(options.buttonSpacing, 5); + }); + + // first create a wrapper outside the container in order to make + // the inputs work and make export correct + if (inputEnabled !== false) { + rangeSelector.div = div = createElement('div', null, { + position: 'relative', + height: 0, + zIndex: 1 // above container + }); + + container.parentNode.insertBefore(div, container); + + // Create the group to keep the inputs + rangeSelector.inputGroup = inputGroup = renderer.g('input-group') + .add(group); + inputGroup.offset = 0; + + rangeSelector.drawInput('min'); + rangeSelector.drawInput('max'); + } + } + + plotLeft = chart.plotLeft - chart.spacing[3]; + rangeSelector.updateButtonStates(); + + // detect collisiton with exporting + if + ( + navButtonOptions && + this.titleCollision(chart) && + verticalAlign === 'top' && + buttonPosition.align === 'right' && + ( + (buttonPosition.y + buttonGroup.getBBox().height - 12) < + ((navButtonOptions.y || 0) + navButtonOptions.height) + ) + ) { + exportingX = -40; + } + + if (buttonPosition.align === 'left') { + translateX = buttonPosition.x - chart.spacing[3]; + } else if (buttonPosition.align === 'right') { + translateX = buttonPosition.x + exportingX - chart.spacing[1]; + } + + // align button group + buttonGroup.align({ + y: buttonPosition.y, + width: buttonGroup.getBBox().width, + align: buttonPosition.align, + x: translateX + }, true, chart.spacingBox); + + // skip animation + rangeSelector.group.placed = animate; + rangeSelector.buttonGroup.placed = animate; + + if (inputEnabled !== false) { + + var inputGroupX, + inputGroupWidth, + buttonGroupX, + buttonGroupWidth; + + // detect collision with exporting + if + ( + navButtonOptions && + this.titleCollision(chart) && + verticalAlign === 'top' && + inputPosition.align === 'right' && + ( + (inputPosition.y - inputGroup.getBBox().height - 12) < + ((navButtonOptions.y || 0) + navButtonOptions.height + chart.spacing[0]) + ) + ) { + exportingX = -40; + } else { + exportingX = 0; + } + + if (inputPosition.align === 'left') { + translateX = plotLeft; + } else if (inputPosition.align === 'right') { + translateX = -Math.max(chart.axisOffset[1], -exportingX); // yAxis offset + } + + // Update the alignment to the updated spacing box + inputGroup.align({ + y: inputPosition.y, + width: inputGroup.getBBox().width, + align: inputPosition.align, + x: inputPosition.x + translateX - 2 // fix wrong getBBox() value on right align + }, true, chart.spacingBox); + + // detect collision + inputGroupX = inputGroup.alignAttr.translateX + inputGroup.alignOptions.x - + exportingX + inputGroup.getBBox().x + 2; // getBBox for detecing left margin, 2px padding to not overlap input and label + + inputGroupWidth = inputGroup.alignOptions.width; + + buttonGroupX = buttonGroup.alignAttr.translateX + buttonGroup.getBBox().x; + buttonGroupWidth = buttonGroup.getBBox().width + 20; // 20 is minimal spacing between elements + + if ( + (inputPosition.align === buttonPosition.align) || + ( + (buttonGroupX + buttonGroupWidth > inputGroupX) && + (inputGroupX + inputGroupWidth > buttonGroupX) && + (buttonPositionY < (inputPositionY + inputGroup.getBBox().height)) + ) + ) { + + inputGroup.attr({ + translateX: inputGroup.alignAttr.translateX + (chart.axisOffset[1] >= -exportingX ? 0 : -exportingX), + translateY: inputGroup.alignAttr.translateY + buttonGroup.getBBox().height + 10 + }); + + } + + // Set or reset the input values + rangeSelector.setInputValue('min', min); + rangeSelector.setInputValue('max', max); + + // skip animation + rangeSelector.inputGroup.placed = animate; + } + + // vertical align + rangeSelector.group.align({ + verticalAlign: verticalAlign + }, true, chart.spacingBox); + + // set position + groupHeight = rangeSelector.group.getBBox().height + 20; // # 20 padding + alignTranslateY = rangeSelector.group.alignAttr.translateY; + + // calculate bottom position + if (verticalAlign === 'bottom') { + legendHeight = legendOptions && legendOptions.verticalAlign === 'bottom' && legendOptions.enabled && + !legendOptions.floating ? legend.legendHeight + pick(legendOptions.margin, 10) : 0; + + groupHeight = groupHeight + legendHeight - 20; + translateY = alignTranslateY - groupHeight - (floating ? 0 : options.y) - 10; // 10 spacing + + } + + if (verticalAlign === 'top') { + if (floating) { + translateY = 0; + } + + if (chart.titleOffset) { + translateY = chart.titleOffset + chart.options.title.margin; + } + + translateY += ((chart.margin[0] - chart.spacing[0]) || 0); + + } else if (verticalAlign === 'middle') { + if (inputPositionY === buttonPositionY) { + if (inputPositionY < 0) { + translateY = alignTranslateY + minPosition; + } else { + translateY = alignTranslateY; + } + } else if (inputPositionY || buttonPositionY) { + if (inputPositionY < 0 || buttonPositionY < 0) { + translateY -= Math.min(inputPositionY, buttonPositionY); + } else { + translateY = alignTranslateY - groupHeight + minPosition; + } + } + } + + rangeSelector.group.translate( + options.x, + options.y + Math.floor(translateY) + ); + + // translate HTML inputs + if (inputEnabled !== false) { + rangeSelector.minInput.style.marginTop = rangeSelector.group.translateY + 'px'; + rangeSelector.maxInput.style.marginTop = rangeSelector.group.translateY + 'px'; + } + + rangeSelector.rendered = true; + }, + + /** + * Extracts height of range selector + * @return {Number} Returns rangeSelector height + */ + getHeight: function () { + var rangeSelector = this, + options = rangeSelector.options, + rangeSelectorGroup = rangeSelector.group, + inputPosition = options.inputPosition, + buttonPosition = options.buttonPosition, + yPosition = options.y, + buttonPositionY = buttonPosition.y, + inputPositionY = inputPosition.y, + rangeSelectorHeight = 0, + minPosition; + + rangeSelectorHeight = rangeSelectorGroup ? (rangeSelectorGroup.getBBox(true).height) + 13 + yPosition : 0; // 13px to keep back compatibility + + minPosition = Math.min(inputPositionY, buttonPositionY); + + if ( + (inputPositionY < 0 && buttonPositionY < 0) || + (inputPositionY > 0 && buttonPositionY > 0) + ) { + rangeSelectorHeight += Math.abs(minPosition); + } + + return rangeSelectorHeight; + }, + + /** + * Detect collision with title or subtitle + * @param {object} chart + * @return {Boolean} Returns collision status + */ + titleCollision: function (chart) { + return !(chart.options.title.text || chart.options.subtitle.text); + }, + + /** + * Update the range selector with new options + * @param {object} options + */ + update: function (options) { + var chart = this.chart; + + merge(true, chart.options.rangeSelector, options); + this.destroy(); + this.init(chart); + chart.rangeSelector.render(); + }, + + /** + * Destroys allocated elements. + */ + destroy: function () { + var rSelector = this, + minInput = rSelector.minInput, + maxInput = rSelector.maxInput; + + rSelector.unMouseDown(); + rSelector.unResize(); + + // Destroy elements in collections + destroyObjectProperties(rSelector.buttons); + + // Clear input element events + if (minInput) { + minInput.onfocus = minInput.onblur = minInput.onchange = null; + } + if (maxInput) { + maxInput.onfocus = maxInput.onblur = maxInput.onchange = null; + } + + // Destroy HTML and SVG elements + H.objectEach(rSelector, function (val, key) { + if (val && key !== 'chart') { + if (val.destroy) { // SVGElement + val.destroy(); + } else if (val.nodeType) { // HTML element + discardElement(this[key]); + } + } + if (val !== RangeSelector.prototype[key]) { + rSelector[key] = null; + } + }, this); + } +}; + +/** + * Add logic to normalize the zoomed range in order to preserve the pressed state of range selector buttons + */ +Axis.prototype.toFixedRange = function (pxMin, pxMax, fixedMin, fixedMax) { + var fixedRange = this.chart && this.chart.fixedRange, + newMin = pick(fixedMin, this.translate(pxMin, true, !this.horiz)), + newMax = pick(fixedMax, this.translate(pxMax, true, !this.horiz)), + changeRatio = fixedRange && (newMax - newMin) / fixedRange; + + // If the difference between the fixed range and the actual requested range is + // too great, the user is dragging across an ordinal gap, and we need to release + // the range selector button. + if (changeRatio > 0.7 && changeRatio < 1.3) { + if (fixedMax) { + newMin = newMax - fixedRange; + } else { + newMax = newMin + fixedRange; + } + } + if (!isNumber(newMin) || !isNumber(newMax)) { // #1195, #7411 + newMin = newMax = undefined; + } + + return { + min: newMin, + max: newMax + }; +}; + +/** + * Get the axis min value based on the range option and the current max. For + * stock charts this is extended via the {@link RangeSelector} so that if the + * selected range is a multiple of months or years, it is compensated for + * various month lengths. + * + * @return {number} The new minimum value. + */ +Axis.prototype.minFromRange = function () { + var rangeOptions = this.range, + type = rangeOptions.type, + timeName = { month: 'Month', year: 'FullYear' }[type], + min, + max = this.max, + dataMin, + range, + // Get the true range from a start date + getTrueRange = function (base, count) { + var date = new Date(base), + basePeriod = date['get' + timeName](); + + date['set' + timeName](basePeriod + count); + + if (basePeriod === date['get' + timeName]()) { + date.setDate(0); // #6537 + } + + return date.getTime() - base; + }; + + if (isNumber(rangeOptions)) { + min = max - rangeOptions; + range = rangeOptions; + } else { + min = max + getTrueRange(max, -rangeOptions.count); + + // Let the fixedRange reflect initial settings (#5930) + if (this.chart) { + this.chart.fixedRange = max - min; + } + } + + dataMin = pick(this.dataMin, Number.MIN_VALUE); + if (!isNumber(min)) { + min = dataMin; + } + if (min <= dataMin) { + min = dataMin; + if (range === undefined) { // #4501 + range = getTrueRange(min, rangeOptions.count); + } + this.newMax = Math.min(min + range, this.dataMax); + } + if (!isNumber(max)) { + min = undefined; + } + return min; + +}; + +// Initialize rangeselector for stock charts +wrap(Chart.prototype, 'init', function (proceed, options, callback) { + + addEvent(this, 'init', function () { + if (this.options.rangeSelector.enabled) { + this.rangeSelector = new RangeSelector(this); + } + }); + + proceed.call(this, options, callback); + +}); + +wrap(Chart.prototype, 'render', function (proceed, options, callback) { + + var chart = this, + axes = chart.axes, + rangeSelector = chart.rangeSelector, + verticalAlign; + + if (rangeSelector) { + + each(axes, function (axis) { + axis.updateNames(); + axis.setScale(); + }); + + chart.getAxisMargins(); + + rangeSelector.render(); + verticalAlign = rangeSelector.options.verticalAlign; + + if (!rangeSelector.options.floating) { + if (verticalAlign === 'bottom') { + this.extraBottomMargin = true; + } else if (verticalAlign !== 'middle') { + this.extraTopMargin = true; + } + } + } + + proceed.call(this, options, callback); + +}); + +wrap(Chart.prototype, 'update', function (proceed, options, redraw, oneToOne) { + + var chart = this, + rangeSelector = chart.rangeSelector, + verticalAlign; + + this.extraBottomMargin = false; + this.extraTopMargin = false; + + if (rangeSelector) { + + rangeSelector.render(); + + verticalAlign = (options.rangeSelector && options.rangeSelector.verticalAlign) || + (rangeSelector.options && rangeSelector.options.verticalAlign); + + if (!rangeSelector.options.floating) { + if (verticalAlign === 'bottom') { + this.extraBottomMargin = true; + } else if (verticalAlign !== 'middle') { + this.extraTopMargin = true; + } + } + } + + proceed.call(this, H.merge(true, options, { + chart: { + marginBottom: pick(options.chart && options.chart.marginBottom, chart.margin.bottom), + spacingBottom: pick(options.chart && options.chart.spacingBottom, chart.spacing.bottom) + } + }), redraw, oneToOne); + +}); + +wrap(Chart.prototype, 'redraw', function (proceed, options, callback) { + var chart = this, + rangeSelector = chart.rangeSelector, + verticalAlign; + + if (rangeSelector && !rangeSelector.options.floating) { + + rangeSelector.render(); + verticalAlign = rangeSelector.options.verticalAlign; + + if (verticalAlign === 'bottom') { + this.extraBottomMargin = true; + } else if (verticalAlign !== 'middle') { + this.extraTopMargin = true; + } + } + + proceed.call(this, options, callback); +}); + +Chart.prototype.adjustPlotArea = function () { + var chart = this, + rangeSelector = chart.rangeSelector, + rangeSelectorHeight; + + if (this.rangeSelector) { + + rangeSelectorHeight = rangeSelector.getHeight(); + + if (this.extraTopMargin) { + this.plotTop += rangeSelectorHeight; + } + + if (this.extraBottomMargin) { + this.marginBottom += rangeSelectorHeight; + } + } +}; + +Chart.prototype.callbacks.push(function (chart) { + var extremes, + rangeSelector = chart.rangeSelector, + unbindRender, + unbindSetExtremes; + + function renderRangeSelector() { + extremes = chart.xAxis[0].getExtremes(); + if (isNumber(extremes.min)) { + rangeSelector.render(extremes.min, extremes.max); + } + } + + if (rangeSelector) { + // redraw the scroller on setExtremes + unbindSetExtremes = addEvent( + chart.xAxis[0], + 'afterSetExtremes', + function (e) { + rangeSelector.render(e.min, e.max); + } + ); + + // redraw the scroller chart resize + unbindRender = addEvent(chart, 'redraw', renderRangeSelector); + + // do it now + renderRangeSelector(); + } + + // Remove resize/afterSetExtremes at chart destroy + addEvent(chart, 'destroy', function destroyEvents() { + if (rangeSelector) { + unbindRender(); + unbindSetExtremes(); + } + }); +}); + + +H.RangeSelector = RangeSelector; + +/* **************************************************************************** + * End Range Selector code * + *****************************************************************************/ + +}(Highcharts)); +(function (H) { +/** + * (c) 2010-2017 Torstein Honsi + * + * License: www.highcharts.com/license + */ +var arrayMax = H.arrayMax, + arrayMin = H.arrayMin, + Axis = H.Axis, + Chart = H.Chart, + defined = H.defined, + each = H.each, + extend = H.extend, + format = H.format, + grep = H.grep, + inArray = H.inArray, + isNumber = H.isNumber, + isString = H.isString, + map = H.map, + merge = H.merge, + pick = H.pick, + Point = H.Point, + Renderer = H.Renderer, + Series = H.Series, + splat = H.splat, + SVGRenderer = H.SVGRenderer, + VMLRenderer = H.VMLRenderer, + wrap = H.wrap, + + + seriesProto = Series.prototype, + seriesInit = seriesProto.init, + seriesProcessData = seriesProto.processData, + pointTooltipFormatter = Point.prototype.tooltipFormatter; + + +/** + * Compare the values of the series against the first non-null, non- + * zero value in the visible range. The y axis will show percentage + * or absolute change depending on whether `compare` is set to `"percent"` + * or `"value"`. When this is applied to multiple series, it allows + * comparing the development of the series against each other. + * + * @type {String} + * @see [compareBase](#plotOptions.series.compareBase), [Axis.setCompare()](#Axis. + * setCompare()) + * @sample {highstock} stock/plotoptions/series-compare-percent/ Percent + * @sample {highstock} stock/plotoptions/series-compare-value/ Value + * @default undefined + * @since 1.0.1 + * @product highstock + * @apioption plotOptions.series.compare + */ + +/** + * Defines if comparisson should start from the first point within the visible + * range or should start from the first point before the range. + * In other words, this flag determines if first point within the visible range + * will have 0% (`compareStart=true`) or should have been already calculated + * according to the previous point (`compareStart=false`). + * + * @type {Boolean} + * @sample {highstock} stock/plotoptions/series-comparestart/ Calculate compare within visible range + * @default false + * @since 6.0.0 + * @product highstock + * @apioption plotOptions.series.compareStart + */ + +/** + * When [compare](#plotOptions.series.compare) is `percent`, this option + * dictates whether to use 0 or 100 as the base of comparison. + * + * @validvalue [0, 100] + * @type {Number} + * @sample {highstock} / Compare base is 100 + * @default 0 + * @since 5.0.6 + * @product highstock + * @apioption plotOptions.series.compareBase + */ + +/** + * Factory function for creating new stock charts. Creates a new {@link Chart| + * Chart} object with different default options than the basic Chart. + * + * @function #stockChart + * @memberOf Highcharts + * + * @param {String|HTMLDOMElement} renderTo + * The DOM element to render to, or its id. + * @param {Options} options + * The chart options structure as described in the {@link + * https://api.highcharts.com/highstock|options reference}. + * @param {Function} callback + * A function to execute when the chart object is finished loading and + * rendering. In most cases the chart is built in one thread, but in + * Internet Explorer version 8 or less the chart is sometimes initialized + * before the document is ready, and in these cases the chart object + * will not be finished synchronously. As a consequence, code that + * relies on the newly built Chart object should always run in the + * callback. Defining a {@link https://api.highcharts.com/highstock/chart.events.load| + * chart.event.load} handler is equivalent. + * + * @return {Chart} + * The chart object. + * + * @example + * var chart = Highcharts.stockChart('container', { + * series: [{ + * data: [1, 2, 3, 4, 5, 6, 7, 8, 9], + * pointInterval: 24 * 60 * 60 * 1000 + * }] + * }); + */ +H.StockChart = H.stockChart = function (a, b, c) { + var hasRenderToArg = isString(a) || a.nodeName, + options = arguments[hasRenderToArg ? 1 : 0], + seriesOptions = options.series, // to increase performance, don't merge the data + defaultOptions = H.getOptions(), + opposite, + + // Always disable startOnTick:true on the main axis when the navigator + // is enabled (#1090) + navigatorEnabled = pick( + options.navigator && options.navigator.enabled, + defaultOptions.navigator.enabled, + true + ), + disableStartOnTick = navigatorEnabled ? { + startOnTick: false, + endOnTick: false + } : null, + + lineOptions = { + + marker: { + enabled: false, + radius: 2 + } + // gapSize: 0 + }, + columnOptions = { + shadow: false, + borderWidth: 0 + }; + + // apply X axis options to both single and multi y axes + options.xAxis = map(splat(options.xAxis || {}), function (xAxisOptions) { + return merge( + { // defaults + minPadding: 0, + maxPadding: 0, + overscroll: 0, + ordinal: true, + title: { + text: null + }, + labels: { + overflow: 'justify' + }, + showLastLabel: true + }, + defaultOptions.xAxis, // #3802 + xAxisOptions, // user options + { // forced options + type: 'datetime', + categories: null + }, + disableStartOnTick + ); + }); + + // apply Y axis options to both single and multi y axes + options.yAxis = map(splat(options.yAxis || {}), function (yAxisOptions) { + opposite = pick(yAxisOptions.opposite, true); + return merge({ // defaults + labels: { + y: -2 + }, + opposite: opposite, + + /** + * @default {highcharts} true + * @default {highstock} false + * @apioption yAxis.showLastLabel + */ + showLastLabel: !!( + // #6104, show last label by default for category axes + yAxisOptions.categories || + yAxisOptions.type === 'category' + ), + + title: { + text: null + } + }, + defaultOptions.yAxis, // #3802 + yAxisOptions // user options + ); + }); + + options.series = null; + + options = merge( + { + chart: { + panning: true, + pinchType: 'x' + }, + navigator: { + enabled: navigatorEnabled + }, + scrollbar: { + // #4988 - check if setOptions was called + enabled: pick(defaultOptions.scrollbar.enabled, true) + }, + rangeSelector: { + // #4988 - check if setOptions was called + enabled: pick(defaultOptions.rangeSelector.enabled, true) + }, + title: { + text: null + }, + tooltip: { + split: pick(defaultOptions.tooltip.split, true), + crosshairs: true + }, + legend: { + enabled: false + }, + + plotOptions: { + line: lineOptions, + spline: lineOptions, + area: lineOptions, + areaspline: lineOptions, + arearange: lineOptions, + areasplinerange: lineOptions, + column: columnOptions, + columnrange: columnOptions, + candlestick: columnOptions, + ohlc: columnOptions + } + + }, + + options, // user's options + + { // forced options + isStock: true // internal flag + } + ); + + options.series = seriesOptions; + + return hasRenderToArg ? + new Chart(a, options, c) : + new Chart(options, b); +}; + +// Override the automatic label alignment so that the first Y axis' labels +// are drawn on top of the grid line, and subsequent axes are drawn outside +wrap(Axis.prototype, 'autoLabelAlign', function (proceed) { + var chart = this.chart, + options = this.options, + panes = chart._labelPanes = chart._labelPanes || {}, + key, + labelOptions = this.options.labels; + if (this.chart.options.isStock && this.coll === 'yAxis') { + key = options.top + ',' + options.height; + if (!panes[key] && labelOptions.enabled) { // do it only for the first Y axis of each pane + if (labelOptions.x === 15) { // default + labelOptions.x = 0; + } + if (labelOptions.align === undefined) { + labelOptions.align = 'right'; + } + panes[key] = this; + return 'right'; + } + } + return proceed.apply(this, [].slice.call(arguments, 1)); +}); + +// Clear axis from label panes (#6071) +wrap(Axis.prototype, 'destroy', function (proceed) { + var chart = this.chart, + key = this.options && (this.options.top + ',' + this.options.height); + + if (key && chart._labelPanes && chart._labelPanes[key] === this) { + delete chart._labelPanes[key]; + } + + return proceed.apply(this, Array.prototype.slice.call(arguments, 1)); +}); + +// Override getPlotLinePath to allow for multipane charts +wrap(Axis.prototype, 'getPlotLinePath', function (proceed, value, lineWidth, old, force, translatedValue) { + var axis = this, + series = (this.isLinked && !this.series ? this.linkedParent.series : this.series), + chart = axis.chart, + renderer = chart.renderer, + axisLeft = axis.left, + axisTop = axis.top, + x1, + y1, + x2, + y2, + result = [], + axes = [], // #3416 need a default array + axes2, + uniqueAxes, + transVal; + + /** + * Return the other axis based on either the axis option or on related series. + */ + function getAxis(coll) { + var otherColl = coll === 'xAxis' ? 'yAxis' : 'xAxis', + opt = axis.options[otherColl]; + + // Other axis indexed by number + if (isNumber(opt)) { + return [chart[otherColl][opt]]; + } + + // Other axis indexed by id (like navigator) + if (isString(opt)) { + return [chart.get(opt)]; + } + + // Auto detect based on existing series + return map(series, function (s) { + return s[otherColl]; + }); + } + + // Ignore in case of colorAxis or zAxis. #3360, #3524, #6720 + if (axis.coll !== 'xAxis' && axis.coll !== 'yAxis') { + return proceed.apply(this, [].slice.call(arguments, 1)); + } + + // Get the related axes based on series + axes = getAxis(axis.coll); + + // Get the related axes based options.*Axis setting #2810 + axes2 = (axis.isXAxis ? chart.yAxis : chart.xAxis); + each(axes2, function (A) { + if (defined(A.options.id) ? A.options.id.indexOf('navigator') === -1 : true) { + var a = (A.isXAxis ? 'yAxis' : 'xAxis'), + rax = (defined(A.options[a]) ? chart[a][A.options[a]] : chart[a][0]); + + if (axis === rax) { + axes.push(A); + } + } + }); + + + // Remove duplicates in the axes array. If there are no axes in the axes array, + // we are adding an axis without data, so we need to populate this with grid + // lines (#2796). + uniqueAxes = axes.length ? [] : [axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; // #3742 + each(axes, function (axis2) { + if ( + inArray(axis2, uniqueAxes) === -1 && + // Do not draw on axis which overlap completely. #5424 + !H.find(uniqueAxes, function (unique) { + return unique.pos === axis2.pos && unique.len && axis2.len; + }) + ) { + uniqueAxes.push(axis2); + } + }); + + transVal = pick(translatedValue, axis.translate(value, null, null, old)); + if (isNumber(transVal)) { + if (axis.horiz) { + each(uniqueAxes, function (axis2) { + var skip; + + y1 = axis2.pos; + y2 = y1 + axis2.len; + x1 = x2 = Math.round(transVal + axis.transB); + + if (x1 < axisLeft || x1 > axisLeft + axis.width) { // outside plot area + if (force) { + x1 = x2 = Math.min(Math.max(axisLeft, x1), axisLeft + axis.width); + } else { + skip = true; + } + } + if (!skip) { + result.push('M', x1, y1, 'L', x2, y2); + } + }); + } else { + each(uniqueAxes, function (axis2) { + var skip; + + x1 = axis2.pos; + x2 = x1 + axis2.len; + y1 = y2 = Math.round(axisTop + axis.height - transVal); + + if (y1 < axisTop || y1 > axisTop + axis.height) { // outside plot area + if (force) { + y1 = y2 = Math.min(Math.max(axisTop, y1), axis.top + axis.height); + } else { + skip = true; + } + } + if (!skip) { + result.push('M', x1, y1, 'L', x2, y2); + } + }); + } + } + return result.length > 0 ? + renderer.crispPolyLine(result, lineWidth || 1) : + null; // #3557 getPlotLinePath in regular Highcharts also returns null +}); + +// Function to crisp a line with multiple segments +SVGRenderer.prototype.crispPolyLine = function (points, width) { + // points format: ['M', 0, 0, 'L', 100, 0] + // normalize to a crisp line + var i; + for (i = 0; i < points.length; i = i + 6) { + if (points[i + 1] === points[i + 4]) { + // Substract due to #1129. Now bottom and left axis gridlines behave the same. + points[i + 1] = points[i + 4] = Math.round(points[i + 1]) - (width % 2 / 2); + } + if (points[i + 2] === points[i + 5]) { + points[i + 2] = points[i + 5] = Math.round(points[i + 2]) + (width % 2 / 2); + } + } + return points; +}; + +if (Renderer === VMLRenderer) { + VMLRenderer.prototype.crispPolyLine = SVGRenderer.prototype.crispPolyLine; +} + + +// Wrapper to hide the label +wrap(Axis.prototype, 'hideCrosshair', function (proceed, i) { + + proceed.call(this, i); + + if (this.crossLabel) { + this.crossLabel = this.crossLabel.hide(); + } +}); + +// Wrapper to draw the label +wrap(Axis.prototype, 'drawCrosshair', function (proceed, e, point) { + + // Draw the crosshair + proceed.call(this, e, point); + + // Check if the label has to be drawn + if ( + !defined(this.crosshair.label) || + !this.crosshair.label.enabled || + !this.cross + ) { + return; + } + + var chart = this.chart, + options = this.options.crosshair.label, // the label's options + horiz = this.horiz, // axis orientation + opposite = this.opposite, // axis position + left = this.left, // left position + top = this.top, // top position + crossLabel = this.crossLabel, // reference to the svgElement + posx, + posy, + crossBox, + formatOption = options.format, + formatFormat = '', + limit, + align, + tickInside = this.options.tickPosition === 'inside', + snap = this.crosshair.snap !== false, + value, + offset = 0; + + // Use last available event (#5287) + if (!e) { + e = this.cross && this.cross.e; + } + + align = (horiz ? 'center' : opposite ? + (this.labelAlign === 'right' ? 'right' : 'left') : + (this.labelAlign === 'left' ? 'left' : 'center')); + + // If the label does not exist yet, create it. + if (!crossLabel) { + crossLabel = this.crossLabel = chart.renderer.label( + null, + null, + null, + options.shape || 'callout' + ) + .addClass('highcharts-crosshair-label' + + (this.series[0] && ' highcharts-color-' + this.series[0].colorIndex)) + .attr({ + align: options.align || align, + padding: pick(options.padding, 8), + r: pick(options.borderRadius, 3), + zIndex: 2 + }) + .add(this.labelGroup); + + + // Presentational + crossLabel + .attr({ + fill: options.backgroundColor || + (this.series[0] && this.series[0].color) || + '#666666', + stroke: options.borderColor || '', + 'stroke-width': options.borderWidth || 0 + }) + .css(extend({ + color: '#ffffff', + fontWeight: 'normal', + fontSize: '11px', + textAlign: 'center' + }, options.style)); + + } + + if (horiz) { + posx = snap ? point.plotX + left : e.chartX; + posy = top + (opposite ? 0 : this.height); + } else { + posx = opposite ? this.width + left : 0; + posy = snap ? point.plotY + top : e.chartY; + } + + if (!formatOption && !options.formatter) { + if (this.isDatetimeAxis) { + formatFormat = '%b %d, %Y'; + } + formatOption = '{value' + (formatFormat ? ':' + formatFormat : '') + '}'; + } + + // Show the label + value = snap ? + point[this.isXAxis ? 'x' : 'y'] : + this.toValue(horiz ? e.chartX : e.chartY); + + crossLabel.attr({ + text: formatOption ? + format(formatOption, { value: value }, chart.time) : + options.formatter.call(this, value), + x: posx, + y: posy, + // Crosshair should be rendered within Axis range (#7219) + visibility: value < this.min || value > this.max ? 'hidden' : 'visible' + }); + + crossBox = crossLabel.getBBox(); + + // now it is placed we can correct its position + if (horiz) { + if ((tickInside && !opposite) || (!tickInside && opposite)) { + posy = crossLabel.y - crossBox.height; + } + } else { + posy = crossLabel.y - (crossBox.height / 2); + } + + // check the edges + if (horiz) { + limit = { + left: left - crossBox.x, + right: left + this.width - crossBox.x + }; + } else { + limit = { + left: this.labelAlign === 'left' ? left : 0, + right: this.labelAlign === 'right' ? left + this.width : chart.chartWidth + }; + } + + // left edge + if (crossLabel.translateX < limit.left) { + offset = limit.left - crossLabel.translateX; + } + // right edge + if (crossLabel.translateX + crossBox.width >= limit.right) { + offset = -(crossLabel.translateX + crossBox.width - limit.right); + } + + // show the crosslabel + crossLabel.attr({ + x: posx + offset, + y: posy, + // First set x and y, then anchorX and anchorY, when box is actually calculated, #5702 + anchorX: horiz ? posx : (this.opposite ? 0 : chart.chartWidth), + anchorY: horiz ? (this.opposite ? chart.chartHeight : 0) : posy + crossBox.height / 2 + }); +}); + +/* **************************************************************************** + * Start value compare logic * + *****************************************************************************/ + +/** + * Extend series.init by adding a method to modify the y value used for plotting + * on the y axis. This method is called both from the axis when finding dataMin + * and dataMax, and from the series.translate method. + */ +seriesProto.init = function () { + + // Call base method + seriesInit.apply(this, arguments); + + // Set comparison mode + this.setCompare(this.options.compare); +}; + +/** + * Highstock only. Set the {@link + * http://api.highcharts.com/highstock/plotOptions.series.compare| + * compare} mode of the series after render time. In most cases it is more + * useful running {@link Axis#setCompare} on the X axis to update all its + * series. + * + * @function setCompare + * @memberOf Series.prototype + * + * @param {String} compare + * Can be one of `null`, `"percent"` or `"value"`. + */ +seriesProto.setCompare = function (compare) { + + // Set or unset the modifyValue method + this.modifyValue = (compare === 'value' || compare === 'percent') ? function (value, point) { + var compareValue = this.compareValue; + + if (value !== undefined && compareValue !== undefined) { // #2601, #5814 + + // Get the modified value + if (compare === 'value') { + value -= compareValue; + + // Compare percent + } else { + value = 100 * (value / compareValue) - + (this.options.compareBase === 100 ? 0 : 100); + } + + // record for tooltip etc. + if (point) { + point.change = value; + } + + return value; + } + } : null; + + // Survive to export, #5485 + this.userOptions.compare = compare; + + // Mark dirty + if (this.chart.hasRendered) { + this.isDirty = true; + } + +}; + +/** + * Extend series.processData by finding the first y value in the plot area, + * used for comparing the following values + */ +seriesProto.processData = function () { + var series = this, + i, + keyIndex = -1, + processedXData, + processedYData, + compareStart = series.options.compareStart === true ? 0 : 1, + length, + compareValue; + + // call base method + seriesProcessData.apply(this, arguments); + + if (series.xAxis && series.processedYData) { // not pies + + // local variables + processedXData = series.processedXData; + processedYData = series.processedYData; + length = processedYData.length; + + // For series with more than one value (range, OHLC etc), compare against + // close or the pointValKey (#4922, #3112) + if (series.pointArrayMap) { + // Use close if present (#3112) + keyIndex = inArray('close', series.pointArrayMap); + if (keyIndex === -1) { + keyIndex = inArray(series.pointValKey || 'y', series.pointArrayMap); + } + } + + // find the first value for comparison + for (i = 0; i < length - compareStart; i++) { + compareValue = processedYData[i] && keyIndex > -1 ? + processedYData[i][keyIndex] : + processedYData[i]; + if ( + isNumber(compareValue) && + processedXData[i + compareStart] >= series.xAxis.min && + compareValue !== 0 + ) { + series.compareValue = compareValue; + break; + } + } + } +}; + +/** + * Modify series extremes + */ +wrap(seriesProto, 'getExtremes', function (proceed) { + var extremes; + + proceed.apply(this, [].slice.call(arguments, 1)); + + if (this.modifyValue) { + extremes = [this.modifyValue(this.dataMin), this.modifyValue(this.dataMax)]; + this.dataMin = arrayMin(extremes); + this.dataMax = arrayMax(extremes); + } +}); + +/** + * Highstock only. Set the compare mode on all series belonging to an Y axis + * after render time. + * + * @param {String} compare + * The compare mode. Can be one of `null`, `"value"` or `"percent"`. + * @param {Boolean} [redraw=true] + * Whether to redraw the chart or to wait for a later call to {@link + * Chart#redraw}, + * + * @function setCompare + * @memberOf Axis.prototype + * + * @see {@link https://api.highcharts.com/highstock/series.plotOptions.compare| + * series.plotOptions.compare} + * + * @sample stock/members/axis-setcompare/ + * Set compoare + */ +Axis.prototype.setCompare = function (compare, redraw) { + if (!this.isXAxis) { + each(this.series, function (series) { + series.setCompare(compare); + }); + if (pick(redraw, true)) { + this.chart.redraw(); + } + } +}; + +/** + * Extend the tooltip formatter by adding support for the point.change variable + * as well as the changeDecimals option + */ +Point.prototype.tooltipFormatter = function (pointFormat) { + var point = this; + + pointFormat = pointFormat.replace( + '{point.change}', + (point.change > 0 ? '+' : '') + + H.numberFormat(point.change, pick(point.series.tooltipOptions.changeDecimals, 2)) + ); + + return pointTooltipFormatter.apply(this, [pointFormat]); +}; + +/* **************************************************************************** + * End value compare logic * + *****************************************************************************/ + + +/** + * Extend the Series prototype to create a separate series clip box. This is + * related to using multiple panes, and a future pane logic should incorporate + * this feature (#2754). + */ +wrap(Series.prototype, 'render', function (proceed) { + // Only do this on not 3d (#2939, #5904) nor polar (#6057) charts, and only + // if the series type handles clipping in the animate method (#2975). + if ( + !(this.chart.is3d && this.chart.is3d()) && + !this.chart.polar && + this.xAxis && + !this.xAxis.isRadial // Gauge, #6192 + ) { + + // First render, initial clip box + if (!this.clipBox && this.animate) { + this.clipBox = merge(this.chart.clipBox); + this.clipBox.width = this.xAxis.len; + this.clipBox.height = this.yAxis.len; + + // On redrawing, resizing etc, update the clip rectangle + } else if (this.chart[this.sharedClipKey]) { + this.chart[this.sharedClipKey].attr({ + width: this.xAxis.len, + height: this.yAxis.len + }); + // #3111 + } else if (this.clipBox) { + this.clipBox.width = this.xAxis.len; + this.clipBox.height = this.yAxis.len; + } + } + proceed.call(this); +}); + +wrap(Chart.prototype, 'getSelectedPoints', function (proceed) { + var points = proceed.call(this); + + each(this.series, function (serie) { + // series.points - for grouped points (#6445) + if (serie.hasGroupedData) { + points = points.concat(grep(serie.points || [], function (point) { + return point.selected; + })); + } + }); + return points; +}); + +wrap(Chart.prototype, 'update', function (proceed, options) { + // Use case: enabling scrollbar from a disabled state. + // Scrollbar needs to be initialized from a controller, Navigator in this + // case (#6615) + if ('scrollbar' in options && this.navigator) { + merge(true, this.options.scrollbar, options.scrollbar); + this.navigator.update({}, false); + delete options.scrollbar; + } + + return proceed.apply(this, Array.prototype.slice.call(arguments, 1)); +}); + +}(Highcharts)); +(function () { + + +}()); +return Highcharts +})); \ No newline at end of file