1 | <template>
|
2 | <div class="vue-charts-css" :style="style">
|
3 | <table
|
4 | :class="chartClasses"
|
5 | >
|
6 | <caption v-if="heading !== null || $slots.heading" class="heading">
|
7 | <slot name="heading" :heading="heading">{{ heading }}</slot>
|
8 | </caption>
|
9 |
|
10 | <tbody>
|
11 | <tr
|
12 | v-for="(row, rowIndex) in rows"
|
13 | :key="rowIndex"
|
14 | >
|
15 | <th scope="row"><slot name="label" :label="labels[rowIndex]" :labelIndex="rowIndex">{{ labels[rowIndex] }}</slot></th>
|
16 |
|
17 | <td
|
18 | v-for="(value, colIndex) in row"
|
19 | :key="colIndex"
|
20 | :style="resolveDataStyle(value, rowIndex, colIndex)"
|
21 | >
|
22 | <span class="data">
|
23 | <slot name="data" :value="value" :formattedValue="formatDataValue(value.valueRaw)">
|
24 | {{ formatDataValue(value.valueRaw) }}
|
25 | </slot>
|
26 | </span>
|
27 | <span v-if="value.tooltip" class="tooltip">{{ value.tooltip }}</span>
|
28 | </td>
|
29 | </tr>
|
30 | </tbody>
|
31 | </table>
|
32 |
|
33 | <slot
|
34 | name="legend"
|
35 | v-if="( $slots.legend || showLegend ) && this.datasets.length > 0"
|
36 | :datasets="datasets"
|
37 | >
|
38 | <ul :class="legendClasses">
|
39 | <li v-for="(dataset, index) in datasets" :key="index + '' + datasets.length">
|
40 | {{ dataset.name }}
|
41 | </li>
|
42 | </ul>
|
43 | </slot>
|
44 | </div>
|
45 | </template>
|
46 |
|
47 | <script>
|
48 | export default {
|
49 | name: "charts-css",
|
50 |
|
51 | props: {
|
52 | type: { type: String, required: true, },
|
53 | heading: { type: String },
|
54 | headingSize: { type: String, default: "1rem", },
|
55 | showHeading: { type: Boolean, default: false, },
|
56 |
|
57 | labels: { type: Array, required: true, },
|
58 | showLabels: { type: Boolean, default: false, },
|
59 |
|
60 | dataSpacing: { type: Number, default: 0, },
|
61 | hideData: { type: Boolean, default: false, },
|
62 | showDataAxis: { type: Boolean, default: false, },
|
63 | showDataOnHover: { type: Boolean, default: false, },
|
64 |
|
65 | datasets: { type: Array, required: true, },
|
66 |
|
67 | showLegend: { type: Boolean, default: false, },
|
68 | legendInline: { type: Boolean, default: true, },
|
69 | legendType: { type: String, default: "square", },
|
70 |
|
71 | showTooltips: { type: Boolean, default: false, },
|
72 | resolveDataTooltip: {
|
73 | type: Function,
|
74 | default: (value, label, datasetName, rowIndex, colIndex, hasMultipleDatasets = false) => {
|
75 | return ( datasetName && hasMultipleDatasets ? datasetName : label ) + ": " + value;
|
76 | },
|
77 | },
|
78 |
|
79 | reverse: { type: Boolean, default: false, },
|
80 | stacked: { type: Boolean, default: false, },
|
81 |
|
82 | classes: { type: String, },
|
83 |
|
84 | color: { type: String, default: null, },
|
85 |
|
86 | formatDataValue: { type: Function, default: (value) => value },
|
87 | resolveDataColor: {
|
88 | type: Function,
|
89 | default: (value, label, datasetName, rowIndex, colIndex, hasMultipleDatasets = false) => null,
|
90 | },
|
91 | },
|
92 |
|
93 | computed: {
|
94 | style()
|
95 | {
|
96 | let style = `--heading-size: ${this.headingSize};`
|
97 |
|
98 | if (this.color){
|
99 | style += `--color: ${this.color};`;
|
100 | }
|
101 |
|
102 | return style;
|
103 | },
|
104 |
|
105 | legendClasses(){
|
106 | if (this.showLegend){
|
107 | return "charts-css legend " + ( this.legendInline ? 'legend-inline':'' ) + " legend-" + this.legendType;
|
108 | }
|
109 | return "";
|
110 | },
|
111 |
|
112 | chartClasses()
|
113 | {
|
114 | let propClasses = {
|
115 | "multiple": this.datasets.length > 1,
|
116 | "reverse": this.reverse,
|
117 | "show-heading": this.showHeading,
|
118 | "hide-data": this.hideData,
|
119 | "show-data-on-hover": this.showDataOnHover,
|
120 | "show-data-axis": this.showDataAxis,
|
121 | "show-labels": this.showLabels,
|
122 | "stacked": this.stacked,
|
123 | };
|
124 |
|
125 | if (this.dataSpacing){
|
126 | propClasses[ "data-spacing-" + this.dataSpacing ] = true;
|
127 | }
|
128 |
|
129 | let propClassesString = Object.keys(propClasses).reduce((carry, chartClass) => {
|
130 | if (propClasses[chartClass]){
|
131 | carry += " " + chartClass;
|
132 | }
|
133 | return carry;
|
134 | }, "");
|
135 |
|
136 | let chartClasses = `charts-css ${this.type} ` + propClassesString + " " + ( this.classes ? this.classes : '' );
|
137 |
|
138 | return chartClasses.trim();
|
139 | },
|
140 |
|
141 | datasetsCount()
|
142 | {
|
143 | return this.datasets.length;
|
144 | },
|
145 |
|
146 | hasHeading()
|
147 | {
|
148 | return !!this.$slots.heading;
|
149 | },
|
150 |
|
151 | /**
|
152 | * Converts from datasets schema to Charts.CSS rendering.
|
153 | * @return {array}
|
154 | */
|
155 | rows()
|
156 | {
|
157 | /**
|
158 | * get highest value in values, so we can calculate scale between 0.0 and 1.0
|
159 | * @type {Number}
|
160 | */
|
161 | const max = Math.max(...this.datasets.reduce((carry, dataset) => {
|
162 | carry = carry.concat(dataset.values);
|
163 | return carry;
|
164 | }, []));
|
165 |
|
166 | const chartType = this.type;
|
167 |
|
168 | return this.datasets.reduce((carry, dataset, index) => {
|
169 |
|
170 | /**
|
171 | * Map dataset to each column
|
172 | */
|
173 | dataset.values.forEach((value, valueIndex) => {
|
174 | if (typeof carry[valueIndex] === "undefined"){
|
175 | carry[valueIndex] = [];
|
176 | }
|
177 |
|
178 | let tooltip = this.resolveDataTooltip && this.showTooltips ?
|
179 | this.resolveDataTooltip(value, this.labels[valueIndex], dataset.name, valueIndex, valueIndex, this.datasets.length > 1) :
|
180 | null
|
181 | ;
|
182 |
|
183 | let mappedValue = {
|
184 | valueRaw: value,
|
185 | valueIndex: valueIndex,
|
186 | datasetName: dataset.name,
|
187 | datasetIndex: index,
|
188 | label: this.labels[valueIndex],
|
189 | tooltip: tooltip,
|
190 | };
|
191 |
|
192 | if (chartType === "column" || chartType === "bar"){
|
193 | mappedValue.size = value / max;
|
194 | }
|
195 |
|
196 | if (chartType === "area" || chartType === "line"){
|
197 | mappedValue.start = value / max;
|
198 | mappedValue.size = dataset.values[valueIndex + 1] ? dataset.values[valueIndex + 1] / max : 0;
|
199 | }
|
200 |
|
201 | carry[valueIndex].push(mappedValue);
|
202 | });
|
203 |
|
204 | return carry;
|
205 | }, []);
|
206 | },
|
207 | },
|
208 |
|
209 | methods: {
|
210 | /**
|
211 | * Returns the mapped rendering CSS style for the given value, row and column.
|
212 | * @param value
|
213 | * @param rowIndex
|
214 | * @param colIndex
|
215 | * @return {{"--size", "--start"}}
|
216 | */
|
217 | resolveDataStyle(value, rowIndex, colIndex)
|
218 | {
|
219 | let style = {
|
220 | '--start': value.start,
|
221 | '--size': value.size,
|
222 | };
|
223 |
|
224 | if (this.resolveDataColor){
|
225 | const color = this.resolveDataColor(value, value.label, value.datasetName, rowIndex, colIndex, this.datasetsCount > 1);
|
226 |
|
227 | if (color){
|
228 | style["--color"] = color;
|
229 | }
|
230 | }
|
231 |
|
232 | return style;
|
233 | }
|
234 | },
|
235 | }
|
236 | </script>
|