diff --git a/packages/core/demo/data/bar.ts b/packages/core/demo/data/bar.ts index 9ef22a03de..b7721af751 100644 --- a/packages/core/demo/data/bar.ts +++ b/packages/core/demo/data/bar.ts @@ -36,6 +36,25 @@ export const groupedBarOptions = { } }; +export const groupedBarSelectedGroupsData = groupedBarData; + +// Grouped bar with selected groups option +export const groupedBarSelectedGroupsOptions = { + title: "Grouped bar (selected groups)", + data: { + selectedGroups: ["Dataset 1", "Dataset 3"] + }, + axes: { + left: { + mapsTo: "value", + }, + bottom: { + scaleType: "labels", + mapsTo: "key", + }, + }, +}; + // Horizontal Grouped export const groupedHorizontalBarData = groupedBarData; diff --git a/packages/core/demo/data/index.ts b/packages/core/demo/data/index.ts index eb6d3e582b..4cdc52784c 100644 --- a/packages/core/demo/data/index.ts +++ b/packages/core/demo/data/index.ts @@ -176,6 +176,11 @@ let allDemoGroups = [ data: barDemos.groupedBarData, chartType: chartTypes.GroupedBarChart }, + { + options: barDemos.groupedBarSelectedGroupsOptions, + data: barDemos.groupedBarSelectedGroupsData, + chartType: chartTypes.GroupedBarChart + }, { options: barDemos.groupedBarEmptyStateOptions, data: barDemos.groupedBarEmptyStateData, @@ -377,6 +382,11 @@ let allDemoGroups = [ data: lineDemos.lineData, chartType: chartTypes.LineChart }, + { + options: lineDemos.lineSelectedGroupsOptions, + data: lineDemos.lineSelectedGroupsData, + chartType: chartTypes.LineChart, + }, { options: lineDemos.lineTimeSeriesRotatedTicksOptions, data: lineDemos.lineTimeSeriesDataRotatedTicks, diff --git a/packages/core/demo/data/line.ts b/packages/core/demo/data/line.ts index 07002eafd7..15727028ea 100644 --- a/packages/core/demo/data/line.ts +++ b/packages/core/demo/data/line.ts @@ -101,7 +101,7 @@ export const lineLongLabelOptions = { }; export const lineCustomDomainOptions = { - title: "Line (discrete with custom domain)", + title: "Line (discrete with customized domain)", axes: { bottom: { title: "2019 Annual Sales Figures", @@ -118,6 +118,48 @@ export const lineCustomDomainOptions = { } }; +export const lineSelectedGroupsData = [ + { group: "Dataset 1", key: "Qty", value: 34200 }, + { group: "Dataset 1", key: "More", value: 23500 }, + { group: "Dataset 1", key: "Sold", value: 53100 }, + { group: "Dataset 1", key: "Restocking", value: 42300 }, + { group: "Dataset 1", key: "Misc", value: 12300 }, + { group: "Dataset 2", key: "Qty", value: 34200 }, + { group: "Dataset 2", key: "More", value: 56000 }, + { group: "Dataset 2", key: "Sold", value: 42300 }, + { group: "Dataset 2", key: "Restocking", value: 21400 }, + { group: "Dataset 2", key: "Misc", value: 0 }, + { group: "Dataset 3", key: "Qty", value: 41200 }, + { group: "Dataset 3", key: "More", value: 18400 }, + { group: "Dataset 3", key: "Sold", value: 34210 }, + { group: "Dataset 3", key: "Restocking", value: 1400 }, + { group: "Dataset 3", key: "Misc", value: 42100 }, + { group: "Dataset 4", key: "Qty", value: 22000 }, + { group: "Dataset 4", key: "More", value: 1200 }, + { group: "Dataset 4", key: "Sold", value: 9000 }, + { group: "Dataset 4", key: "Restocking", value: 24000, audienceSize: 10 }, + { group: "Dataset 4", key: "Misc", value: 3000, audienceSize: 10 }, +]; + +export const lineSelectedGroupsOptions = { + title: "Line (selected groups)", + data: { + selectedGroups: ["Dataset 1", "Dataset 3"] + }, + axes: { + bottom: { + title: "2019 Annual Sales Figures", + mapsTo: "key", + scaleType: "labels", + }, + left: { + mapsTo: "value", + title: "Conversion rate", + scaleType: "linear", + }, + }, +}; + export const lineTimeSeriesData = [ { group: "Dataset 1", date: new Date(2019, 0, 1), value: 50000 }, { group: "Dataset 1", date: new Date(2019, 0, 5), value: 65000 }, @@ -159,7 +201,7 @@ export const lineTimeSeriesOptions = { }; export const lineTimeSeriesCustomDomainOptions = { - title: "Line (time series with custom domain)", + title: "Line (time series with customized domain)", axes: { bottom: { title: "2019 Annual Sales Figures", diff --git a/packages/core/src/components/essentials/legend.ts b/packages/core/src/components/essentials/legend.ts index eed36dd2f1..8e840b6971 100644 --- a/packages/core/src/components/essentials/legend.ts +++ b/packages/core/src/components/essentials/legend.ts @@ -33,7 +33,10 @@ export class Legend extends Component { const addedLegendItems = legendItems .enter() .append("g") - .classed("legend-item", true); + .classed("legend-item", true) + .classed("active", function (d, i) { + return d.status === options.legend.items.status.ACTIVE; + }); // Configs const checkboxRadius = options.legend.checkbox.radius; @@ -216,14 +219,24 @@ export class Legend extends Component { legendItem .select("text") .attr("x", startingPoint + spaceNeededForCheckbox) - .attr("y", yOffset + yPosition + 2); + .attr("y", yOffset + yPosition + 3); lastYPosition = yPosition; + // Test if legendItems are placed in the correct direction + const testHorizontal = (!legendOrientation || + legendOrientation === LegendOrientations.HORIZONTAL) && + legendItem.select("rect.checkbox").attr("y") === '0'; + + const testVertical = legendOrientation === LegendOrientations.VERTICAL && + legendItem.select("rect.checkbox").attr("x") === '0'; + + const hasCorrectLegendDirection = testHorizontal || testVertical; + // Render checkbox check icon - if ( - hasDeactivatedItems && - legendItem.select("g.check").empty() + if (hasDeactivatedItems && + legendItem.select("g.check").empty() && + hasCorrectLegendDirection ) { legendItem.append("g").classed("check", true).html(` { + return dataGroups.find( + (group) => group.name === datum[groupMapsTo] + ); + }); + } + + getDisplayData() { + if (!this.get("data")) { + return null; + } + + const { ACTIVE } = Configuration.legend.items.status; + const dataGroups = this.getDataGroups(); + const { groupMapsTo } = this.getOptions().data; + + const allDataFromDomain = this.getAllDataFromDomain(); + + return allDataFromDomain.filter((datum) => { const group = dataGroups.find( (group) => group.name === datum[groupMapsTo] ); @@ -339,6 +356,22 @@ export class ChartModel { }); } + // Updates selected groups + const updatedActiveItems = dataGroups.filter(group => group.status === ACTIVE); + const options = this.getOptions(); + + const hasUpdatedDeactivatedItems = dataGroups.some( + group => group.status === DISABLED + ); + + // If there are deactivated items, map the item name into selected groups + if (hasUpdatedDeactivatedItems) { + options.data.selectedGroups = updatedActiveItems.map(activeItem => activeItem.name); + } else { + // If every item is active, clear array + options.data.selectedGroups = []; + }; + // dispatch legend filtering event with the status of all the dataLabels this.services.events.dispatchEvent(Events.Legend.ITEMS_UPDATE, { dataGroups @@ -479,15 +512,32 @@ export class ChartModel { protected generateDataGroups(data) { const { groupMapsTo } = this.getOptions().data; - const { ACTIVE } = Configuration.legend.items.status; + const { ACTIVE, DISABLED } = Configuration.legend.items.status; + const options = this.getOptions(); const uniqueDataGroups = map( data, (datum) => datum[groupMapsTo] ).keys(); - return uniqueDataGroups.map((groupName) => ({ + + // check if selectedGroups can be applied to chart with current data groups + if (options.data.selectedGroups.length) { + const hasAllSelectedGroups = options.data.selectedGroups + .every(groupName => uniqueDataGroups.includes(groupName)); + if (!hasAllSelectedGroups) { + options.data.selectedGroups = []; + }; + } + + // Get group status based on items in selected groups + const getStatus = (groupName) => + !options.data.selectedGroups.length || options.data.selectedGroups.includes(groupName) + ? ACTIVE + : DISABLED; + + return uniqueDataGroups.map(groupName => ({ name: groupName, - status: ACTIVE + status: getStatus(groupName) })); } diff --git a/packages/core/src/selectedGroups.spec.ts b/packages/core/src/selectedGroups.spec.ts new file mode 100644 index 0000000000..21411fb043 --- /dev/null +++ b/packages/core/src/selectedGroups.spec.ts @@ -0,0 +1,56 @@ +import { TestEnvironment } from "./tests/index"; + +// import the settings for the css prefixes +import settings from "carbon-components/es/globals/js/settings"; + +import { options } from "./configuration"; +import { Events } from "./interfaces"; + +import { select } from "d3-selection"; + +describe("selectedGroups option", () => { + beforeEach(function () { + const testEnvironment = new TestEnvironment(); + testEnvironment.render(); + + this.chart = testEnvironment.getChartReference(); + this.testEnvironment = testEnvironment; + }); + + describe("selected legend labels", () => { + it("should match the selected groups provided in options", function (done) { + const sampleSelectedGroups = ["Dataset 1", "Dataset 3"]; + + const chartEventsService = this.chart.services.events; + + const renderCb = () => { + // Remove render event listener + chartEventsService.removeEventListener( + Events.Chart.RENDER_FINISHED, + renderCb + ); + + const selectedLegendLabels = select( + `g.${settings.prefix}--${options.chart.style.prefix}--legend` + ) + .selectAll("g.legend-item.active > text") + .nodes() + .map((item) => item["innerHTML"]); + + expect(selectedLegendLabels).toEqual(sampleSelectedGroups); + + done(); + }; + + // Add event listener for when chart render is finished + chartEventsService.addEventListener( + Events.Chart.RENDER_FINISHED, + renderCb + ); + }); + }); + + afterEach(function () { + this.testEnvironment.destroy(); + }); +}); diff --git a/packages/core/src/tests/test-environment.ts b/packages/core/src/tests/test-environment.ts index 7ca1d518cf..1558216d35 100644 --- a/packages/core/src/tests/test-environment.ts +++ b/packages/core/src/tests/test-environment.ts @@ -9,7 +9,10 @@ import { groupedBarData, groupedBarOptions } from "../../demo/data"; export const data = groupedBarData as ChartData; export const options = Object.assign(groupedBarOptions, { - title: "My chart" + title: "My chart", + data: { + selectedGroups: ["Dataset 1", "Dataset 3"] + } }) as any; export class TestEnvironment {