Completed
Push — master ( 6dc7d8...407c40 )
by Karsten
15:45
created

formats/jqplot/SRF_jqPlotSeries.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * A query printer for charts series using the jqPlot JavaScript library.
5
 *
6
 * @since 1.8
7
 * @licence GNU GPL v2 or later
8
 *
9
 * @author mwjames
10
 */
11
class SRFjqPlotSeries extends SMWResultPrinter {
12
13
	/**
14
	 * @see SMWResultPrinter::getName
15
	 */
16
	public function getName() {
17
		return wfMessage( 'srf-printername-jqplotseries' )->text();
18
	}
19
20
	/**
21
	 * Returns an array with the numerical data in the query result.
22
	 *
23
	 *
24
	 * @param SMWQueryResult $result
25
	 * @param $outputMode
26
	 *
27
	 * @return string
28
	 */
29
	protected function getResultText( SMWQueryResult $result, $outputMode ) {
30
31
		// Get data set
32
		$data = $this->getResultData( $result, $outputMode );
33
34
		// Check data availability
35
		if ( $data['series'] === [] ) {
36
			return $result->addErrors(
37
				[
38
					wfMessage( 'srf-warn-empy-chart' )
39
						->inContentLanguage()->text() ]
40
			);
41
		} else {
42
			$options['sask'] = SRFUtils::htmlQueryResultLink( $this->getLink( $result, SMW_OUTPUT_HTML ) );
0 ignored issues
show
Coding Style Comprehensibility introduced by
$options was never initialized. Although not strictly required by PHP, it is generally a good practice to add $options = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
43
			return $this->getFormatOutput( $this->getFormatSettings( $this->getNumbersTicks( $data ), $options ) );
44
		}
45
	}
46
47
	/**
48
	 * Returns an array with the numerical data
49
	 *
50
	 * @since 1.8
51
	 *
52
	 * @param SMWQueryResult $result
53
	 * @param $outputMode
54
	 *
55
	 * @return array
56
	 */
57
	protected function getResultData( SMWQueryResult $res, $outputMode ) {
58
		$data = [];
59
		$data['series'] = [];
60
61
		while ( $row = $res->getNext() ) {
62
			// Loop over their fields (properties)
63
			$label = '';
64
			$i = 0;
65
66
			foreach ( $row as /* SMWResultArray */
67
					  $field ) {
68
				$i++;
69
				$rowNumbers = [];
70
71
				// Grouping by subject (page object) or property
72
				if ( $this->params['group'] === 'subject' ) {
73
					$groupedBy = $field->getResultSubject()->getTitle()->getText();
74
				} else {
75
					$groupedBy = $field->getPrintRequest()->getLabel();
76
				}
77
78
				// Property label
79
				$property = $field->getPrintRequest()->getLabel();
80
81
				// First column property typeid
82
				$i == 1 ? $data['fcolumntypeid'] = $field->getPrintRequest()->getTypeID() : '';
83
84
				// Loop over all values for the property.
85
				while ( ( /* SMWDataValue */
86
					$object = $field->getNextDataValue() ) !== false ) {
87
88
					if ( $object->getDataItem()->getDIType() == SMWDataItem::TYPE_NUMBER ) {
89
						$number = $object->getNumber();
90
91
						// Checking against the row and in case the first column is a numeric
92
						// value it is handled as label with the remaining steps continue to work
93
						// as it were a text label
94
						// The first column container will not be part of the series container
95
						if ( $i == 1 ) {
96
							$label = $number;
97
							continue;
98
						}
99
100
						if ( $label !== '' && $number >= $this->params['min'] ) {
101
102
							// Reference array summarize all items per row
103
							$rowNumbers += [ 'subject' => $label, 'value' => $number, 'property' => $property ];
104
105
							// Store plain numbers for simpler handling
106
							$data['series'][$groupedBy][] = $number;
107
						}
108
					} elseif ( $object->getDataItem()->getDIType() == SMWDataItem::TYPE_TIME ) {
109
						$label = $object->getShortWikiText();
110
					} else {
111
						$label = $object->getWikiValue();
112
					}
113
				}
114
				// Only for array's with numbers 
115
				if ( count( $rowNumbers ) > 0 ) {
116
117
					// For cases where mainlabel=- we assume that the subject should not be
118
					// used as identifier and therefore we try to match the groupby
119
					// with the first available text label
120
					if ( $this->params['mainlabel'] == '-' && $this->params['group'] === 'subject' ) {
121
						$data[$this->params['group']][$label][] = $rowNumbers;
122
					} else {
123
						$data[$this->params['group']][$groupedBy][] = $rowNumbers;
124
					}
125
				}
126
			}
127
		}
128
		return $data;
129
	}
130
131
	/**
132
	 * Data set sorting
133
	 *
134
	 * @since 1.8
135
	 *
136
	 * @param array $data label => value
137
	 *
138
	 * @return array
139
	 */
140
	private function getFormatSettings( $data, $options ) {
141
142
		// Init
143
		$dataSet = [];
144
		$options['mode'] = 'series';
145
		$options['autoscale'] = false;
146
147
		// Available markers
148
		$marker = [ 'circle', 'diamond', 'square', 'filledCircle', 'filledDiamond', 'filledSquare' ];
149
150
		// Series colour(has to be null otherwise jqplot runs with a type error)
151
		$seriescolors = $this->params['chartcolor'] !== '' ? array_filter(
152
			explode( ",", $this->params['chartcolor'] )
153
		) : null;
154
155
		// Re-grouping
156
		foreach ( $data[$this->params['group']] as $rowKey => $row ) {
157
			$values = [];
158
159
			foreach ( $row as $key => $value ) {
160
				// Switch labels according to the group parameter
161
				$label = $this->params['grouplabel'] === 'property' ? $value['property'] : $value['subject'];
162
				$values[] = [ $label, $value['value'] ];
163
			}
164
			$dataSet[] = $values;
165
		}
166
167
		// Series plotting parameters
168
		foreach ( $data[$this->params['group']] as $key => $row ) {
169
			$series[] = [
0 ignored issues
show
Coding Style Comprehensibility introduced by
$series was never initialized. Although not strictly required by PHP, it is generally a good practice to add $series = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
170
				'label' => $key,
171
				'xaxis' => 'xaxis', // xaxis could also be xaxis2 or ...
172
				'yaxis' => 'yaxis',
173
				'fill' => $this->params['stackseries'],
174
				'showLine' => $this->params['charttype'] !== 'scatter',
175
				'showMarker' => true,
176
				'trendline' => [
177
					'show' => in_array( $this->params['trendline'], [ 'exp', 'linear' ] ),
178
					'shadow' => $this->params['theme'] !== 'simple',
179
					'type' => $this->params['trendline'],
180
				],
181
				'markerOptions' => [
182
					'style' => $marker[array_rand( $marker )],
183
					'shadow' => $this->params['theme'] !== 'simple'
184
				],
185
				'rendererOptions' => [ 'barDirection' => $this->params['direction'] ]
186
			];
187
		};
188
189
		// Basic parameters
190
		$parameters = [
191
			'numbersaxislabel' => $this->params['numbersaxislabel'],
192
			'labelaxislabel' => $this->params['labelaxislabel'],
193
			'charttitle' => $this->params['charttitle'],
194
			'charttext' => $this->params['charttext'],
195
			'infotext' => $this->params['infotext'],
196
			'theme' => $this->params['theme'] ? $this->params['theme'] : null,
197
			'valueformat' => $this->params['datalabels'] === 'label' ? '' : $this->params['valueformat'],
198
			'ticklabels' => $this->params['ticklabels'],
199
			'highlighter' => $this->params['highlighter'],
200
			'autoscale' => $options['autoscale'],
201
			'gridview' => $this->params['gridview'],
202
			'direction' => $this->params['direction'],
203
			'smoothlines' => $this->params['smoothlines'],
204
			'cursor' => $this->params['cursor'],
205
			'chartlegend' => $this->params['chartlegend'] !== '' ? $this->params['chartlegend'] : 'none',
206
			'colorscheme' => $this->params['colorscheme'] !== '' ? $this->params['colorscheme'] : null,
207
			'pointlabels' => $this->params['datalabels'] === 'none' ? false : $this->params['datalabels'],
208
			'datalabels' => $this->params['datalabels'],
209
			'stackseries' => $this->params['stackseries'],
210
			'grid' => $this->params['theme'] === 'vector' ? [ 'borderColor' => '#a7d7f9' ] : ( $this->params['theme'] === 'simple' ? [ 'borderColor' => '#ddd' ] : null ),
211
			'seriescolors' => $seriescolors
212
		];
213
214
		return [
215
			'data' => $dataSet,
216
			//'rawdata'      => $data , // control array
217
			'series' => $series,
218
			'ticks' => $data['numbersticks'],
219
			'total' => $data['total'],
220
			'fcolumntypeid' => $data['fcolumntypeid'],
221
			'sask' => $options['sask'],
222
			'mode' => $options['mode'],
223
			'renderer' => $this->params['charttype'],
224
			'parameters' => $parameters
225
		];
226
	}
227
228
	/**
229
	 * Fetch numbers ticks
230
	 *
231
	 * @since 1.8
232
	 *
233
	 * @param array $data
234
	 */
235
	protected function getNumbersTicks( array $data ) {
236
237
		// Only look for numeric values that have been stored
238
		$numerics = array_values( $data['series'] );
239
240
		// Find min and max values to determine the graphs axis parameter
241
		$maxValue = count( $numerics ) == 0 ? 0 : max( array_map( "max", $numerics ) );
242
243
		if ( $this->params['min'] === false ) {
244
			$minValue = count( $numerics ) == 0 ? 0 : min( array_map( "min", $numerics ) );
245
		} else {
246
			$minValue = $this->params['min'];
247
		}
248
249
		// Get ticks info
250
		$data['numbersticks'] = SRFjqPlot::getNumbersTicks( $minValue, $maxValue );
251
		$data['total'] = array_sum( array_map( "array_sum", $numerics ) );
252
253
		return $data;
254
	}
255
256
	/**
257
	 * Add resource definitions
258
	 *
259
	 * @since 1.8
260
	 *
261
	 * @param array $data
262
	 *
263
	 * @return string
264
	 */
265
	protected function addResources() {
266
		// RL module
267
		switch ( $this->params['charttype'] ) {
268
			case 'bubble':
269
				SMWOutputs::requireResource( 'ext.srf.jqplot.bubble' );
270
				break;
271
			case 'donut':
272
				SMWOutputs::requireResource( 'ext.srf.jqplot.donut' );
273
				break;
274
			case 'scatter':
275
			case 'line':
276
			case 'bar':
277
				SMWOutputs::requireResource( 'ext.srf.jqplot.bar' );
278
				break;
279
		}
280
281
		// Trendline plugin
282
		if ( in_array( $this->params['trendline'], [ 'exp', 'linear' ] ) ) {
283
			SMWOutputs::requireResource( 'ext.srf.jqplot.trendline' );
284
		}
285
286
		// Cursor plugin
287
		if ( in_array( $this->params['cursor'], [ 'zoom', 'tooltip' ] ) ) {
288
			SMWOutputs::requireResource( 'ext.srf.jqplot.cursor' );
289
		}
290
291
		// Highlighter plugin
292
		if ( $this->params['highlighter'] ) {
293
			SMWOutputs::requireResource( 'ext.srf.jqplot.highlighter' );
294
		}
295
296
		// Enhancedlegend plugin
297
		if ( $this->params['chartlegend'] ) {
298
			SMWOutputs::requireResource( 'ext.srf.jqplot.enhancedlegend' );
299
		}
300
301
		// gridview plugin
302
		if ( in_array( $this->params['gridview'], [ 'tabs' ] ) ) {
303
			SMWOutputs::requireResource( 'ext.srf.util.grid' );
304
		}
305
306
		// Pointlabels plugin
307
		if ( in_array( $this->params['datalabels'], [ 'value', 'label', 'percent' ] ) ) {
308
			SMWOutputs::requireResource( 'ext.srf.jqplot.pointlabels' );
309
		}
310
	}
311
312
	/**
313
	 * Prepare data for the output
314
	 *
315
	 * @since 1.8
316
	 *
317
	 * @param array $data
318
	 *
319
	 * @return string
320
	 */
321
	protected function getFormatOutput( array $data ) {
322
323
		$this->isHTML = true;
324
325
		static $statNr = 0;
326
		$chartID = 'jqplot-series-' . ++$statNr;
327
328
		// Encoding
329
		$requireHeadItem = [ $chartID => FormatJson::encode( $data ) ];
330
		SMWOutputs::requireHeadItem( $chartID, Skin::makeVariablesScript( $requireHeadItem ) );
331
332
		// Add RL resources
333
		$this->addResources();
334
335
		// Processing placeholder
336
		$processing = SRFUtils::htmlProcessingElement( $this->isHTML );
337
338
		// Conversion due to a string as value that can contain %
339
		$width = strstr( $this->params['width'], "%" ) ? $this->params['width'] : $this->params['width'] . 'px';
340
341
		// Chart/graph placeholder
342
		$chart = Html::rawElement(
343
			'div',
344
			[
345
				'id' => $chartID,
346
				'class' => 'container',
347
				'style' => "display:none; width: {$width}; height: {$this->params['height']}px;"
348
			],
349
			null
350
		);
351
352
		// Beautify class selector
353
		$class = $this->params['charttype'] ? '-' . $this->params['charttype'] : '';
354
		$class = $this->params['class'] ? $class . ' ' . $this->params['class'] : $class . ' jqplot-common';
355
356
		// Chart/graph wrappper
357
		return Html::rawElement(
358
			'div',
359
			[
360
				'class' => 'srf-jqplot' . $class,
361
			],
362
			$processing . $chart
363
		);
364
	}
365
366
	/**
367
	 * @see SMWResultPrinter::getParamDefinitions
368
	 *
369
	 * @since 1.8
370
	 *
371
	 * @param $definitions array of IParamDefinition
372
	 *
373
	 * @return array of IParamDefinition|array
374
	 */
375
	public function getParamDefinitions( array $definitions ) {
376
		$params = array_merge( parent::getParamDefinitions( $definitions ), SRFjqPlot::getCommonParams() );
377
378
		$params['infotext'] = [
379
			'message' => 'srf-paramdesc-infotext',
380
			'default' => '',
381
		];
382
383
		$params['stackseries'] = [
384
			'type' => 'boolean',
385
			'message' => 'srf-paramdesc-stackseries',
386
			'default' => false,
387
		];
388
389
		$params['group'] = [
390
			'message' => 'srf-paramdesc-group',
391
			'default' => 'subject',
392
			'values' => [ 'property', 'subject' ],
393
		];
394
395
		$params['grouplabel'] = [
396
			'message' => 'srf-paramdesc-grouplabel',
397
			'default' => 'subject',
398
			'values' => [ 'property', 'subject' ],
399
		];
400
401
		$params['charttype'] = [
402
			'message' => 'srf-paramdesc-charttype',
403
			'default' => 'bar',
404
			'values' => [ 'bar', 'line', 'donut', 'bubble', 'scatter' ],
405
		];
406
407
		$params['trendline'] = [
408
			'message' => 'srf-paramdesc-trendline',
409
			'default' => 'none',
410
			'values' => [ 'none', 'exp', 'linear' ],
411
		];
412
413
		$params['cursor'] = [
414
			'message' => 'srf-paramdesc-chartcursor',
415
			'default' => 'none',
416
			'values' => [ 'none', 'zoom', 'tooltip' ],
417
		];
418
419
		$params['gridview'] = [
420
			'message' => 'srf-paramdesc-gridview',
421
			'default' => 'none',
422
			'values' => [ 'none', 'tabs' ],
423
		];
424
425
		return $params;
426
	}
427
}