Updae Dataviz to merging with Roassal2-RTSunburst

Co-authored-by: yanekgil <yanekgil@gmail.com>
This commit is contained in:
akevalion 2016-04-07 16:35:40 +00:00 committed by SantiagoBragagnolo
parent 378a9d1094
commit f2ccfd15cc
6 changed files with 1445 additions and 29 deletions

View File

@ -0,0 +1,18 @@
"
I store metadata for this package. These meta data are used by other tools such as the SmalllintManifestChecker and the critics Browser
"
Class {
#name : #ManifestDataviz,
#superclass : #PackageManifest,
#category : #Dataviz
}
{ #category : #'code-critics' }
ManifestDataviz class >> ruleRBSentNotImplementedRuleV1FalsePositive [
^ #(#(#(#RGClassDefinition #(#PublishedMedInfo)) #'2015-12-06T12:21:46.001583-05:00') )
]
{ #category : #'code-critics' }
ManifestDataviz class >> ruleRBUnclassifiedMethodsRuleV1FalsePositive [
^ #(#(#(#RGClassDefinition #(#MedAgency)) #'2015-12-17T23:52:38.455558-05:00') )
]

View File

@ -0,0 +1,64 @@
"
I'm used to help in the citizen oversight of public spending.
I load scrapped information of public Government sites (starting with
Colombian ones) and help in its understanding by providing visualizations and other techniques.
"
Class {
#name : #OpenSpending,
#superclass : #Object,
#instVars : [
'contractsData'
],
#category : #'Dataviz-OpenSpending'
}
{ #category : #accessing }
OpenSpending >> contractsData [
^ contractsData
]
{ #category : #accessing }
OpenSpending >> contractsData: anObject [
contractsData := anObject
]
{ #category : #'data import' }
OpenSpending >> listSectors [
"I import data from aFile storaged in Comma Separated Values (CSV) format.
Is supposed that they're taken from a query that contains ths contracting sectors
according to https://en.wikipedia.org/wiki/UNSPSC"
| queryResults dbSectorsClassifier |
dbSectorsClassifier := FileLocator documents / 'DataWeek' / 'clasificador.csv'.
queryResults := (RTTabTable new input: dbSectorsClassifier contents usingDelimiter: $,) values.
"table removeFirstRow.
self contractsData: table."
^ queryResults
]
{ #category : #'data import' }
OpenSpending >> loadDataFromCSV: aFile usingDelimiter: aCharacter [
"I import data from aFile storaged in Comma Separated Values (CSV) format.
Is supposed that they're taken from a query that sums the sectors of the economy"
| table |
table := RTTabTable new input: aFile contents usingDelimiter: aCharacter.
table removeFirstRow.
self contractsData: table.
]
{ #category : #'as yet unclassified' }
OpenSpending >> showTreeMap [
"I show a treemap visualization of my contract data"
| builder popup sectors subtotals |
popup := RTPopup new.
sectors := self contractsData valuesOfColumn: 1.
subtotals := self contractsData valuesOfColumn: 2.
popup text: [:index | (sectors at: index), String cr, (subtotals at: index) ].
builder := RTTreeMapBuilder new.
builder shape color: [:n | Color random].
builder from: (1 to: (self contractsData numberOfRows)) using: [#()].
builder weight: [:n | (self contractsData valuesOfColumn: 2) at: n ].
builder build.
builder view elements @ popup.
^ builder view.
]

View File

@ -0,0 +1,635 @@
"
Please comment me using the following template inspired by Class Responsibility Collaborator (CRC) design:
For the Class part: State a one line summary. For example, ""I represent a paragraph of text"".
I represent the published medicine information from several sources for
a particular medicine.
My main responsabilities are:
- Visualize published medicine information to help in comparing several
agencies.
- I know how much information is published for a particular medicine.
My main collaborator is:
- MedDataItem: I store several of them.
"
Class {
#name : #PublishedMedInfo,
#superclass : #Object,
#instVars : [
'medName',
'medDataItems',
'medDataKeys',
'medDataMatrix',
'adminDataSize',
'arcWidth',
'columnLabels'
],
#category : #'Dataviz-MedicineInfo'
}
{ #category : #utility }
PublishedMedInfo >> addLabelsFrom: anArray surrounding: aView withRadio: aDistance [
"I put a list of labels on aView which around a circle with a given radio"
| angle |
1 to: anArray size do: [ :index | |label|
angle := (index * 360/ anArray size) degreesToRadians.
label := RTLabel new
text: (anArray at: index) asString;
height: aDistance / 30.
aView
add: (label element translateBy: (Point r: (aDistance)* 1.06 theta: angle - 0.075))
].
^ aView.
]
{ #category : #utility }
PublishedMedInfo >> addLegendTo: aView titled: aString withData: anArray withColors: aColorPalette [
"Adds a legend with a given title to a given view"
| legend |
legend := RTLegendBuilder new.
legend addText: aString.
aView
add: legend.
]
{ #category : #utility }
PublishedMedInfo >> addLineSeparatorsTo: aView withData: data columnsDistance: aDistance centerSized: internalRadio ringSized: ringSize [
| e1 e2 ang |
1 to: data children size do: [ :i |
Transcript show: i.
e1 := (RTBox new size: 1) element.
ang := (i * 360 / data children size) degreesToRadians.
e1 translateTo: (internalRadio * ang cos)@(internalRadio * ang sin).
e2 := (RTBox new size: 1) element.
e2 translateTo: ((internalRadio + ringSize) * ang cos )@ ((internalRadio + ringSize)* ang sin).
aView
add: e1;
add: e2;
add: (RTLine new width: aDistance - 4 ; color: Color veryLightGray; edgeFrom: e1 to: e2)
]
]
{ #category : #utility }
PublishedMedInfo >> addRotatedLabelsFrom: anArray surrounding: aView withRadio: aDistance withAngularCorrection: aValue [
"I put a list of rotated labels on aView which around a circle with a given radio"
| angle |
1 to: anArray size do: [ :index | |label|
angle := ((index * 360/ anArray size) - aValue) negated degreesToRadians.
label := (RTRotatedLabel new
text: (anArray at: index) asString;
height: aDistance / 22) element.
label translateBy: (Point r: label trachelShape notRotatedWidth/2+(aDistance)*1.06 theta: angle).
aView add: label.
angle := angle radiansToDegrees.
angle := angle + ((angle between: -270 and: -90) ifTrue: [ 180 ] ifFalse: [ 0 ]).
label trachelShape angleInDegree: angle.
].
^ aView.
]
{ #category : #accessing }
PublishedMedInfo >> adminDataSize [
"I calculate the amount of administrative data in the matrix. By convention all administrative data starts with '*' in its name"
^ adminDataSize isNil
ifTrue: [
(self medDataKeys select: [:key | (key at: 1) = $*]) size
]
]
{ #category : #accessing }
PublishedMedInfo >> adminDataSize: anObject [
adminDataSize := anObject
]
{ #category : #utility }
PublishedMedInfo >> arcWidth [
^ arcWidth
]
{ #category : #accessing }
PublishedMedInfo >> arcWidth: anObject [
arcWidth := anObject
]
{ #category : #utility }
PublishedMedInfo >> colorForWithoutBiosimilar [
"Returns a customized color for a medicament which has not biosimilar in its published info"
^ Color r: 218/255 g: 223/255 b: 225/255
]
{ #category : #utility }
PublishedMedInfo >> colorPalette16 [
"Returns a custom color palette for administrative data. In the data matrix, this correspond
to the rows which name starts by '*' (first two admin data, contry and variant are already mapped in the name of each arc and don't
start with '*'"
| baseColors newColorPalette index |
baseColors := RTColorPalette qualitative
colors: 12
scheme:'Set3'.
index := 1.
newColorPalette := Array new: 16.
baseColors do: [ :color |
newColorPalette at: index put: color.
index := index + 1.
].
"Customizing some individual colors to get a more constrasted palette, useful to distinguis in
large information collections"
newColorPalette
at: 3 put: (Color r: 71/255 g:30/255 b: 253/255);
at: 8 put: (Color r: 134/255 g:43/255 b: 80/255);
at: 9 put: (Color r: 156/255 g:143/255 b: 50/255);
at: 11 put: (Color r: 164/255 g:44/255 b: 107/255);
at: 13 put: (Color r:0.75 g:0.22 b:0.17);
at: 14 put: (Color r:0.42 g:0.48 b:0.54);
at: 15 put: (Color r:0.83 g:0.33 b:0);
at: 16 put: (Color r: 211/255 g:26/255 b: 239/255).
^ newColorPalette.
]
{ #category : #utility }
PublishedMedInfo >> colorPalette26 [
"Returns a custom color palette for prescrition and use data. In the data matrix, this correspond
to the rows which name doesn't start by '*' (first two admin data, contry and variant are already mapped in the name of each arc and doesn't
start with '*'"
| baseColors newColorPalette index |
baseColors := RTColorPalette qualitative
colors: 20
scheme:'FlatUI1'.
index := 1.
newColorPalette := Array new: 26.
baseColors do: [ :color |
newColorPalette at: index put: color.
index := index + 1.
].
newColorPalette
at: 2 put: (Color r:0.65 g:0.22 b:0.23);
at: 4 put: (Color r:1 g:0.72 b:0.1);
at: 6 put: (Color r:255/255 g:230/255 b:141/255);
at: 8 put: (Color r:178/255 g:24/255 b:17/255);
at: 14 put: (Color r:255/255 g:214/255 b:208/255);
at: 18 put: (Color r:218/255 g:0/255 b:132/255);
at: 21 put: (Color r:223/255 g:93/255 b:255/255);
at: 22 put: (Color r:0.29 g:0.47 b:0.75);
at: 23 put: (Color r:0.64 g:0.87 b:0.82);
at: 24 put: (Color r:131/255 g:178/255 b:10/255);
at: 25 put: (Color r:0.91 g:0.83 b:038);
at: 26 put: (Color r:0.42 g:0.48 b:054).
^ newColorPalette.
]
{ #category : #accessing }
PublishedMedInfo >> columnLabels [
^ columnLabels
]
{ #category : #accessing }
PublishedMedInfo >> columnLabels: anObject [
columnLabels := anObject
]
{ #category : #'data visualization' }
PublishedMedInfo >> exploreDataForCountriesFromRow: firstRowIndex upTo: lastRowIndex coloredWith: aColorPalette centerTitled: aString arcWidth: aNumber [
"I show a subset of the matrix rows using a sunburst visualization variation"
self arcWidth: aNumber.
^ self
exploreMatrix: (self medDataMatrix copyFrom: firstRowIndex to: lastRowIndex)
by: 'country'
coloredWith: aColorPalette
centerSized: 200
separationSized: 5
slicedFrom: firstRowIndex
centerTitled: aString
]
{ #category : #'data visualization' }
PublishedMedInfo >> exploreMatrix: aMatrix by: type coloredWith: aColorPalette centerSized: centerSize separationSized: separationSize slicedFrom: anInteger centerTitled: aString [
"I create a sunburst diagram for matrix alike information using aPalette for each of the rings on the sunburst.
Information of the popup is taken according to the type of exploration that is being done in the matrix, which can
done by the country which is publishing the info or by properties that are bein published.
This code has a lot of parameters and maybe needs serious refactoring"
| b data externalCircle internalCircle medLabel index |
data := self matrixToTreeReversed: aMatrix.
b := RTSunburstBuilder new.
b strategy
centerWidth: centerSize;
hasCenter: false;
arcWidth: self arcWidth;
radialSpacing: 50.
internalCircle := RTEllipse new
width: centerSize*2; height: centerSize*2;
color: Color white trans;
borderColor: Color veryLightGray;
borderWidth: 1.
externalCircle := RTEllipse new
width: internalCircle element width + (2 * aMatrix size * (self arcWidth + 1));
height: internalCircle element width + (2 * aMatrix size * (self arcWidth + 1));
color: Color white trans;
borderColor: Color veryLightGray;
borderWidth: 1.
b view add: externalCircle element.
b view add: internalCircle element.
medLabel := RTLabel new
text: aString, String cr, self medName;
color: Color black;
height: (internalCircle element width)/20 asInteger.
b view add: (medLabel element translateBy: (internalCircle element center)).
b radialSpacing: 1.
index := 1.
b shape color: [ :node |
node header asString size > 1
ifTrue: [ Color black ]
ifFalse: [
node header asInteger isZero
ifTrue: [ Color white ]
ifFalse: [ node header asInteger = 2
ifTrue: [ self colorForWithoutBiosimilar ]
ifFalse: [ aColorPalette at: node level ]
]
]
].
b interaction noInteractions.
type asLowercase = 'country' ifTrue: [
b interaction
addInteraction:
(RTPopup text: [:d | (self medDataKeys at: ((aMatrix size) - d level + anInteger + 1)), '-> ', d header]).
].
type asLowercase = 'property' ifTrue: [
b interactions
add:
(RTPopup text: [:d | self labelsForCountries at: (d level - 1)]).
].
b explore: data using: #children.
b strategy hasCenter: false.
b build.
b view @ RTZoomableBoxView.
self
addLineSeparatorsTo: b view
withData: data
columnsDistance: separationSize
centerSized: (internalCircle element width / 2)
ringSized: ((externalCircle element width/2) - (internalCircle element width/2)).
^ {'theView' -> b view . 'externalRadio' -> (externalCircle element width/2)} asDictionary.
]
{ #category : #utility }
PublishedMedInfo >> labelsForAdminProperties [
"I got the information about the administrative properties that are being published by
all countries public health agencies.
This information is a subset of medDataKeys"
^ self medDataKeys copyFrom: 3 to: self adminDataSize + 2.
]
{ #category : #utility }
PublishedMedInfo >> labelsForCountries [
"I got the information about the country who is publishing the medicine info.
This information is stored in the first row of the medDataMatrix.
Countries use the ISO 3166-1 alpha 3 standard."
| labels allCountries |
allCountries := (self medDataMatrix at: 1).
labels := OrderedCollection new.
1 to: allCountries size by: 2 do: [:index |
labels add: (allCountries at: index).
].
^ labels reversed.
]
{ #category : #utility }
PublishedMedInfo >> labelsForCountriesAndVariants [
"I got the information about the country and variant of medicine info that is being published by a
country public health agency.
This information is stored in the first two rows of the medDataMatrix.
Countries use the ISO 3166-1 alpha 3 standard and for the medicine variant we use complete words
in the CVS but select only the first letter for visualization purposes, so the first letter is
supposed to be different on each variant."
| labels countries variants |
countries := self medDataMatrix at: 1.
variants := self medDataMatrix at: 2.
labels := Array new: countries size.
1 to: countries size do: [:index |
labels
at: index
put: (countries at: (countries size + 1 - index)),
'-',
(((variants at: (variants size + 1 - index)) copyFrom: 1 to: 1) asLowercase)
].
^ labels
]
{ #category : #utility }
PublishedMedInfo >> labelsForPUProperties [
"I got the information about the properties that are being published by all countries public health agencies.
This information is a subset of medDataKeys"
^ self medDataKeys copyFrom: 3 + self adminDataSize to: self medDataKeys size.
]
{ #category : #'data import' }
PublishedMedInfo >> loadDataFromCSV: aFile usingDelimiter: aCharacter [
"I import data from aFile storaged in Comma Separated Values (CSV) format.
Is supposed that:
- First row contains medicine name information.
- First column contains the attributes that are being evaluated
- Other columns contain the result of such evaluation as numerical values"
| table |
table := RTTabTable new input: aFile contents usingDelimiter: aCharacter.
self medName: ((table valuesOfColumn: 2) at: 1).
table removeFirstRow.
self medDataKeys: table firstColumn.
table removeColumn: 1.
self medDataMatrix: table values.
]
{ #category : #utility }
PublishedMedInfo >> matrixDataForAdmin [
"I extract all the administrative information from the medDataMatrix as a transposed matrix, so it can be used
as an input for the matrixSunburst visualizations.
By convention, this data is stored after the admin data for each drug has ended"
| submatrixForAdmin numberOfColums transposedMatrix transposedRow |
submatrixForAdmin := self medDataMatrix copyFrom: 3 to: (self adminDataSize + 2).
numberOfColums := (submatrixForAdmin at: 1) size.
transposedMatrix := OrderedCollection new.
1 to: (numberOfColums) by: 2 do: [ :columIndex |
transposedRow := Array new: submatrixForAdmin size.
1 to: submatrixForAdmin size do: [:index |
transposedRow at: index put: ((submatrixForAdmin at: index) at: columIndex)
].
transposedMatrix add: transposedRow
].
^ transposedMatrix asArray
]
{ #category : #utility }
PublishedMedInfo >> matrixDataForPU [
"I extract all the prescription and use data from the medDataMatrix as a transposed matrix, so it can be used
as an input for the matrixSunburst visualizations.
By convention, this data is stored after the admin data for each drug has ended"
| submatrixForPU numberOfColums transposedMatrix transposedRow |
submatrixForPU := self medDataMatrix copyFrom: 3 + self adminDataSize to: self medDataMatrix size.
numberOfColums := (submatrixForPU at: 1) size.
transposedMatrix := OrderedCollection new.
1 to: (numberOfColums) by: 2 do: [ :columIndex |
transposedRow := Array new: submatrixForPU size.
1 to: submatrixForPU size do: [:index |
transposedRow at: index put: ((submatrixForPU at: index) at: columIndex)
].
transposedMatrix add: transposedRow
].
^ transposedMatrix asArray
]
{ #category : #'data visualization' }
PublishedMedInfo >> matrixSunburstForAdminDataByCountry [
"Shows a matrix sunburst for the drug administrative data in several public drug agencies, using the
properties of the medicine as tows and the countries that release the info (or not) as columns.
By convention, this data starts at the 3rd row and ends in 2 + self adminDataSize."
| temp view radio |
temp := self
exploreDataForCountriesFromRow: 3
upTo: 1 + self adminDataSize
coloredWith: self colorPalette16
centerTitled: ''
arcWidth: 15.
view := temp at: 'theView'.
radio := temp at: 'externalRadio'.
^ self
addLabelsFrom: self labelsForCountriesAndVariants
surrounding: view
withRadio: radio*1.05.
]
{ #category : #'data visualization' }
PublishedMedInfo >> matrixSunburstForAdminDataByProperty [
"Shows a matrix sunburst for the drug administrative data in several public drug agencies, using the
properties of the medicine as columns and the countries as rows."
| temp view radio |
temp := self
exploreMatrix: self matrixDataForAdmin
by: 'property'
coloredWith: self colorPalette16
centerSized: 200
separationSized: 6
slicedFrom: nil
centerTitled: ''.
view := temp at: 'theView'.
radio := temp at: 'externalRadio'.
^ self
addRotatedLabelsFrom: self labelsForAdminProperties
surrounding: view
withRadio: radio * 1.05
withAngularCorrection: 14.5
]
{ #category : #'data visualization' }
PublishedMedInfo >> matrixSunburstForAdminDataWithColorConvention [
"Shows a matrix sunburst for the drug administrative data in several public drug agencies"
| composer sunburst |
composer := RTComposer new.
sunburst := self matrixSunburstForAdminDataByCountry.
composer group: #sunburst.
composer view.
]
{ #category : #'data visualization' }
PublishedMedInfo >> matrixSunburstForPUDataByCountry [
"Shows a matrix sunburst for the drug prescripton and use data in several public drug agencies.
For convention, this data stats at the row 3 + self adminDataSize and ends at the last one of the matrix"
| temp view radio |
temp:= self
exploreDataForCountriesFromRow: 3 + self adminDataSize
upTo: self medDataKeys size
coloredWith: self colorPalette26
centerTitled: ''
arcWidth: 15.
view := temp at: 'theView'.
radio := temp at: 'externalRadio'.
^ self
addLabelsFrom: self labelsForCountriesAndVariants
surrounding: view
withRadio: radio*1.05.
]
{ #category : #'data visualization' }
PublishedMedInfo >> matrixSunburstForPUDataByProperty [
"Shows a matrix sunburst for the drug administrative data in several public drug agencies, using the
properties of the medicine as columns and the countries as rows."
| temp view radio |
temp := self
exploreMatrix: self matrixDataForPU
by: 'property'
coloredWith: self colorPalette16
centerSized: 200
separationSized: 6
slicedFrom: nil
centerTitled: ''.
view := temp at: 'theView'.
radio := temp at: 'externalRadio'.
^ self
addRotatedLabelsFrom: self labelsForPUProperties
surrounding: view
withRadio: radio * 1.05
withAngularCorrection: 9
]
{ #category : #utility }
PublishedMedInfo >> matrixToTree: aMatrix [
"Converts aMatrix to a tree so it can be visualized using a sunburst"
| currentNode tree column |
tree := GrafoscopioNode new level: 1.
(aMatrix at: 1) do: [ :label |
tree addNode: (GrafoscopioNode new header: label)
].
column := 1.
tree children do: [ :child |
currentNode := child.
2 to: (aMatrix size) do: [:row |
currentNode addNode: (GrafoscopioNode new header: ((aMatrix at: row) at: column)).
currentNode := currentNode children at: 1.
].
column := column + 1.
].
^ tree.
]
{ #category : #utility }
PublishedMedInfo >> matrixToTreeReversed: aMatrix [
"Converts aMatrix to a tree so it can be visualized using a sunburst.
Root of the tree is near to the last row, so the most exterior rings correspond to first rows,
and inner most ones correspond to the last rows"
| currentNode tree column |
tree := GrafoscopioNode new level: 1.
(aMatrix at: aMatrix size) do: [ :label |
tree addNode: (GrafoscopioNode new header: label)
].
column := 1.
tree children do: [ :child |
currentNode := child.
(aMatrix size -1 ) to: 1 by: -1 do: [:row |
currentNode addNode: (GrafoscopioNode new header: ((aMatrix at: row) at: column)).
currentNode := currentNode children at: 1.
].
column := column + 1.
].
^ tree.
]
{ #category : #accessing }
PublishedMedInfo >> medDataItems [
^ medDataItems
]
{ #category : #accessing }
PublishedMedInfo >> medDataItems: anObject [
medDataItems := anObject
]
{ #category : #accessing }
PublishedMedInfo >> medDataKeys [
^ medDataKeys
]
{ #category : #accessing }
PublishedMedInfo >> medDataKeys: anObject [
medDataKeys := anObject
]
{ #category : #accessing }
PublishedMedInfo >> medDataMatrix [
^ medDataMatrix
]
{ #category : #accessing }
PublishedMedInfo >> medDataMatrix: anObject [
medDataMatrix := anObject
]
{ #category : #accessing }
PublishedMedInfo >> medName [
^ medName
]
{ #category : #accessing }
PublishedMedInfo >> medName: anObject [
medName := anObject
]
{ #category : #'data visualization' }
PublishedMedInfo >> showAdminDataColorConventions [
"I create a legend showing the color palette and properties used in the matrix sunburst for administrative drug data"
| legendBox adminDataKeys |
legendBox := RTLegendBuilder new.
legendBox addText: 'Convenciones' asUppercase.
adminDataKeys := self medDataKeys copyFrom: 3 to: self adminDataSize + 1.
1 to: adminDataKeys size do: [ :i |
legendBox addColor: (self colorPalette16 at: (self colorPalette16 size + 1 - i)) text: ((adminDataKeys at: i) copyReplaceAll: '* ' with: '').
].
legendBox addColor: self colorForWithoutBiosimilar text: 'Sin biosimilar aprobado'.
legendBox addColor: Color white text: 'Sin informacion publicada'.
legendBox build.
^ legendBox view
]
{ #category : #'data visualization' }
PublishedMedInfo >> showCountryColorConventions [
"I create a legend showing the color palette and countries used in the matrix sunburst for prescription drug data"
| legendBox |
legendBox := RTLegendBuilder new.
legendBox addText: 'Convenciones' asUppercase.
self labelsForCountries size to: 1 by: -1 do: [ :i |
legendBox addColor: (self colorPalette16 at: i + 1) text: (self labelsForCountries at: i).
].
legendBox addColor: self colorForWithoutBiosimilar text: 'Sin biosimilar aprobado'.
legendBox addColor: Color white text: 'Sin informacion publicada'.
legendBox build.
^ legendBox view
]
{ #category : #'data visualization' }
PublishedMedInfo >> showPUDataColorConventions [
"I create a legend showing the color palette and properties used in the matrix sunburst for prescription drug data.
By convention this data starst where admin data ends and two first rows are 'special' admin data: name and variant of
medicament which are already mapped on"
| legendBox |
legendBox := RTLegendBuilder new.
legendBox addText: 'Convenciones' asUppercase.
1 to: (self medDataKeys size) - (self adminDataSize + 2) do: [ :i |
legendBox addColor: (self colorPalette26 at: (self colorPalette26 size + 1 - i)) text: ((self medDataKeys at: self adminDataSize + 2 + i) copyReplaceAll: '* ' with: '').
].
legendBox addColor: self colorForWithoutBiosimilar text: 'Sin biosimilar aprobado'.
legendBox addColor: Color white text: 'Sin informacion publicada'.
legendBox build.
^ legendBox view
]

View File

@ -11,7 +11,11 @@ Class {
'date',
'message',
'profile',
'url'
'url',
'mentions',
'links',
'hashtags',
'type'
],
#category : #'Dataviz-Twitter'
}
@ -26,6 +30,46 @@ Tweet >> date: anObject [
date := anObject
]
{ #category : #accessing }
Tweet >> hashtags [
^ hashtags
]
{ #category : #accessing }
Tweet >> hashtags: anObject [
hashtags := anObject
]
{ #category : #'data scrapping' }
Tweet >> impact [
"Computes the maximum amount of followers that a tweet can reach, including the retweets.
It doesn't take into account that retweeters can share followers and in future versions
this should be computed, even if we don't have a full list of followers.
Also the computing time could be sustancially improved by extracting only followers data
from the profiles instead of scrapping all and by having a better cache which includes
prescrapped data.
'Real time' is not the main issue here, but 'slow data'.
For example we could include snapshots of the retweeters profiles and to aks for a
commented retweet to reach new followers"
| retweetsHtml retweetsTree retweeters impactData |
retweetsHtml := ZnClient new get: 'http://mutabit.com/deltas/repos.fossil/datapolis/doc/tip/Data/Sources/Single/', ((self url splitOn: '//') at: 2), '/retweets.html'.
retweetsTree := Soup fromString: retweetsHtml.
retweeters := (retweetsTree findAllTagsByClass: 'username')
collect: [:each | each text copyReplaceAll: '@' with: ''].
impactData := Dictionary new.
impactData
at: 'retweeters' put: retweeters size;
at: 'reach' put: (retweeters collect: [:each | (TwitterProfile new scrapDataForProfile: each) followers]) sum.
^ impactData.
]
{ #category : #'data scrapping' }
Tweet >> impactFor: aTweetUrl [
"Computes the maximum amount of followers that a tweet can reach, including the retweets.
@ -51,7 +95,7 @@ Tweet >> impactFor: aTweetUrl [
impactData
at: 'retweeters' put: retweeters size;
at: 'reach' put: (retweeters collect: [:each | (TwitterProfile new scrapDataFromProfile: each) followers]) sum.
at: 'reach' put: (retweeters collect: [:each | (TwitterProfile new scrapFollowersForProfile: each)]) sum.
^ impactData.
]
@ -94,6 +138,62 @@ RTNest new on: background nest: els.
^ background
]
{ #category : #accessing }
Tweet >> links [
^ links
]
{ #category : #accessing }
Tweet >> links: anObject [
links := anObject
]
{ #category : #accessing }
Tweet >> mentions [
^ mentions
]
{ #category : #accessing }
Tweet >> mentions: anObject [
mentions := anObject
]
{ #category : #'data visualization' }
Tweet >> mentionsClusterSeparated: aDistance inView: aView [
"I return a graphic of all avatars mentioned in a tweet, layered as a cluster a separated
by aDistance"
| container avatars profiles |
"Profiles: Create profiles collecting information for all users mentioned in the tweet"
profiles := OrderedCollection new.
self mentions do: [:eachMention |
profiles add: (TwitterProfile new scrapDataForProfile: eachMention)].
"Avatars: Use the profiles to create a collection of avatar images which are draggable
elements on a view"
avatars := OrderedCollection new.
profiles do: [:eachProfile |
avatars add: (RTBitmap new form: eachProfile avatar) element ].
RTCircleLayout new
initialRadius: (avatars size)*aDistance;
on: avatars.
container := (RTBox new color: Color transparent) element .
RTNest new on: container nest: avatars.
"Adding avatars and its labels"
aView addAll: avatars.
aView add: container.
1 to: (avatars size) do: [:i |
(avatars at: i) @ (RTLabelled new
text: (('@',(self mentions at: i)));
fontSize: 35;
color: (Color black))].
^ container
]
{ #category : #accessing }
Tweet >> message [
^ message
@ -114,33 +214,199 @@ Tweet >> profile: anObject [
profile := anObject
]
{ #category : #'data scrapping' }
Tweet >> scrapDataFromUrl: aTweetUrl [
"Scraps most of the data in the page of a aTweetUrl. Most of the tweets are prestored now, but in the future
it will combine prestored data and live streaming data"
| tweetSourceHtml tweetSourceTree hour dateTmp day month year|
tweetSourceHtml := ZnClient new get:
'http://mutabit.com/deltas/repos.fossil/datapolis/doc/tip/Data/Sources/Single/',
((aTweetUrl splitOn: '//') at: 2), '/tweet.html'.
tweetSourceTree := Soup fromString: tweetSourceHtml.
self url: aTweetUrl .
self profile: ((aTweetUrl splitOn: '/') at: 4).
self message: ((((tweetSourceTree findAllTagsByClass: 'tweet') at: 1) children at: 6) children at: 2).
self mentions: ((self message findAllTagsByClass: 'twitter-atreply')
collect: [:each | (each attributeAt: 'href') copyReplaceAll: '/' with: '']).
self hashtags: ((self message findAllTagsByClass: 'twitter-hashtag') collect: [:each | each text]).
self links: ((self message findAllTagsByClass: 'twitter-timeline-link')
collect: [:each | each attributeAt: 'data-url']).
hour := ((((tweetSourceTree findAllTagsByClass: 'tweet') at: 1) children at: 8) children at: 2).
dateTmp := (((((tweetSourceTree findAllTagsByClass: 'tweet') at: 1) children at: 8) children at: 4) text) splitOn: ' '.
day := dateTmp at: 3.
month := dateTmp at: 4.
year := '20', (dateTmp at: 5).
self date: (year, month, day, hour text, hour nextSibling text) asDateAndTime.
]
{ #category : #'data visualization' }
Tweet >> silenceMap [
"Creates a visualization of how long a tweet has not been answered (any kind of answer: not favs, not RT, no nothing), as
'circles of silence' "
Tweet >> showInView: aView [
"shows the general information (author, author's avatar and text) of the tweet in a view"
| v circle ex1 ex2 line retweets reach diameter radious origin |
| tweetPanel tweetText holder avatar |
"Tweet text"
(self message text size > 50)
ifTrue:[
tweetText := RTLabel new
text: (self splitByLines at: 1), String cr,
(self splitByLines at: 2), String cr, String cr,
(self date asString)]
ifFalse: [tweetText := RTLabel new
text: (self splitByLines at: 1), String cr, String cr,
(self date asString) ].
tweetText height: 35.
tweetText := tweetText element.
"Avatar"
avatar := (RTBitmap new form:
((TwitterProfile new scrapDataForProfile: self profile) avatar)) element.
avatar translateBy: (-30*(30)@0).
"Putting tweet elements together"
tweetPanel := OrderedCollection new.
tweetPanel add: avatar; add: tweetText.
holder := (RTBox new color: Color transparent) element @ RTDraggable.
RTNest new on: holder nest: tweetPanel.
"Adding elements and labels to the view"
aView add: tweetText.
aView add: avatar.
aView add: holder.
avatar @ (RTLabelled new
text: '@', (self profile);
fontSize: 35;
color: Color black;
top;
offsetOnEdge: 1 ).
^ holder.
]
diameter := 60.
origin := (100@100).
radious := diameter / 2.
v := RTView new.
{ #category : #'data visualization' }
Tweet >> showInView: aView sized: aSize [
"shows the general information (author, author's avatar and text) of the tweet in a view"
circle := (RTEllipse new size: diameter; borderColor: Color black; color: Color transparent) element.
ex1 := RTBox element.
ex2 := RTBox element.
circle translateTo: origin.
ex1 translateTo: ((radious negated @ 0) + circle center).
ex2 translateTo: (radious @ 0) + circle center.
line := (RTLine new; color: Color black) edgeFrom: ex1 to: ex2.
v add: circle; add: line.
| tweetPanel tweetText holder avatar |
"Tweet text"
(self message text size > 50)
ifTrue:[
tweetText := RTLabel new
text: (self splitByLines at: 1), String cr,
(self splitByLines at: 2), String cr, String cr,
(self date asString)]
ifFalse: [tweetText := RTLabel new
text: (self splitByLines at: 1), String cr, String cr,
(self date asString) ].
tweetText height: aSize.
tweetText := tweetText element.
"Avatar"
avatar := (RTBitmap new form:
((TwitterProfile new scrapDataForProfile: self profile) avatar)) element.
avatar translateBy: (-30*aSize@0).
"Putting tweet elements together"
tweetPanel := OrderedCollection new.
tweetPanel add: avatar; add: tweetText.
holder := (RTBox new color: Color transparent) element @ RTDraggable.
RTNest new on: holder nest: tweetPanel.
"Adding elements and labels to the view"
aView add: tweetText.
aView add: avatar.
aView add: holder.
avatar @ (RTLabelled new
text: '@', (self profile);
fontSize: 35;
color: Color black;
top;
offsetOnEdge: 1 ).
^ holder.
]
retweets := (RTLabel text: 5) element.
reach := (RTLabel text: '3.1k') element.
v add: retweets; add: reach.
retweets translateTo: (0 @ -12) + circle center.
reach translateTo: (0 @ 12) + circle center.
^ v.
{ #category : #'data visualization' }
Tweet >> silenceMapFor: arg1 [
| tmp1 tmp2 tmp3 tmp4 tmp5 tmp6 tmp7 tmp8 tmp9 tmp11 tmp13 tmp15 tmp17 tmp19 tmp21 |
self scrapDataFromUrl: arg1.
tmp5 := (Date today - self date) days.
tmp7 := self impactFor: arg1.
tmp11 := RTBox new.
tmp11 color: Color red.
tmp3 := tmp11 size: 200.
tmp3 := tmp3 element.
tmp3 translateBy: (tmp5 * -50) @ 0.
tmp1 := RTView new.
tmp6 := self mentionsClusterSeparated: 70 inView: tmp1.
tmp2 := self showInView: tmp1 sized: 35.
tmp2 translateBy: (tmp5 * -25) @ -600.
tmp1 add: tmp3.
tmp4 := RTEdge from: tmp6 to: tmp3.
tmp13 := RTGradientColoredLine new.
tmp13
colors: (Array with: (Color white alpha: 0.3) with: (Color red alpha: 0.9));
precision: 100;
width: 20.
tmp1 add: tmp4 + tmp13 gradientColorShape.
tmp15 := RTLabel new.
tmp15 text: tmp5 asString , ' días sin respuesta'.
tmp8 := (tmp15 height: tmp5 * 2.5) element @ RTDraggable.
tmp8 translateBy: (tmp5 * -25) @ -105.
tmp17 := RTLabel new.
tmp17
text: 'al ' , Date today asString , ' y contando...';
height: tmp5 * 1.
tmp9 := (tmp17 color: Color gray) element @ RTDraggable.
tmp9 translateBy: (tmp5 * -15) @ 50.
tmp1
add: tmp8;
add: tmp9.
tmp19 := RTLabelled new.
tmp19
text: (tmp7 at: 'retweeters') asString , ' retweets';
fontSize: tmp5 * 2.
tmp3 @ (tmp19 color: Color gray).
tmp21 := RTLabelled new.
tmp21
text: (tmp7 at: 'reach') asString , ' lectores' , String cr , '(max)';
fontSize: tmp5 * 2;
color: Color gray.
tmp3 @ tmp21 below.
tmp1 view canvas focusOnCenterScaled.
^ tmp1 @ RTDraggableView
]
{ #category : #'as yet unclassified' }
Tweet >> splitByLines [
"Splits the message text of the tweet in lines, in case of being too long for a better rendering
in silenceMap and other views."
| summa i charLimit words wordSizes lines |
words := self message text splitOn: ' '.
wordSizes := words collect: [:eachWord | eachWord size].
i := 1.
summa := 0.
charLimit := 50.
[summa < charLimit] whileTrue: [
summa := (wordSizes at: i) + summa.
i := i +1].
lines := OrderedCollection new.
lines add: (Character space join: (words copyFrom: 1 to: i)).
lines add: (Character space join: (words copyFrom: i + 1 to: words size)).
^ lines
]
{ #category : #accessing }
Tweet >> type [
^ type
]
{ #category : #accessing }
Tweet >> type: anObject [
type := anObject
]
{ #category : #accessing }

View File

@ -0,0 +1,345 @@
"
Please comment me using the following template inspired by Class Responsibility Collaborator (CRC) design:
I'm Tweets a helper class to work with collections of tweet objects.
For the Responsibility part: Three sentences about my main responsibility, what I'm doing, what services do I offer.
For the Collaborators Part: State my main collaborators and one line about how I interact with them.
Public API and Key Messages
- message one
- message two
- what is the way to create instances is a plus.
One simple example is simply gorgeous.
Internal Representation and Key Implementation Points.
Instance Variables
tweets: <Object>
Implementation Points
"
Class {
#name : #TweetsCollection,
#superclass : #Object,
#instVars : [
'tweets'
],
#category : #'Dataviz-Twitter'
}
{ #category : #'data visualization' }
TweetsCollection >> activityHistogramFor: aProfileName in: aDataBaseFile [
"I draw a histogram of the tweeter activity for a given profile name with data stored in aDataBaseFile.
The database stores the individual tweets for this profile, with their type (tweet, retweet or reply),
unique url and date.
A proper schema of the data base still needs to be published.
Is the one used in all references to aDataBaseFile."
| sample activityDataArray monthOfFirstTweet activityDataCollection histogramData plot |
sample := TweetsCollection new.
activityDataArray := sample monthlyActivityDataFor: aProfileName in: aDataBaseFile.
(activityDataArray size > 0)
ifFalse: [
self inform: 'There is no data for ', aProfileName, ' in the database'
]
ifTrue:[
"Detecting where happened the first tweet and storing only retweets over this value, will delete the
outliers which correspond to retweets of original tweets far away of the period of the sample"
monthOfFirstTweet := activityDataArray detect: [ :each | (each value at: 1) > 0].
activityDataCollection := OrderedCollection new.
activityDataArray do: [ :each |
(each key >= monthOfFirstTweet key) ifTrue: [
activityDataCollection add: {
each key asString .
each value at: 1 .
each value at: 2 .
each value at: 3 }
]
].
"This part was adapted from the awesome roassal examples"
plot := RTGrapher new.
histogramData := RTMultipleData new.
histogramData barShape color: Color green.
histogramData
points: activityDataCollection;
addMetric: #second;
addMetric: #third;
addMetric: #fourth.
"Horizontal text"
"d barChartWithBarCenteredTitle: #first."
"Rotated text with integer axis"
histogramData barChartUsing: (RTBarLabelFactory new label: #first; fontSize: 7).
plot add: histogramData.
plot axisY noDecimal.
^ plot
]
]
{ #category : #'data queries' }
TweetsCollection >> importTweetsFromJSONFile: aJSONFile [
"I import all the tweets for aJSONFile and convert them in tweets inside a TweetCollection"
| stream truncated jsonData currentTweet |
stream := aJSONFile readStream.
"We need to truncate the original file to quite the first line, which is the name of the exported array, so NeoJSONReader doesn't complain"
truncated := WriteStream on: String new.
stream contents lines allButFirstDo: [ :each | truncated nextPutAll: each ].
jsonData := NeoJSONReader fromString: truncated contents asString.
jsonData do: [:each |
currentTweet := Tweet new.
currentTweet
message: (each at: 'text');
profile: ((each at: 'user') at: 'screen_name');
date: ((each at: 'created_at') copyFrom: 1 to: 19) asDateAndTime.
"Detecting the kind of message and processing accordingly"
(each keys includes: 'in_reply_to_status_id')
ifTrue: [
currentTweet
type: 'reply';
url: '/', (currentTweet profile, '/status/', (each at: 'id_str'))]
ifFalse: [(each keys includes: 'retweeted_status')
ifTrue: [
currentTweet
type: 'retweet';
url: '/',
(((each at: 'retweeted_status') at: 'user') at: 'screen_name'),
'/status/',
((each at: 'retweeted_status') at: 'id_str').
]
ifFalse: [
currentTweet
type: 'tweet';
url: '/', (currentTweet profile, '/status/', (each at: 'id_str')) ]
].
"Detecting hashtags"
"(((each at: 'entities') at: 'hashtags') size > 0)
ifTrue: [
(each at: 'entities') at: 'hashtags'
]."
self tweets add: currentTweet.
].
]
{ #category : #'data queries' }
TweetsCollection >> loadTweetsFor: aProfileName from: aDataBaseFile [
"I select all the tweets for aProfileName in a given database"
| db queryResults temporalTweet |
"openning connection"
db := NBSQLite3Connection on: aDataBaseFile.
db open.
"Querying the data base"
queryResults := (db execute: 'select * from tweets where profile="',aProfileName,'";') rows.
db close.
queryResults do: [ :each |
temporalTweet := Tweet new.
temporalTweet
url: (each at: 'url');
date: (TimeStamp fromUnixTime: (each at: 'date')) asUTC;
type: (each at: 'type');
message: (each at: 'message');
profile: (each at: 'profile').
self tweets add: temporalTweet
].
]
{ #category : #'data queries' }
TweetsCollection >> monthlyActivityDataFor: aProfileName in: aDataBaseFile [
"I present a histogram of the tweets that differenciates tweets, retweets and replies,
for a given profile in a given SQLite database (for the moment I supposse that the profile exist there
and data base schema is correct)"
| db queryResults firstMonth lastMonth currentMonth activityCalendar monthOfFirstTweet |
"openning connection"
db := NBSQLite3Connection on: aDataBaseFile.
db open.
"Querying the data base"
db execute: 'create temporary table profile_tweets as select * from tweets where profile="',aProfileName,'";'.
queryResults := (db execute:
'SELECT strftime("%Y-%m",datetime(date, "unixepoch","localtime")) as month, type, count(*) as amount
FROM profile_tweets GROUP BY strftime("%Y-%m",datetime(date, "unixepoch","localtime")), type;') rows.
db execute: 'drop table if exists profile_tweets;'.
db close.
activityCalendar := Dictionary new.
(queryResults size > 0)
ifFalse: [
self inform: 'There is no data for that profile in the database'
]
ifTrue: [
"Detecting where happened the first tweet and storing only retweets over this value, will delete the
outliers which correspond to retweets of original tweets far away of the period of the sample"
monthOfFirstTweet := queryResults detect: [ :each | (each at: 'month') notNil].
firstMonth := ((monthOfFirstTweet at: 'month'), '-01') asDate asMonth.
lastMonth := ((queryResults last at: 'month'), '-01') asDate asMonth.
currentMonth := firstMonth.
[ currentMonth = (lastMonth next)]
whileFalse:[
activityCalendar at: (currentMonth) put: { 0 . 0 . 0 }.
currentMonth := currentMonth next.
].
queryResults do: [ :each |
(each at: 'type') = 'tweet' & ((each at: 'month') notNil) ifTrue: [
currentMonth := ((each at: 'month'), '-01') asDate asMonth.
(activityCalendar at: currentMonth)
at: 1
put: (((activityCalendar at: currentMonth) at: 1) + (each at: 'amount'))
].
(each at: 'type') = 'retweet' ifTrue: [
currentMonth := ((each at: 'month'), '-01') asDate asMonth.
(activityCalendar at: currentMonth)
at: 2
put: (((activityCalendar at: currentMonth) at: 2) + (each at: 'amount'))
].
(each at: 'type') = 'reply' ifTrue: [
currentMonth := ((each at: 'month'), '-01') asDate asMonth.
(activityCalendar at: currentMonth)
at: 3
put: (((activityCalendar at: currentMonth) at: 3) + (each at: 'amount'))
]
].
].
^ (activityCalendar associations sorted)
]
{ #category : #'data storage / persistance' }
TweetsCollection >> populateDataBase: aDataBaseFile [
"I populate a SQLite database file with my tweets data"
| db |
"openning connection"
db := NBSQLite3Connection on: aDataBaseFile.
db open.
"Creating the data base tweets schema"
db execute:
'create table if not exists tweets (
url text primary key,
profile text,
date integer,
type text,
message text
);'.
"Populating the database"
self tweets do: [:each |
db execute: 'INSERT INTO tweets values (?, ?, ?, ?, ?);'
with: {
each url .
each profile .
each date ifNotNil: [each date asUnixTime asString ].
each type .
each message }.
].
db close.
]
{ #category : #'data visualization' }
TweetsCollection >> ringOverview [
"I present a overview of the tweets as a ring that differenciates tweets, retweets and replies"
| totalTweets replies retweets ring |
replies := 0.
retweets := 0.
tweets do: [ :each |
(each type = 'reply') ifTrue: [replies := replies + 1].
(each type = 'retweets') ifTrue: [retweets := retweets + 1]].
totalTweets := (self tweets size) - replies - retweets.
ring := RTPieBuilder new.
ring interaction popup.
ring shape current
innerRadius: 80;
externalRadius: 100.
ring objects: {totalTweets . retweets . replies}.
(ring slice: #value)ifNotNil: [ :group |
group do: [:each | each @ (RTDraggable groupToDrag: group)]
].
ring normalizer distinctColor.
ring build.
^ ring.
]
{ #category : #'data visualization' }
TweetsCollection >> ringOverviewFor: aProfileName in: aDataBaseFile [
"I present a overview of the tweets as a ring that differenciates tweets, retweets and replies,
for a given profile in a given SQLite database (for the moment I supposse that the profile exist there
and data base schema is correct)"
| db totalTweets replies retweets ring |
"openning connection"
db := NBSQLite3Connection on: aDataBaseFile.
db open.
"Querying the data base"
retweets := (db execute: 'select * from tweets where profile="', aProfileName ,'" and type="retweet";') rows size.
replies := (db execute: 'select * from tweets where profile="', aProfileName ,'" and type="reply";') rows size.
totalTweets := (db execute: 'select * from tweets where profile="', aProfileName ,'";') rows size - retweets - replies.
db close.
(totalTweets > 0)
ifFalse: [
self inform: 'There are no tweets in the database for that profile'
]
ifTrue: [
ring := RTPieBuilder new.
ring interaction popup.
ring shape current
innerRadius: 80;
externalRadius: 100.
ring objects: {totalTweets . retweets . replies}.
(ring slice: #value)ifNotNil: [ :group |
group do: [:each | each @ (RTDraggable groupToDrag: group)]
].
ring normalizer distinctColor.
ring build.
^ ring].
]
{ #category : #'data scrapping' }
TweetsCollection >> scrapTweetsFromHtmlFile: aHtmlFile [
"I scraps tweets from a downloaded html file.
On how to download such file for any given public twitter profile look at:
http://blog.databigbang.com/scraping-web-sites-which-dynamically-load-data/
"
| tweetsDump htmlTree tweetsHtml tweet unixTime answersArray tweetsTemp profile |
tweetsDump := aHtmlFile readStream.
htmlTree := Soup fromString: tweetsDump contents.
profile := (((htmlTree findAllTagsByClass: 'ProfileHeaderCard-screennameLink') at: 1) attributeAt: 'href') copyReplaceAll: '/' with: ''.
tweetsHtml := htmlTree findAllTagsByClass: 'tweet'.
tweetsTemp := OrderedCollection new.
tweetsHtml allButLastDo: [:each |
tweet := Tweet new.
(each findAllTagsByClass: '_timestamp') size > 0
ifTrue: [
unixTime := (((each findAllTagsByClass: '_timestamp') at: 1) attributeAt: 'data-time') asInteger.
tweet date: (TimeStamp fromUnixTime: unixTime) asUTC
].
answersArray := each findAllTagsByClass: 'js-retweet-text'.
(answersArray size = 1)
ifTrue: [tweet type: 'retweet']
ifFalse: [
(each attributeAt: 'data-is-reply-to') isString
ifTrue: [tweet type: 'reply']
ifFalse: [tweet type: 'tweet']
].
tweet url: (each attributeAt: 'data-permalink-path').
(each findAllTagsByClass: 'TweetTextSize') size > 0
ifTrue: [tweet message: (((each findAllTagsByClass: 'TweetTextSize') at: 1) text)].
tweet profile: profile.
tweetsTemp add: tweet.
].
self tweets: tweetsTemp.
]
{ #category : #accessing }
TweetsCollection >> tweets [
^ tweets ifNil: [tweets := OrderedCollection new]
]
{ #category : #accessing }
TweetsCollection >> tweets: anOrderedCollection [
tweets := anOrderedCollection
]

View File

@ -162,6 +162,26 @@ TwitterProfile >> lastTweets: anObject [
lastTweets := anObject
]
{ #category : #'data storage / persistance' }
TwitterProfile >> loadDataFor: aProfileName fromDatabase: aDataBaseFile [
| db queryResults |
"openning connection"
db := NBSQLite3Connection on: aDataBaseFile.
db open.
"Querying the data base"
queryResults := (db execute: 'SELECT * FROM profiles WHERE screenName="',aProfileName,'";') rows at: 1.
db close.
self
screenName: (queryResults at: 'screenName');
name: (queryResults at: 'name');
avatar: (queryResults at: 'avatar');
bio: (queryResults at: 'bio');
favs: (queryResults at: 'favs');
followers: (queryResults at: 'followers');
following: (queryResults at: 'following')
]
{ #category : #'data storage / persistance' }
TwitterProfile >> loadDataFromFile: aFileReference [
"Opens the twitter profile from aFileReference stored in the STON format"
@ -226,6 +246,39 @@ TwitterProfile >> name: anObject [
name := anObject
]
{ #category : #'data storage / persistance' }
TwitterProfile >> populateDataBase: aDataBaseFile [
"I populate a SQLite database file with myself data"
| db |
"openning connection"
db := NBSQLite3Connection on: aDataBaseFile.
db open.
"Creating the data base tweets schema"
db execute:
'create table if not exists profiles (
screenName text primary key,
name text,
avatar blob,
bio text,
favs integer,
followers integer,
following integer,
location text
);'.
"Populating the database"
db execute: 'INSERT INTO profiles values (?, ?, ?, ?, ?, ?, ?, ?);'
with: {
self screenName.
self name.
self avatar.
self bio.
self favs.
self followers.
self following.
self location}.
db close.
]
{ #category : #'data storage / persistance' }
TwitterProfile >> saveToFile: aFileReference [
"Saves the twitter profile to aFileReference in the STON format"
@ -235,6 +288,24 @@ TwitterProfile >> saveToFile: aFileReference [
stream nextPutAll: (STON toStringPretty: self).
]
{ #category : #dataweek }
TwitterProfile >> sayBye [
"Just says hello to all the people which is listening. A dummy example on how to create new messages"
Transcript open.
Transcript show: 'Adios, perfil de Twitter, despidiéndose. Pásala bueno ;-)'
]
{ #category : #dataweek }
TwitterProfile >> sayHello [
"Just says hello to all the people which is listening. A dummy example on how to create new messages"
Transcript open.
Transcript show: 'Hola! soy un perfil de Twitter :-)'
]
{ #category : #'data scrapping' }
TwitterProfile >> scrapAvatarFrom: aHtmlString [
"Finds the avatar in a twitter's main page profile, scales it (200x200), cast it agains different formats (jpeg, png) and returns it"
@ -245,15 +316,14 @@ TwitterProfile >> scrapAvatarFrom: aHtmlString [
(avatarUrl asLowercase endsWith: '.png')
ifTrue: [avatarImage := ZnEasy getPng: avatarUrl].
((avatarUrl asLowercase endsWith: '.jpeg') or: (avatarUrl asLowercase endsWith: '.jpg'))
ifTrue: [
avatarImage := ZnEasy get: avatarUrl.
"(PNGReadWriter on: avatarImage ) nextPutImage: (JPEGReadWriter on: avatarImage )" ].
ifTrue: [avatarImage := ZnEasy getJpeg: avatarUrl].
^avatarImage.
]
{ #category : #'data scrapping' }
TwitterProfile >> scrapDataFromProfile: aProfileName [
"Scraps data from aProfileName and fills out the TwitterProfile. The profile name is the last part of a twitter profile url
TwitterProfile >> scrapDataForProfile: aProfileName [
"Scraps data from aProfileName and fills out the TwitterProfile.
The profile name is the last part of a twitter profile url
(i.e: 'https://twitter.com/aProfileName')."
| client source numericalData anUrl |
@ -282,6 +352,24 @@ TwitterProfile >> scrapDataFromProfile: aProfileName [
]
{ #category : #'data scrapping' }
TwitterProfile >> scrapFollowersForProfile: aProfileName [
"Scraps data from a predefined profile name"
| client source numericalData anUrl |
anUrl := 'https://twitter.com/', aProfileName.
client := ZnClient new.
client get: anUrl.
client isSuccess
ifTrue:[
source := Soup fromString: (client) contents asString.
numericalData := (source findAllTagsByClass: 'ProfileNav-value') collect:[:each | each text].
followers := self asNumber: (numericalData at: 3).
]
ifFalse:[self inform: 'Algo salió mal. Verifique su conexión a Internet y que el contenido buscado estén disponibles'].
^ self followers
]
{ #category : #'data scrapping' }
TwitterProfile >> scrapTweetsFromFile: aFile [
"Scraps tweets data from aFile, wich contains tweets scrapped from a public profile."