diff --git a/packages/ez-vue/src/components/Bars.tsx b/packages/ez-vue/src/components/Bars.tsx index f62ea04..1844536 100644 --- a/packages/ez-vue/src/components/Bars.tsx +++ b/packages/ez-vue/src/components/Bars.tsx @@ -42,12 +42,22 @@ export default class Bars extends Vue { } render() { - const { shapeData } = this; + const { + shapeData, chart, $scopedSlots, cartesianScale, colorScale, + } = this; + const { dimensions } = chart; + return ( - {shapeData.map((rectDatum) => ( - - ))} + {$scopedSlots.default + ? $scopedSlots.default({ + shapeData, + scales: { ...cartesianScale, colorScale }, + dimensions, + }) + : shapeData.map((rectDatum) => ( + + ))} ); } diff --git a/packages/ez-vue/src/components/StackedBars.tsx b/packages/ez-vue/src/components/StackedBars.tsx new file mode 100644 index 0000000..ce2458d --- /dev/null +++ b/packages/ez-vue/src/components/StackedBars.tsx @@ -0,0 +1,114 @@ +import Vue, { PropType } from 'vue'; +import Component from 'vue-class-component'; +import { + ChartContext, + Direction, + RectangleDatum, + ScaleLinearOrBand, +} from 'eazychart-core/src/types'; +import { InjectReactive, Prop } from 'vue-property-decorator'; +import { ScaleOrdinal, scaleRectangleData } from 'eazychart-core/src'; +import Bar from '@/components/shapes/Bar'; + +@Component({ components: { Bar } }) +export default class StackedBars extends Vue { + @InjectReactive('chart') + private chart!: ChartContext; + + @InjectReactive('cartesianScale') + private cartesianScale!: { + xScale: ScaleLinearOrBand; + yScale: ScaleLinearOrBand; + }; + + @Prop({ + type: String, + required: true, + }) + private readonly singleDomainKey!: string; + + @Prop({ + type: Array, + required: true, + }) + private readonly stackDomainKeys!: string[]; + + @Prop({ + type: String as PropType, + required: true, + }) + private readonly direction: Direction = Direction.VERTICAL; + + get colorScale() { + return this.chart.getScale('colorScale') as ScaleOrdinal; + } + + get scaledDataDict() { + return this.stackDomainKeys.reduce( + (acc: { [key: string]: RectangleDatum[] }, yDomainKey) => { + acc[yDomainKey] = scaleRectangleData( + this.chart.data, + this.singleDomainKey, + yDomainKey, + this.cartesianScale.xScale, + this.cartesianScale.yScale, + this.colorScale, + this.chart.dimensions, + this.chart.isRTL, + ); + return acc; + }, + {}, + ); + } + + render() { + const { + scaledDataDict, chart, stackDomainKeys, colorScale, direction, + } = this; + + return ( + + {chart.data.map((_datum, idx) => ( + // The Domain keys still needs to be sorted. + // We create a bar for every data row + // Each bar is a stack bar where every element is a domain key. + + {stackDomainKeys.map((yDomainKey, domainIdx) => { + const color = colorScale.scale(yDomainKey); + const scaledData = scaledDataDict[yDomainKey][idx]; + // The first domain key will not be affected. + const previousRectWidth = domainIdx !== 0 + ? scaledDataDict[stackDomainKeys[domainIdx - 1]][idx].width + : 0; + const previousRectHeight = domainIdx !== 0 + ? scaledDataDict[stackDomainKeys[domainIdx - 1]][idx].height + : 0; + // The height or the width of the current bar will be computed depending + // to the orientaion + // the height will be currentDKHeight - previousDKHeight (same for the width) + const shapeDatum = { + ...scaledData, + width: + direction === Direction.HORIZONTAL + ? scaledData.width - previousRectWidth + : scaledData.width, + height: + direction === Direction.VERTICAL + ? scaledData.height - previousRectHeight + : scaledData.height, + }; + + return ( + + ); + })} + + ))} + + ); + } +} diff --git a/packages/ez-vue/src/components/scales/ColorScale.tsx b/packages/ez-vue/src/components/scales/ColorScale.tsx index d3f561a..24c8cbe 100644 --- a/packages/ez-vue/src/components/scales/ColorScale.tsx +++ b/packages/ez-vue/src/components/scales/ColorScale.tsx @@ -24,7 +24,7 @@ export default class ColorScale extends Vue { }) private readonly definition!: ScaleOrdinalDefinition; - mounted() { + created() { this.colorScale = this.defineScale(); this.chart.registerScale('colorScale', this.colorScale); } diff --git a/packages/ez-vue/src/recipes/column/ColumnChart.stories.tsx b/packages/ez-vue/src/recipes/column/ColumnChart.stories.tsx index 2dd20c2..9514eb6 100644 --- a/packages/ez-vue/src/recipes/column/ColumnChart.stories.tsx +++ b/packages/ez-vue/src/recipes/column/ColumnChart.stories.tsx @@ -5,6 +5,7 @@ import { } from 'eazychart-dev/storybook/data'; import ColumnChart from './ColumnChart'; import LineColumnChart from './LineColumnChart'; +import StackedColumnChart from './StackedColumnChart'; const meta: Meta = { title: 'Vue/Column Chart', @@ -38,6 +39,17 @@ const LineColumnTemplate: Story = (_args, { argTypes }) => ({ `, }); +const StackedColumnTemplate: Story = (_args, { argTypes }) => ({ + title: 'StackedColumn', + components: { StackedColumnChart, ChartWrapper }, + props: Object.keys(argTypes), + template: ` + + + + `, +}); + // By passing using the Args format for exported stories, // you can control the props for a component for reuse in a test // https://storybook.js.org/docs/vue/workflows/unit-testing @@ -84,3 +96,16 @@ const lineColumnArguments = { }; LineColumn.args = lineColumnArguments; + +export const StackedColumn = StackedColumnTemplate.bind({}); + +const StackedColumnArguments = { + ...defaultArguments, + yAxis: { + domainKeys: ['value', 'value1', 'value2'], + title: 'Temperature', + tickFormat: (d: number) => `${d}°`, + }, +}; + +StackedColumn.args = StackedColumnArguments; diff --git a/packages/ez-vue/src/recipes/column/StackedColumnChart.tsx b/packages/ez-vue/src/recipes/column/StackedColumnChart.tsx new file mode 100644 index 0000000..c712a1a --- /dev/null +++ b/packages/ez-vue/src/recipes/column/StackedColumnChart.tsx @@ -0,0 +1,212 @@ +import { PropType } from 'vue'; +import Component, { mixins } from 'vue-class-component'; +import { + AnimationOptions, + ChartPadding, + Direction, + Position, + RawData, + GridConfig, + AxisConfig, + Dimensions, + AxisConfigMulti, +} from 'eazychart-core/src/types'; +import { Prop } from 'vue-property-decorator'; +import { ScaleBand, ScaleLinear } from 'eazychart-core/src'; +import Chart from '@/components/Chart'; +import Axis from '@/components/scales/Axis'; +import Legend from '@/components/addons/legend/Legend'; +import Tooltip from '@/components/addons/tooltip/Tooltip'; +import StackedBars from '@/components/StackedBars'; +import Grid from '@/components/scales/grid/Grid'; +import ColorScale from '@/components/scales/ColorScale'; +import CartesianScale from '@/components/scales/CartesianScale'; +import ToggleDomainKeyMixin from '@/lib/ToggleDomainKeyMixin'; + +@Component({ + components: { + Chart, + Grid, + StackedBars, + Axis, + Legend, + Tooltip, + }, +}) +export default class StackedColumnChart extends mixins(ToggleDomainKeyMixin) { + @Prop({ + type: Array as PropType, + required: true, + }) + private readonly data!: RawData; + + @Prop({ + type: Array, + default() { + return ['#339999', '#993399', '#333399']; + }, + }) + private readonly colors!: string[]; + + @Prop({ + type: Object as PropType, + default() { + return {}; + }, + }) + private readonly dimensions!: Partial; + + @Prop({ + type: Object as PropType, + default() { + return { + easing: 'easeBack', + duration: 400, + delay: 0, + }; + }, + }) + private readonly animationOptions!: AnimationOptions; + + @Prop({ + type: Object as PropType, + default() { + return { + left: 150, + bottom: 100, + right: 150, + top: 100, + }; + }, + }) + private readonly padding!: ChartPadding; + + @Prop({ + type: Object as PropType, + default() { + return { + directions: [Direction.HORIZONTAL, Direction.VERTICAL], + color: '#a8a8a8', + }; + }, + }) + private readonly grid!: GridConfig; + + @Prop({ + type: Object as PropType>, + default() { + return { + domainKey: 'name', + }; + }, + }) + private readonly xAxis!: AxisConfig; + + @Prop({ + type: Object as PropType>, + default() { + return { + domainKeys: ['value', 'value1', 'value2'], + }; + }, + }) + private readonly yAxis!: AxisConfigMulti; + + @Prop({ + type: Boolean, + default() { + return false; + }, + }) + private readonly isRTL!: boolean; + + getData(): RawData { + return this.data; + } + + getDomainKeys(): string[] { + return this.yAxis.domainKeys; + } + + render() { + const { + xAxis, + yAxis, + data, + colors, + padding, + animationOptions, + grid, + isRTL, + $scopedSlots, + dimensions, + activeDomain, + activeDomainKeys, + toggleDomainKey, + } = this; + + const scopedSlots = { + Legend: $scopedSlots.Legend + ? $scopedSlots.Legend + : () => , + Tooltip: $scopedSlots.Tooltip, + }; + return ( + + + + + + + + + + + ); + } +}