123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401 |
- /**
- * (c) 2010-2019 Torstein Honsi
- *
- * License: www.highcharts.com/license
- */
- 'use strict';
- import Highcharts from './Globals.js';
- import './Utilities.js';
- var H = Highcharts,
- addEvent = H.addEvent,
- css = H.css,
- discardElement = H.discardElement,
- defined = H.defined,
- fireEvent = H.fireEvent,
- 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 is available from the `chart.legend`
- * property. Each chart has only one legend.
- *
- * @class
- * @name Highcharts.Legend
- *
- * @param {Highcharts.Chart} chart
- * The chart instance.
- *
- * @param {Highcharts.LegendOptions} options
- * Legend options.
- */
- Highcharts.Legend = function (chart, options) {
- this.init(chart, options);
- };
- Highcharts.Legend.prototype = {
- /**
- * Initialize the legend.
- *
- * @private
- * @function Highcharts.Legend#init
- *
- * @param {Highcharts.Chart} chart
- * The chart instance.
- *
- * @param {Highcharts.LegendOptions} options
- * Legend options.
- */
- init: function (chart, options) {
- /**
- * Chart of this legend.
- *
- * @readonly
- * @name Highcharts.Legend#chart
- * @type {Highcharts.Chart}
- */
- this.chart = chart;
- this.setOptions(options);
- if (options.enabled) {
- // Render it
- this.render();
- // move checkboxes
- addEvent(this.chart, 'endResize', function () {
- this.legend.positionCheckboxes();
- });
- if (this.proximate) {
- this.unchartrender = addEvent(
- this.chart,
- 'render',
- function () {
- this.legend.proximatePositions();
- this.legend.positionItems();
- }
- );
- } else if (this.unchartrender) {
- this.unchartrender();
- }
- }
- },
- /**
- * @private
- * @function Highcharts.Legend#setOptions
- *
- * @param {Highcharts.LegendOptions} options
- */
- setOptions: function (options) {
- var padding = pick(options.padding, 8);
- /**
- * Legend options.
- *
- * @readonly
- * @name Highcharts.Legend#options
- * @type {Highcharts.LegendOptions}
- */
- this.options = options;
- if (!this.chart.styledMode) {
- 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.symbolWidth = pick(options.symbolWidth, 16);
- this.pages = [];
- this.proximate = options.layout === 'proximate' && !this.chart.inverted;
- },
- /**
- * Update the legend with new options. Equivalent to running `chart.update`
- * with a legend configuration option.
- *
- * @sample highcharts/legend/legend-update/
- * Legend update
- *
- * @function Highcharts.Legend#update
- *
- * @param {Highcharts.LegendOptions} options
- * Legend options.
- *
- * @param {boolean} [redraw=true]
- * Whether to redraw the chart after the axis 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.
- * Whether to redraw the chart.
- *
- * @fires Highcharts.Legends#event:afterUpdate
- */
- 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();
- }
- fireEvent(this, 'afterUpdate');
- },
- /**
- * Set the colors for the legend item.
- *
- * @private
- * @function Highcharts.Legend#colorizeItem
- *
- * @param {Highcharts.Point|Highcharts.Series} item
- * A Series or Point instance
- *
- * @param {boolean} [visible=false]
- * Dimmed or colored
- *
- * @todo
- * Make events official: Fires the event `afterColorizeItem`.
- */
- colorizeItem: function (item, visible) {
- item.legendGroup[visible ? 'removeClass' : 'addClass'](
- 'highcharts-legend-item-hidden'
- );
- if (!this.chart.styledMode) {
- 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) {
- // #6769
- symbolAttr.stroke = symbolAttr.fill = hiddenColor;
- }
- }
- legendSymbol.attr(symbolAttr);
- }
- }
- fireEvent(this, 'afterColorizeItem', { item: item, visible: visible });
- },
- /**
- * @private
- * @function Highcharts.Legend#positionItems
- */
- positionItems: function () {
- // Now that the legend width and height are established, put the items
- // in the final position
- this.allItems.forEach(this.positionItem, this);
- if (!this.chart.isResizing) {
- this.positionCheckboxes();
- }
- },
- /**
- * Position the legend item.
- *
- * @private
- * @function Highcharts.Legend#positionItem
- *
- * @param {Highcharts.Point|Highcharts.Series} 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[defined(legendGroup.translateY) ? 'animate' : 'attr']({
- translateX: ltr ?
- itemX :
- legend.legendWidth - itemX - 2 * symbolPadding - 4,
- translateY: itemY
- });
- }
- if (checkbox) {
- checkbox.x = itemX;
- checkbox.y = itemY;
- }
- },
- /**
- * Destroy a single legend item, used internally on removing series items.
- *
- * @private
- * @function Highcharts.Legend#destroyItem
- *
- * @param {Highcharts.Point|Highcharts.Series} item
- * The item to remove
- */
- destroyItem: function (item) {
- var checkbox = item.checkbox;
- // destroy SVG elements
- ['legendItem', 'legendLine', 'legendSymbol', 'legendGroup'].forEach(
- 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.
- *
- * @private
- * @function Highcharts.Legend#destroy
- */
- destroy: function () {
- function destroyItems(key) {
- if (this[key]) {
- this[key] = this[key].destroy();
- }
- }
- // Destroy items
- this.getAllItems().forEach(function (item) {
- ['legendItem', 'legendGroup'].forEach(destroyItems, item);
- });
- // Destroy legend elements
- [
- 'clipRect',
- 'up',
- 'down',
- 'pager',
- 'nav',
- 'box',
- 'title',
- 'group'
- ].forEach(destroyItems, this);
- this.display = null; // Reset in .render on update.
- },
- /**
- * Position the checkboxes after the width is determined.
- *
- * @private
- * @function Highcharts.Legend#positionCheckboxes
- */
- positionCheckboxes: function () {
- var alignAttr = this.group && this.group.alignAttr,
- translateY,
- clipHeight = this.clipHeight || this.legendHeight,
- titleHeight = this.titleHeight;
- if (alignAttr) {
- translateY = alignAttr.translateY;
- this.allItems.forEach(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: this.proximate || (
- top > translateY - 6 &&
- top < translateY + clipHeight - 6
- ) ?
- '' :
- 'none'
- });
- }
- }, this);
- }
- },
- /**
- * Render the legend title on top of the legend.
- *
- * @private
- * @function Highcharts.Legend#renderTitle
- */
- renderTitle: function () {
- var options = this.options,
- padding = this.padding,
- titleOptions = options.title,
- titleHeight = 0,
- bBox;
- if (titleOptions.text) {
- if (!this.title) {
- /**
- * SVG element of the legend title.
- *
- * @readonly
- * @name Highcharts.Legend#title
- * @type {Highcharts.SVGElement}
- */
- this.title = this.chart.renderer.label(
- titleOptions.text,
- padding - 3,
- padding - 4,
- null,
- null,
- null,
- options.useHTML,
- null,
- 'legend-title'
- )
- .attr({ zIndex: 1 });
- if (!this.chart.styledMode) {
- this.title.css(titleOptions.style);
- }
- this.title.add(this.group);
- }
- // Set the max title width (#7253)
- if (!titleOptions.width) {
- this.title.css({
- width: this.maxLegendWidth + 'px'
- });
- }
- 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.
- *
- * @function Highcharts.Legend#setText
- *
- * @param {Highcharts.Point|Highcharts.Series} 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
- * @function Highcharts.Legend#renderItem
- *
- * @param {Highcharts.Point|Highcharts.Series} 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,
- itemDistance = horizontal ? pick(options.itemDistance, 20) : 0,
- ltr = !options.rtl,
- bBox,
- 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,
- 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
- );
- if (!chart.styledMode) {
- // merge to prevent modifying original (#1021)
- li.css(merge(item.visible ? itemStyle : itemHiddenStyle));
- }
- li.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) {
- legend.fontMetrics = renderer.fontMetrics(
- chart.styledMode ? 12 : itemStyle.fontSize,
- li
- );
- legend.baseline =
- legend.fontMetrics.f + 3 + legend.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 (chart.styledMode || !itemStyle.width) {
- li.css({
- width: (
- options.itemWidth ||
- legend.widthOption ||
- chart.spacingBox.width
- ) - itemExtraWidth
- });
- }
- // Always update the text
- legend.setText(item);
- // calculate the positions for the next line
- bBox = li.getBBox();
- item.itemWidth = item.checkboxOffset =
- options.itemWidth ||
- item.legendItemWidth ||
- bBox.width + itemExtraWidth;
- legend.maxItemWidth = Math.max(legend.maxItemWidth, item.itemWidth);
- legend.totalItemWidth += item.itemWidth;
- legend.itemHeight = item.itemHeight = Math.round(
- item.legendItemHeight || bBox.height || legend.symbolHeight
- );
- },
- /**
- * Get the position of the item in the layout. We now know the
- * maxItemWidth from the previous loop.
- *
- * @private
- * @function Highcharts.Legend#layoutItem
- *
- * @param {Highcharts.Point|Highcharts.Series} item
- */
- layoutItem: function (item) {
- var options = this.options,
- padding = this.padding,
- horizontal = options.layout === 'horizontal',
- itemHeight = item.itemHeight,
- itemMarginBottom = options.itemMarginBottom || 0,
- itemMarginTop = this.itemMarginTop,
- itemDistance = horizontal ? pick(options.itemDistance, 20) : 0,
- maxLegendWidth = this.maxLegendWidth,
- itemWidth = (
- options.alignColumns &&
- this.totalItemWidth > maxLegendWidth
- ) ?
- this.maxItemWidth :
- item.itemWidth;
- // If the item exceeds the width, start a new line
- if (
- horizontal &&
- this.itemX - padding + itemWidth > maxLegendWidth
- ) {
- this.itemX = padding;
- this.itemY += itemMarginTop + this.lastLineHeight +
- itemMarginBottom;
- this.lastLineHeight = 0; // reset for next line (#915, #3976)
- }
- // Set the edge positions
- this.lastItemY = itemMarginTop + this.itemY + itemMarginBottom;
- this.lastLineHeight = Math.max( // #915
- itemHeight,
- this.lastLineHeight
- );
- // cache the position of the newly generated or reordered items
- item._legendItemPos = [this.itemX, this.itemY];
- // advance
- if (horizontal) {
- this.itemX += itemWidth;
- } else {
- this.itemY += itemMarginTop + itemHeight + itemMarginBottom;
- this.lastLineHeight = itemHeight;
- }
- // the width of the widest item
- this.offsetWidth = this.widthOption || Math.max(
- (
- horizontal ? this.itemX - padding - (item.checkbox ?
- // decrease by itemDistance only when no checkbox #4853
- 0 :
- itemDistance
- ) : itemWidth
- ) + padding,
- this.offsetWidth
- );
- },
- /**
- * Get all items, which is one item per series for most series and one
- * item per point for pie series and its derivatives.
- *
- * @private
- * @function Highcharts.Legend#getAllItems
- *
- * @return {Array<Highcharts.Point|Highcharts.Series>}
- * The current items in the legend.
- *
- * @fires Highcharts.Legend#event:afterGetAllItems
- *
- * @todo
- * Make events official: Fires the event `afterGetAllItems`.
- */
- getAllItems: function () {
- var allItems = [];
- this.chart.series.forEach(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
- )
- );
- }
- });
- fireEvent(this, 'afterGetAllItems', { allItems: allItems });
- return allItems;
- },
- /**
- * Get a short, three letter string reflecting the alignment and layout.
- *
- * @private
- * @function Highcharts.Legend#getAlignment
- *
- * @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)
- if (this.proximate) {
- return options.align.charAt(0) + 'tv';
- }
- 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
- * @function Highcharts.Legend#adjustMargins
- *
- * @param {Array<number>} margin
- *
- * @param {number} spacing
- */
- adjustMargins: function (margin, spacing) {
- var chart = this.chart,
- options = this.options,
- alignment = this.getAlignment();
- if (alignment) {
- ([
- /(lth|ct|rth)/,
- /(rtv|rm|rbv)/,
- /(rbh|cb|lbh)/,
- /(lbv|lm|ltv)/
- ]).forEach(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.options.title.margin !== undefined ?
- chart.titleOffset +
- chart.options.title.margin :
- 0
- ) // #7428, #7894
- )
- );
- }
- });
- }
- },
- /**
- * @private
- * @function Highcharts.Legend#proximatePositions
- */
- proximatePositions: function () {
- var chart = this.chart,
- boxes = [],
- alignLeft = this.options.align === 'left';
- this.allItems.forEach(function (item) {
- var lastPoint,
- height,
- useFirstPoint = alignLeft;
- if (item.xAxis && item.points) {
- if (item.xAxis.options.reversed) {
- useFirstPoint = !useFirstPoint;
- }
- lastPoint = H.find(
- useFirstPoint ?
- item.points :
- item.points.slice(0).reverse(),
- function (item) {
- return H.isNumber(item.plotY);
- }
- );
- height = item.legendGroup.getBBox().height;
- boxes.push({
- target: item.visible ?
- (lastPoint ? lastPoint.plotY : item.xAxis.height) -
- 0.3 * height :
- chart.plotHeight,
- size: height,
- item: item
- });
- }
- }, this);
- H.distribute(boxes, chart.plotHeight);
- boxes.forEach(function (box) {
- box.item._legendItemPos[1] =
- chart.plotTop - chart.spacing[0] + box.pos;
- });
- },
- /**
- * 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.
- *
- * @private
- * @function Highcharts.Legend#render
- */
- 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,
- allowedWidth;
- legend.itemX = padding;
- legend.itemY = legend.initialItemY;
- legend.offsetWidth = 0;
- legend.lastItemY = 0;
- legend.widthOption = H.relativeLength(
- options.width,
- chart.spacingBox.width - padding
- );
- // Compute how wide the legend is allowed to be
- allowedWidth = chart.spacingBox.width - 2 * padding - options.x;
- if (['rm', 'lm'].indexOf(legend.getAlignment().substring(0, 2)) > -1) {
- allowedWidth /= 2;
- }
- legend.maxLegendWidth = legend.widthOption || allowedWidth;
- if (!legendGroup) {
- /**
- * SVG group of the legend.
- *
- * @readonly
- * @name Highcharts.Legend#group
- * @type {Highcharts.SVGElement}
- */
- 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();
- }
- /**
- * All items for the legend, which is an array of series for most series
- * and an array of points for pie series and its derivatives.
- *
- * @readonly
- * @name Highcharts.Legend#allItems
- * @type {Array<Highcharts.Point|Highcharts.Series>}
- */
- legend.allItems = allItems;
- legend.display = display = !!allItems.length;
- // Render the items. First we run a loop to set the text and properties
- // and read all the bounding boxes. The next loop computes the item
- // positions based on the bounding boxes.
- legend.lastLineHeight = 0;
- legend.maxItemWidth = 0;
- legend.totalItemWidth = 0;
- legend.itemHeight = 0;
- allItems.forEach(legend.renderItem, legend);
- allItems.forEach(legend.layoutItem, legend);
- // Get the box
- legendWidth = (legend.widthOption || legend.offsetWidth) + padding;
- legendHeight = legend.lastItemY + legend.lastLineHeight +
- legend.titleHeight;
- legendHeight = legend.handleOverflow(legendHeight);
- legendHeight += padding;
- // Draw the border and/or background
- if (!box) {
- /**
- * SVG element of the legend box.
- *
- * @readonly
- * @name Highcharts.Legend#box
- * @type {Highcharts.SVGElement}
- */
- legend.box = box = renderer.rect()
- .addClass('highcharts-legend-box')
- .attr({
- r: options.borderRadius
- })
- .add(legendGroup);
- box.isNew = true;
- }
- // Presentational
- if (!chart.styledMode) {
- 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']();
- // Open for responsiveness
- if (chart.styledMode && legendGroup.getStyle('display') === 'none') {
- legendWidth = legendHeight = 0;
- }
- legend.legendWidth = legendWidth;
- legend.legendHeight = legendHeight;
- 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,
- verticalAlign: this.proximate ? 'top' : options.verticalAlign
- }), true, alignTo);
- }
- if (!this.proximate) {
- this.positionItems();
- }
- },
- /**
- * Set up the overflow handling by adding navigation with up and down arrows
- * below the legend.
- *
- * @private
- * @function Highcharts.Legend#handleOverflow
- *
- * @param {number} legendHeight
- *
- * @return {number}
- */
- 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)
- allItems.forEach(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 &&
- y !== lastY // #2617
- ) {
- 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');
- if (!chart.styledMode) {
- this.pager.css(navOptions.style);
- }
- this.pager.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.
- *
- * @private
- * @function Highcharts.Legend#scroll
- *
- * @param {number} scrollBy
- * The number of pages to scroll.
- *
- * @param {Highcharts.AnimationOptionsObject} 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'
- });
- if (!this.chart.styledMode) {
- 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();
- }
- }
- };
- /**
- * Legend symbol mixin.
- *
- * @private
- * @mixin Highcharts.LegendSymbolMixin
- */
- H.LegendSymbolMixin = {
- /**
- * Get the series' symbol in the legend
- *
- * @private
- * @function Highcharts.LegendSymbolMixin.drawRectangle
- *
- * @param {Highcharts.Legend} legend
- * The legend object
- *
- * @param {Highcharts.Point|Highcharts.Series} 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.
- *
- * @private
- * @function Highcharts.LegendSymbolMixin.drawLineMarker
- *
- * @param {Highcharts.Legend} 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
- if (!this.chart.styledMode) {
- 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 && symbolWidth) {
- // 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
- if (!legend.bubbleLegend) {
- setTimeout(runPositionItem);
- }
- });
- }
|