SRFjqPlotSeries   B
last analyzed

Complexity

Total Complexity 51

Size/Duplication

Total Lines 417
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 4
dl 0
loc 417
ccs 0
cts 165
cp 0
rs 7.92
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getName() 0 3 1
A getResultText() 0 17 2
C getResultData() 0 73 14
F getFormatSettings() 0 87 13
A getNumbersTicks() 0 20 4
F addResources() 0 46 12
A getFormatOutput() 0 44 4
A getParamDefinitions() 0 52 1

How to fix   Complexity   

Complex Class

Complex classes like SRFjqPlotSeries often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SRFjqPlotSeries, and based on these observations, apply Extract Interface, too.

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 mwjames
$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
0 ignored issues
show
Bug introduced by mwjames
There is no parameter named $result. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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 Jeroen De Dauw
$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,
0 ignored issues
show
Bug introduced by mwjames
The variable $series does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
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
0 ignored issues
show
Bug introduced by mwjames
There is no parameter named $data. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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
}